Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3aadf54

Browse files
committed
Adds support for multiple eligible incremental files in a single build. Swap to full build when the incremental queue would take longer. Fixes #4274
1 parent 6043b6c commit 3aadf54

16 files changed

Lines changed: 226 additions & 390 deletions

cmd.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,11 @@ async function exec() {
114114
// Only relevant for watch/serve
115115
elev.setIgnoreInitial(argv["ignore-initial"]);
116116

117+
// v4.0.0-alpha.8 multiple now supported via:
118+
// --incremental=one.md --incremental=two.md => ["one.md", "two.md"]
119+
// --incremental="one.md,two.md"
117120
if(argv.incremental) {
118-
elev.setIncrementalFile(argv.incremental);
121+
elev.setIncrementalFiles(argv.incremental);
119122
} else if(argv.incremental !== undefined) {
120123
elev.setIncrementalBuild(argv.incremental === "" || argv.incremental);
121124
}

src/CoreMinimal.js

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,7 @@ export class MinimalCore {
377377
* @param {boolean} isIncremental - Shall Eleventy run in incremental build mode and only write the files that trigger watch updates
378378
*/
379379
setIncrementalBuild(isIncremental) {
380-
this.isIncremental = !!isIncremental;
381-
382-
if (this.writer) {
383-
this.writer.setIncrementalBuild(this.isIncremental);
384-
}
380+
this.isIncremental = Boolean(isIncremental);
385381
}
386382

387383
/**
@@ -488,7 +484,6 @@ export class MinimalCore {
488484
this.writer.logger = this.logger;
489485
this.writer.extensionMap = this.extensionMap;
490486
this.writer.setRunInitialBuild(this.isRunInitialBuild);
491-
this.writer.setIncrementalBuild(this.isIncremental);
492487

493488
let debugStr = `Directories:
494489
Input:
@@ -662,29 +657,37 @@ Verbose Output: ${this.verboseMode}`;
662657
* This method is also wired up to the CLI --incremental=incrementalFile
663658
*
664659
* @method
665-
* @param {string} incrementalFile - File path (added or modified in a project)
660+
* @param {Array|string} incrementalFiles - File path (added or modified in a project)
666661
*/
667-
setIncrementalFile(incrementalFile) {
668-
if (incrementalFile) {
669-
// This used to also setIgnoreInitial(true) but was changed in 3.0.0-alpha.14
670-
this.setIncrementalBuild(true);
662+
setIncrementalFiles(incrementalFiles) {
663+
if (!incrementalFiles) {
664+
return;
665+
}
671666

672-
this.programmaticApiIncrementalFile = TemplatePath.addLeadingDotSlash(incrementalFile);
667+
// This used to also setIgnoreInitial(true) but was changed in 3.0.0-alpha.14
668+
this.setIncrementalBuild(true);
673669

674-
// Used to determind template relevance for compile cache keys
675-
this.eleventyConfig.setPreviousBuildModifiedFile(incrementalFile);
670+
let files;
671+
if (typeof incrementalFiles === "string") {
672+
files = incrementalFiles.split(",");
673+
} else if (Array.isArray(incrementalFiles)) {
674+
files = incrementalFiles;
675+
} else {
676+
throw new Error("Invalid argument for setIncrementalFiles, needs string or Array");
676677
}
677-
}
678678

679-
unsetIncrementalFile() {
680-
// only applies to initial build, no re-runs (--watch/--serve)
681-
if (this.programmaticApiIncrementalFile) {
682-
// this.setIgnoreInitial(false);
683-
this.programmaticApiIncrementalFile = undefined;
684-
}
679+
let normalized = files.map((p) => TemplatePath.addLeadingDotSlash(p));
680+
681+
// Saved from --incremental
682+
this.programmaticApiIncrementalFile = normalized;
685683

686-
// reset back to false
687-
this.setIgnoreInitial(false);
684+
// Also used to determind template relevance for compile cache keys
685+
this.watchQueue?.setActiveQueue(normalized);
686+
}
687+
688+
// Backwards compatibility (rename in v4.0.0-alpha.8)
689+
setIncrementalFile(incrementalFile) {
690+
this.setIncrementalFiles(incrementalFile);
688691
}
689692

690693
/**
@@ -772,6 +775,20 @@ Verbose Output: ${this.verboseMode}`;
772775
throw new Error("Feature removed in Eleventy v4: https://github.com/11ty/eleventy/issues/3382");
773776
}
774777

778+
/*
779+
* If the active queue has a mix of template/non-template files (includes, etc), swap to run a full build
780+
*/
781+
isIncrementalBuildPossible(queuedFiles = []) {
782+
let hasNonTemplateFiles = Boolean(
783+
queuedFiles.find((path) => !this.directories.isTemplateFile(path)),
784+
);
785+
if (hasNonTemplateFiles) {
786+
return false;
787+
}
788+
789+
return true;
790+
}
791+
775792
/**
776793
* tbd.
777794
*
@@ -797,10 +814,10 @@ Verbose Output: ${this.verboseMode}`;
797814
);
798815
}
799816

800-
let incrementalFile =
801-
this.programmaticApiIncrementalFile || this.watchQueue?.getIncrementalFile();
802-
if (incrementalFile) {
803-
this.writer.setIncrementalFile(incrementalFile);
817+
let incrementalFiles = this.watchQueue?.getActiveQueue() || [];
818+
if (this.isIncremental && this.isIncrementalBuildPossible(incrementalFiles)) {
819+
// This really controls whether a build is incremental or not (internally)
820+
this.writer.setIncrementalFiles(incrementalFiles);
804821
}
805822

806823
let returnObj;
@@ -856,8 +873,10 @@ Verbose Output: ${this.verboseMode}`;
856873
returnObj = [resolved.passthroughCopy, resolved.templates];
857874
}
858875

859-
this.unsetIncrementalFile();
860-
this.writer.resetIncrementalFile();
876+
// always reset after first build
877+
this.setIgnoreInitial(false);
878+
this.writer.resetIncremental();
879+
this.config.events.emit("eleventy#previousqueue", incrementalFiles);
861880

862881
eventsArg.uses = this.eleventyConfig.usesGraph.map;
863882
await this.config.events.emit("afterBuild", eventsArg);

src/Eleventy.js

Lines changed: 46 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,9 @@ export default class Eleventy extends Core {
3434
// super(input, output, options, eleventyConfig);
3535
// }
3636

37-
/**
38-
* Sets the incremental build mode.
39-
*
40-
* @param {boolean} isIncremental - Shall Eleventy run in incremental build mode and only write the files that trigger watch updates
41-
*/
42-
setIncrementalBuild(isIncremental) {
43-
super.setIncrementalBuild(isIncremental);
44-
45-
if (this.#watchQueue) {
46-
this.watchQueue.incremental = !!isIncremental;
47-
}
48-
}
49-
5037
get watchQueue() {
5138
if (!this.#watchQueue) {
5239
this.#watchQueue = new WatchQueue();
53-
this.#watchQueue.incremental = this.isIncremental;
5440
}
5541
return this.#watchQueue;
5642
}
@@ -100,33 +86,12 @@ export default class Eleventy extends Core {
10086
* @param {string} changedFilePath - File that triggered a re-run (added or modified)
10187
* @param {boolean} [isResetConfig] - are we doing a config reset
10288
*/
103-
async #addFileToWatchQueue(changedFilePath, isResetConfig) {
104-
let isAdded = this.watchQueue.addToPendingQueue(changedFilePath);
105-
if (!isAdded) {
106-
return;
107-
}
108-
109-
// Currently this is only for 11ty.js deps but should be extended with usesGraph
110-
let usedByDependants = [];
111-
if (this.watchTargets) {
112-
usedByDependants = this.watchTargets.getDependantsOf(
113-
TemplatePath.addLeadingDotSlash(changedFilePath),
114-
);
115-
}
89+
#resetFileInWatchQueue(changedFilePath, isResetConfig) {
90+
// v3.1.0: `eleventy.templateModified` is no longer used internally
91+
// v4.0.0-alpha.8 `eleventy.templateModified` event removed
11692

117-
let relevantLayouts = this.eleventyConfig.usesGraph.getLayoutsUsedBy(changedFilePath);
118-
119-
// `eleventy.templateModified` is no longer used internally, remove in a future major version.
120-
eventBus.emit("eleventy.templateModified", changedFilePath, {
121-
usedByDependants,
122-
relevantLayouts,
123-
});
124-
125-
// These listeners are *global*, not cleared even on config reset
126-
eventBus.emit("eleventy.resourceModified", changedFilePath, usedByDependants, {
127-
viaConfigReset: isResetConfig,
128-
relevantLayouts,
129-
});
93+
// These listeners are *global*, not cleared even on config reset (v4.0.0-alpha.8 removed some arguments here)
94+
eventBus.emit("eleventy.resourceModified", changedFilePath);
13095

13196
this.config.events.emit("eleventy#templateModified", changedFilePath);
13297
}
@@ -176,27 +141,25 @@ export default class Eleventy extends Core {
176141
);
177142
}
178143

179-
async #rewatch(opts = {}) {
180-
let { skipIncremental } = opts;
181-
144+
async #rewatch() {
182145
if (this.watchQueue.isBuildRunning()) {
183146
// this.logger.forceLog("Waiting for previous build to finish…");
184147
return;
185148
}
186149

187-
if (skipIncremental) {
188-
// skip incremental when pending queue size > 2 and has non-template files
189-
this.unsetIncrementalFile();
190-
this.writer?.resetIncrementalFile();
191-
this.watchQueue.setIncrementalOverride(true);
192-
this.watchQueue.clearPendingQueue();
193-
}
194-
195150
this.watchQueue.startBuild();
196151

197152
let queue = this.watchQueue.getActiveQueue();
198153
let isResetConfig = this.#shouldResetConfig(queue);
199-
this.logger.forceLog("Build starting…" + (isResetConfig ? " (configuration reset)" : ""));
154+
155+
for (let p of queue) {
156+
this.#resetFileInWatchQueue(p, isResetConfig);
157+
}
158+
159+
this.logger.forceLog(
160+
`Build starting${queue.length > 1 ? ` (${queue.length} queued changes)` : ""}…` +
161+
(isResetConfig ? " (configuration reset)" : ""),
162+
);
200163

201164
await this.config.events.emit("beforeWatch", queue);
202165
await this.config.events.emit("eleventy.beforeWatch", queue);
@@ -241,15 +204,13 @@ export default class Eleventy extends Core {
241204
);
242205
});
243206

244-
let files = this.watchQueue.getActiveQueue();
245-
246207
// Maps passthrough copy files to output URLs for CSS live reload
247208
let stylesheetUrls = new Set();
248209
for (let entry of passthroughCopyResults) {
249210
for (let filepath in entry.map) {
250211
if (
251212
filepath.endsWith(".css") &&
252-
files.includes(TemplatePath.addLeadingDotSlash(filepath))
213+
queue.includes(TemplatePath.addLeadingDotSlash(filepath))
253214
) {
254215
stylesheetUrls.add(
255216
"/" + TemplatePath.stripLeadingSubPath(entry.map[filepath], this.outputDir),
@@ -270,7 +231,7 @@ export default class Eleventy extends Core {
270231
});
271232

272233
await this.eleventyServe.reload({
273-
files,
234+
files: queue,
274235
subtype: onlyCssChanges ? "css" : undefined,
275236
build: {
276237
stylesheets: Array.from(stylesheetUrls),
@@ -288,24 +249,7 @@ export default class Eleventy extends Core {
288249
// Re-fetch
289250
let pendingQueue = this.watchQueue.getPendingQueue();
290251
if (pendingQueue.length > 0) {
291-
let hasNonTemplateFiles = Boolean(
292-
pendingQueue.find((path) => !this.directories.isTemplateFile(path)),
293-
);
294-
let skipIncremental = pendingQueue.length > 1 && hasNonTemplateFiles;
295-
296-
if (!skipIncremental && this.isIncremental) {
297-
this.logger.forceLog(
298-
`Build queued for: ${TemplatePath.stripLeadingDotSlash(pendingQueue.slice(0, 1).pop())}${pendingQueue.length > 1 ? ` (1/${pendingQueue.length} changes)` : ""}`,
299-
);
300-
} else {
301-
this.logger.forceLog(
302-
`Build queued… (${pendingQueue.length} change${pendingQueue.length !== 1 ? "s" : ""})`,
303-
);
304-
}
305-
306-
await this.#rewatch({
307-
skipIncremental,
308-
});
252+
await this.#rewatch();
309253
} else if (!this.#interrupted) {
310254
// also logs in startWatch for initial build
311255
this.logger.forceLog(`Waiting…`);
@@ -329,36 +273,33 @@ export default class Eleventy extends Core {
329273
return this.bench.get("Watcher");
330274
}
331275

332-
static async wait(timeMs) {
333-
let { promise, resolve, reject } = withResolvers();
276+
async waitThrottle() {
277+
if (this.#watchDelay) {
278+
clearTimeout(this.#watchDelay);
279+
}
334280

335-
let id = setTimeout(resolve, timeMs);
281+
if (this.config.watchThrottleWaitTime > 0) {
282+
let { promise, resolve } = withResolvers();
336283

337-
await promise;
284+
this.#watchDelay = setTimeout(resolve, this.config.watchThrottleWaitTime);
338285

339-
return id;
286+
return promise;
287+
}
340288
}
341289

290+
// Triggers when files are modified on file system
342291
async triggerWatchRunForPath(path) {
343-
path = TemplatePath.normalize(path);
292+
this.watchQueue.addToPendingQueue(path);
344293

345-
try {
346-
let isResetConfig = this.#shouldResetConfig([path]);
347-
this.#addFileToWatchQueue(path, isResetConfig);
348-
349-
if (this.#watchDelay) {
350-
clearTimeout(this.#watchDelay);
351-
}
352-
353-
if (this.config.watchThrottleWaitTime) {
354-
this.#watchDelay = Eleventy.wait(this.config.watchThrottleWaitTime);
355-
}
294+
await this.waitThrottle();
356295

296+
try {
357297
await this.#rewatch();
358298
} catch (e) {
299+
this.watchQueue.finishBuild();
300+
359301
if (e instanceof EleventyBaseError) {
360302
this.errorHandler.error(e, "Eleventy watch error");
361-
this.watchQueue.finishBuild();
362303
} else {
363304
this.errorHandler.fatal(e, "Eleventy fatal watch error");
364305
await this.close();
@@ -420,34 +361,38 @@ export default class Eleventy extends Core {
420361
// Emulated passthrough copy logs from the server
421362
if (!this.eleventyServe.isEmulatedPassthroughCopyMatch(path)) {
422363
this.logger.forceLog(
423-
`File changed: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}`,
364+
`File changed: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}${this.watchQueue.isBuildRunning() ? " (queued for next build)" : ""}`,
424365
);
425366
}
426367

427-
await this.triggerWatchRunForPath(path);
368+
// don’t await
369+
this.triggerWatchRunForPath(path);
428370
});
429371

430372
this.watcher.on("add", async (path) => {
431373
// Emulated passthrough copy logs from the server
432374
if (!this.eleventyServe.isEmulatedPassthroughCopyMatch(path)) {
433375
this.logger.forceLog(
434-
`File added: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}`,
376+
`File added: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}${this.watchQueue.isBuildRunning() ? " (queued for next build)" : ""}`,
435377
);
436378
}
437379

438380
this.fileSystemSearch.add(path);
439-
await this.triggerWatchRunForPath(path);
381+
// don’t await
382+
this.triggerWatchRunForPath(path);
440383
});
441384

442385
this.watcher.on("unlink", async (path) => {
443386
// Emulated passthrough copy logs from the server
444387
if (!this.eleventyServe.isEmulatedPassthroughCopyMatch(path)) {
445388
this.logger.forceLog(
446-
`File deleted: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}`,
389+
`File deleted: ${TemplatePath.stripLeadingDotSlash(TemplatePath.standardizeFilePath(path))}${this.watchQueue.isBuildRunning() ? " (queued for next build)" : ""}`,
447390
);
448391
}
392+
449393
this.fileSystemSearch.delete(path);
450-
await this.triggerWatchRunForPath(path);
394+
// don’t await
395+
this.triggerWatchRunForPath(path);
451396
});
452397
}
453398

@@ -588,8 +533,9 @@ Arguments:
588533
--incremental
589534
Only build the files that have changed. Best with watch/serve.
590535
591-
--incremental=filename.md
592-
Does not require watch/serve. Run an incremental build targeting a single file.
536+
--incremental=first.md,second.md
537+
--incremental=first.md --incremental=second.md
538+
Does not require watch/serve. Run an incremental build targeting one or more files.
593539
594540
--ignore-initial
595541
Start without a build; build when files change. Works best with watch/serve/incremental.

0 commit comments

Comments
 (0)