@11ty/eleventy
Advanced tools
+128
| import debugUtil from "debug"; | ||
| import { TemplatePath } from "@11ty/eleventy-utils"; | ||
| import chokidar from "chokidar"; | ||
| import { isGlobMatch } from "./Util/GlobMatcher.js"; | ||
| import { GlobStripper } from "./Util/GlobStripper.js"; | ||
| const debug = debugUtil("Eleventy:Watch"); | ||
| export class Watch { | ||
| /** @type {module:chokidar} */ | ||
| #chokidar; | ||
| /** @type {Set} */ | ||
| #watchedGlobs = []; | ||
| /** @type {Set} */ | ||
| #ignoredGlobs = []; | ||
| constructor(config) { | ||
| if (!config || config.constructor.name !== "TemplateConfig") { | ||
| throw new Error("Internal error: Missing or invalid `config` argument."); | ||
| } | ||
| this.templateConfig = config; | ||
| } | ||
| getChokidarConfig() { | ||
| let options = Object.assign( | ||
| { | ||
| ignoreInitial: true, | ||
| awaitWriteFinish: { | ||
| stabilityThreshold: 150, | ||
| pollInterval: 25, | ||
| }, | ||
| }, | ||
| this.templateConfig.userConfig.chokidarConfig, | ||
| ); | ||
| // unsupported: using your own `ignored` | ||
| if (options.ignored) { | ||
| delete options.ignored; | ||
| } | ||
| return options; | ||
| } | ||
| // alias for watchTargets() (backwards compat) | ||
| add(targets = []) { | ||
| this.watchTargets(targets); | ||
| } | ||
| watchTargets(targets = []) { | ||
| let uniqueSet = new Set(); | ||
| for (let target of targets) { | ||
| this.#watchedGlobs.push(TemplatePath.stripLeadingDotSlash(target)); | ||
| // strip globs off of target, chokidar@4 | ||
| let { path } = GlobStripper.parse(target); | ||
| if (path) { | ||
| uniqueSet.add(path); | ||
| } | ||
| } | ||
| this.#chokidar?.add(Array.from(uniqueSet)); | ||
| } | ||
| addIgnores(ignores) { | ||
| for (let target of ignores) { | ||
| this.#ignoredGlobs.push(target); | ||
| } | ||
| } | ||
| #isDirectory(path) { | ||
| return this.templateConfig.existsCache.isDirectory(path); | ||
| } | ||
| async start() { | ||
| let options = this.getChokidarConfig(); | ||
| options.ignored = (filepath) => { | ||
| // don’t ignore root (if specified) | ||
| if (filepath === ".") { | ||
| return false; | ||
| } | ||
| if (this.#ignoredGlobs.length > 0 && isGlobMatch(filepath, this.#ignoredGlobs)) { | ||
| debug("Ignore file (ignore globs)", filepath); | ||
| return true; | ||
| } | ||
| // don’t ignore directories that are not in ignores | ||
| if (this.#isDirectory(filepath)) { | ||
| return false; | ||
| } | ||
| // make sure this matches at least one of the original globs | ||
| if (this.#watchedGlobs.length === 0 || !isGlobMatch(filepath, this.#watchedGlobs)) { | ||
| debug("Ignore file (no glob match)", filepath, this.#watchedGlobs); | ||
| return true; | ||
| } | ||
| return false; | ||
| }; | ||
| // strip globs off of target, chokidar@4 | ||
| let targets = this.#watchedGlobs | ||
| .map((target) => { | ||
| let { path } = GlobStripper.parse(target); | ||
| return path; | ||
| }) | ||
| .filter(Boolean); | ||
| this.#chokidar = chokidar.watch(targets, options); | ||
| // Note: if there are no watch targets the `ready` event doesn’t fire so skip it | ||
| if (targets.length > 0) { | ||
| await new Promise((resolve) => { | ||
| this.#chokidar.on("ready", () => resolve()); | ||
| }); | ||
| } | ||
| } | ||
| on(event, callback) { | ||
| this.#chokidar.on(event, callback); | ||
| } | ||
| async close() { | ||
| return this.#chokidar?.close(); | ||
| } | ||
| } |
| import { TemplatePath } from "@11ty/eleventy-utils"; | ||
| import PathNormalizer from "./Util/PathNormalizer.js"; | ||
| /* Decides when to watch and in what mode to watch | ||
| * Incremental builds don’t batch changes, they queue. | ||
| * Nonincremental builds batch. | ||
| */ | ||
| class WatchQueue { | ||
| constructor() { | ||
| this.incremental = false; | ||
| this.isActive = false; | ||
| this.activeQueue = []; | ||
| } | ||
| isBuildRunning() { | ||
| return this.isActive; | ||
| } | ||
| setBuildRunning() { | ||
| this.isActive = true; | ||
| // pop waiting queue into the active queue | ||
| this.activeQueue = this.popNextActiveQueue(); | ||
| } | ||
| setBuildFinished() { | ||
| this.isActive = false; | ||
| this.activeQueue = []; | ||
| } | ||
| getIncrementalFile() { | ||
| if (this.incremental) { | ||
| return this.activeQueue.length ? this.activeQueue[0] : false; | ||
| } | ||
| return false; | ||
| } | ||
| /* Returns the changed files currently being operated on in the current `watch` build | ||
| * Works with or without incremental (though in incremental only one file per time will be processed) | ||
| */ | ||
| getActiveQueue() { | ||
| if (!this.isActive) { | ||
| return []; | ||
| } else if (this.incremental && this.activeQueue.length === 0) { | ||
| return []; | ||
| } else if (this.incremental) { | ||
| return [this.activeQueue[0]]; | ||
| } | ||
| return this.activeQueue; | ||
| } | ||
| _queueMatches(file) { | ||
| let filterCallback; | ||
| if (typeof file === "function") { | ||
| filterCallback = file; | ||
| } else { | ||
| filterCallback = (path) => path === file; | ||
| } | ||
| return this.activeQueue.filter(filterCallback); | ||
| } | ||
| hasAllQueueFiles(file) { | ||
| return ( | ||
| this.activeQueue.length > 0 && this.activeQueue.length === this._queueMatches(file).length | ||
| ); | ||
| } | ||
| hasQueuedFile(file) { | ||
| if (file) { | ||
| return this._queueMatches(file).length > 0; | ||
| } | ||
| return false; | ||
| } | ||
| hasQueuedFiles(files) { | ||
| for (const file of files) { | ||
| if (this.hasQueuedFile(file)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| get pendingQueue() { | ||
| if (!this._queue) { | ||
| this._queue = []; | ||
| } | ||
| return this._queue; | ||
| } | ||
| set pendingQueue(value) { | ||
| this._queue = value; | ||
| } | ||
| addToPendingQueue(path) { | ||
| if (path) { | ||
| path = PathNormalizer.normalizeSeperator(TemplatePath.addLeadingDotSlash(path)); | ||
| this.pendingQueue.push(path); | ||
| } | ||
| } | ||
| getPendingQueueSize() { | ||
| return this.pendingQueue.length; | ||
| } | ||
| getPendingQueue() { | ||
| return this.pendingQueue; | ||
| } | ||
| getActiveQueueSize() { | ||
| return this.activeQueue.length; | ||
| } | ||
| // returns array | ||
| popNextActiveQueue() { | ||
| if (this.incremental) { | ||
| return this.pendingQueue.length ? [this.pendingQueue.shift()] : []; | ||
| } | ||
| let ret = this.pendingQueue.slice(); | ||
| this.pendingQueue = []; | ||
| return ret; | ||
| } | ||
| } | ||
| export default WatchQueue; |
| import { TemplatePath } from "@11ty/eleventy-utils"; | ||
| import { DepGraph } from "dependency-graph"; | ||
| import JavaScriptDependencies from "./Util/JavaScriptDependencies.js"; | ||
| import eventBus from "./EventBus.js"; | ||
| export default class WatchTargets { | ||
| #templateConfig; | ||
| constructor(templateConfig) { | ||
| this.targets = new Set(); | ||
| this.dependencies = new Set(); | ||
| this.newTargets = new Set(); | ||
| this.isEsm = false; | ||
| this.graph = new DepGraph(); | ||
| this.#templateConfig = templateConfig; | ||
| } | ||
| setProjectUsingEsm(isEsmProject) { | ||
| this.isEsm = !!isEsmProject; | ||
| } | ||
| isJavaScriptDependency(path) { | ||
| return this.dependencies.has(path); | ||
| } | ||
| reset() { | ||
| this.newTargets = new Set(); | ||
| } | ||
| addToDependencyGraph(parent, deps) { | ||
| if (!this.graph.hasNode(parent)) { | ||
| this.graph.addNode(parent); | ||
| } | ||
| for (let dep of deps) { | ||
| if (!this.graph.hasNode(dep)) { | ||
| this.graph.addNode(dep); | ||
| } | ||
| this.graph.addDependency(parent, dep); | ||
| } | ||
| } | ||
| uses(parent, dep) { | ||
| return this.getDependenciesOf(parent).includes(dep); | ||
| } | ||
| getDependenciesOf(parent) { | ||
| if (!this.graph.hasNode(parent)) { | ||
| return []; | ||
| } | ||
| return this.graph.dependenciesOf(parent); | ||
| } | ||
| getDependantsOf(child) { | ||
| if (!this.graph.hasNode(child)) { | ||
| return []; | ||
| } | ||
| return this.graph.dependantsOf(child); | ||
| } | ||
| addRaw(targets, isDependency) { | ||
| for (let target of targets) { | ||
| let path = TemplatePath.addLeadingDotSlash(target); | ||
| if (!this.targets.has(target)) { | ||
| this.newTargets.add(path); | ||
| } | ||
| this.targets.add(path); | ||
| if (isDependency) { | ||
| this.dependencies.add(path); | ||
| } | ||
| } | ||
| } | ||
| static normalize(targets) { | ||
| if (!targets) { | ||
| return []; | ||
| } else if (Array.isArray(targets)) { | ||
| return targets; | ||
| } | ||
| return [targets]; | ||
| } | ||
| // add only a target | ||
| add(targets) { | ||
| this.addRaw(WatchTargets.normalize(targets)); | ||
| } | ||
| static normalizeToGlobs(targets) { | ||
| return WatchTargets.normalize(targets).map((entry) => | ||
| TemplatePath.convertToRecursiveGlobSync(entry), | ||
| ); | ||
| } | ||
| // add only a target’s dependencies | ||
| async addDependencies(targets, filterCallback) { | ||
| if (this.#templateConfig && !this.#templateConfig.shouldSpiderJavaScriptDependencies()) { | ||
| return; | ||
| } | ||
| targets = WatchTargets.normalize(targets); | ||
| let deps = await JavaScriptDependencies.getDependencies(targets, this.isEsm); | ||
| if (filterCallback) { | ||
| deps = deps.filter(filterCallback); | ||
| } | ||
| for (let target of targets) { | ||
| this.addToDependencyGraph(target, deps); | ||
| } | ||
| this.addRaw(deps, true); | ||
| } | ||
| setWriter(templateWriter) { | ||
| this.writer = templateWriter; | ||
| } | ||
| clearImportCacheFor(filePathArray) { | ||
| let paths = new Set(); | ||
| for (const filePath of filePathArray) { | ||
| paths.add(filePath); | ||
| // Delete from require cache so that updates to the module are re-required | ||
| let importsTheChangedFile = this.getDependantsOf(filePath); | ||
| for (let dep of importsTheChangedFile) { | ||
| paths.add(dep); | ||
| } | ||
| let isImportedInTheChangedFile = this.getDependenciesOf(filePath); | ||
| for (let dep of isImportedInTheChangedFile) { | ||
| paths.add(dep); | ||
| } | ||
| // Use GlobalDependencyMap | ||
| if (this.#templateConfig) { | ||
| for (let dep of this.#templateConfig.usesGraph.getDependantsFor(filePath)) { | ||
| paths.add(dep); | ||
| } | ||
| } | ||
| } | ||
| eventBus.emit("eleventy.importCacheReset", paths); | ||
| } | ||
| getNewTargetsSinceLastReset() { | ||
| return Array.from(this.newTargets); | ||
| } | ||
| getTargets() { | ||
| return Array.from(this.targets); | ||
| } | ||
| } |
+4
-4
| { | ||
| "name": "@11ty/eleventy", | ||
| "version": "4.0.0-alpha.3", | ||
| "version": "4.0.0-alpha.4", | ||
| "description": "A simpler static site generator.", | ||
@@ -127,5 +127,5 @@ "publishConfig": { | ||
| "sass": "^1.89.2", | ||
| "simple-git-hooks": "^2.13.0", | ||
| "simple-git-hooks": "^2.13.1", | ||
| "tsx": "^4.20.3", | ||
| "typescript": "^5.8.3", | ||
| "typescript": "^5.9.2", | ||
| "vue": "^3.5.18", | ||
@@ -138,3 +138,3 @@ "zod": "^3.25.76", | ||
| "@11ty/dependency-tree-esm": "^2.0.0", | ||
| "@11ty/eleventy-dev-server": "3.0.0-alpha.2", | ||
| "@11ty/eleventy-dev-server": "3.0.0-alpha.3", | ||
| "@11ty/eleventy-plugin-bundle": "^3.0.6", | ||
@@ -141,0 +141,0 @@ "@11ty/eleventy-utils": "^2.0.7", |
@@ -794,3 +794,3 @@ import debugUtil from "debug"; | ||
| let incrementalFile = | ||
| this.programmaticApiIncrementalFile || this.watchManager?.getIncrementalFile(); | ||
| this.programmaticApiIncrementalFile || this.watchQueue?.getIncrementalFile(); | ||
| if (incrementalFile) { | ||
@@ -797,0 +797,0 @@ this.writer.setIncrementalFile(incrementalFile); |
+33
-114
@@ -8,4 +8,5 @@ import { relative } from "node:path"; | ||
| import EleventyServe from "./EleventyServe.js"; | ||
| import EleventyWatch from "./EleventyWatch.js"; | ||
| import WatchTargets from "./EleventyWatchTargets.js"; | ||
| import { Watch } from "./Watch.js"; | ||
| import WatchQueue from "./WatchQueue.js"; | ||
| import WatchTargets from "./WatchTargets.js"; | ||
| import EleventyBaseError from "./Errors/EleventyBaseError.js"; | ||
@@ -20,3 +21,2 @@ | ||
| import { withResolvers } from "./Util/PromiseUtil.js"; | ||
| import GlobRemap from "./Util/GlobRemap.js"; | ||
@@ -29,8 +29,5 @@ const debug = debugUtil("Eleventy"); | ||
| /** @type {module:chokidar} */ | ||
| #chokidar; | ||
| /** @type {WatchQueue} */ | ||
| #watchQueue; | ||
| /** @type {EleventyWatch} */ | ||
| #watchManager; | ||
| // constructor(input, output, options = {}, eleventyConfig = null) { | ||
@@ -48,13 +45,13 @@ // super(input, output, options, eleventyConfig); | ||
| if (this.#watchManager) { | ||
| this.watchManager.incremental = !!isIncremental; | ||
| if (this.#watchQueue) { | ||
| this.watchQueue.incremental = !!isIncremental; | ||
| } | ||
| } | ||
| get watchManager() { | ||
| if (!this.#watchManager) { | ||
| this.#watchManager = new EleventyWatch(); | ||
| this.#watchManager.incremental = this.isIncremental; | ||
| get watchQueue() { | ||
| if (!this.#watchQueue) { | ||
| this.#watchQueue = new WatchQueue(); | ||
| this.#watchQueue.incremental = this.isIncremental; | ||
| } | ||
| return this.#watchManager; | ||
| return this.#watchQueue; | ||
| } | ||
@@ -132,3 +129,3 @@ | ||
| this.watchManager.addToPendingQueue(changedFilePath); | ||
| this.watchQueue.addToPendingQueue(changedFilePath); | ||
| } | ||
@@ -181,9 +178,9 @@ | ||
| async #rewatch(isResetConfig = false) { | ||
| if (this.watchManager.isBuildRunning()) { | ||
| if (this.watchQueue.isBuildRunning()) { | ||
| return; | ||
| } | ||
| this.watchManager.setBuildRunning(); | ||
| this.watchQueue.setBuildRunning(); | ||
| let queue = this.watchManager.getActiveQueue(); | ||
| let queue = this.watchQueue.getActiveQueue(); | ||
@@ -220,3 +217,3 @@ await this.config.events.emit("beforeWatch", queue); | ||
| let newWatchTargets = this.watchTargets.getNewTargetsSinceLastReset(); | ||
| this.watcher.add(newWatchTargets); | ||
| this.watcher.watchTargets(newWatchTargets); | ||
@@ -226,3 +223,3 @@ // Is a CSS input file and is not in the includes folder | ||
| // TODO add additional API for this, maybe a config callback? | ||
| let onlyCssChanges = this.watchManager.hasAllQueueFiles((path) => { | ||
| let onlyCssChanges = this.watchQueue.hasAllQueueFiles((path) => { | ||
| return ( | ||
@@ -235,3 +232,3 @@ path.endsWith(".css") && | ||
| let files = this.watchManager.getActiveQueue(); | ||
| let files = this.watchQueue.getActiveQueue(); | ||
@@ -278,5 +275,5 @@ // Maps passthrough copy files to output URLs for CSS live reload | ||
| this.watchManager.setBuildFinished(); | ||
| this.watchQueue.setBuildFinished(); | ||
| let queueSize = this.watchManager.getPendingQueueSize(); | ||
| let queueSize = this.watchQueue.getPendingQueueSize(); | ||
| if (queueSize > 0) { | ||
@@ -299,16 +296,2 @@ this.logger.log( | ||
| get chokidar() { | ||
| if (!this.#chokidar) { | ||
| // We use a string module name and try/catch here to hide this from the zisi and esbuild serverless bundlers | ||
| // eslint-disable-next-line no-useless-catch | ||
| try { | ||
| let moduleName = "chokidar"; | ||
| this.#chokidar = import(moduleName).then((mod) => mod.default); | ||
| } catch (e) { | ||
| throw e; | ||
| } | ||
| } | ||
| return this.#chokidar; | ||
| } | ||
| /** | ||
@@ -340,36 +323,19 @@ * Set up watchers and benchmarks. | ||
| // setup chokidar instance | ||
| let promises = []; | ||
| promises.push(this.chokidar); | ||
| // Close previous watcher | ||
| if (this.watcher) { | ||
| // Close previous watcher | ||
| promises.push(this.watcher.close()); | ||
| await this.watcher.close(); | ||
| } | ||
| // TODO improve unwatching if JS dependencies are removed (or files are deleted) | ||
| let { targets, cwd, ignores } = await this.getWatchedTargets(); | ||
| let { targets, ignores } = await this.getWatchedTargets(); | ||
| debug("Watching for changes to: %o", targets); | ||
| let options = this.getChokidarConfig(); | ||
| this.watcher = new Watch(this.eleventyConfig); | ||
| this.watcher.watchTargets(targets); | ||
| this.watcher.addIgnores(ignores); | ||
| if (cwd) { | ||
| options.cwd = cwd; | ||
| } | ||
| await this.watcher.start(); | ||
| debug("Ignoring watcher changes to: %o", ignores); | ||
| this.logger.forceLog("Watching…"); | ||
| options.alwaysStat = true; | ||
| options.ignored = (path, stats) => { | ||
| if (stats?.isFile()) { | ||
| let isMatch = this.watchTargets.isWatchMatch(path, ignores); | ||
| if (!isMatch) { | ||
| return true; | ||
| } | ||
| } | ||
| }; | ||
| let [chokidar] = await Promise.all(promises); | ||
| this.watcher = chokidar.watch(targets, options); | ||
| let watchDelay; | ||
@@ -394,3 +360,3 @@ let watchRun = async (path) => { | ||
| this.errorHandler.error(e, "Eleventy watch error"); | ||
| this.watchManager.setBuildFinished(); | ||
| this.watchQueue.setBuildFinished(); | ||
| } else { | ||
@@ -439,8 +405,2 @@ this.errorHandler.fatal(e, "Eleventy fatal watch error"); | ||
| // wait for chokidar to be ready. | ||
| await new Promise((resolve) => { | ||
| this.watcher.on("ready", () => resolve()); | ||
| }); | ||
| this.logger.forceLog("Watching…"); | ||
| // For testability | ||
@@ -491,52 +451,11 @@ return watchRun; | ||
| * @method | ||
| * @returns {Object} containing `cwd` string, `targets` file paths, and `ignores` globs Array | ||
| * @returns {Object} `targets` file paths, and `ignores` globs Array | ||
| */ | ||
| async getWatchedTargets() { | ||
| let rawWatchedGlobs = await this.watchTargets.getTargets(); | ||
| let ignores = this.eleventyFiles.getGlobWatcherIgnores(); | ||
| // Remap all paths to `cwd` if in play (Issue #3854) | ||
| // This is likely unnecessary since #3854 was for tinyglobby it’s probably better to have a cwd | ||
| // anyway to have nice paths passed into chokidar events | ||
| let remapper = new GlobRemap(rawWatchedGlobs); | ||
| let cwd = remapper.getCwd(); | ||
| if (cwd) { | ||
| rawWatchedGlobs = remapper.getInput().map((entry) => { | ||
| return TemplatePath.stripLeadingDotSlash(entry); | ||
| }); | ||
| ignores = remapper.getRemapped(ignores || []); | ||
| } | ||
| ignores = ignores.map((entry) => { | ||
| return TemplatePath.stripLeadingDotSlash(entry); | ||
| }); | ||
| debug("Ignoring watcher changes to: %o", ignores); | ||
| return { | ||
| cwd, | ||
| targets: rawWatchedGlobs, | ||
| ignores, | ||
| targets: await this.watchTargets.getTargets(), | ||
| ignores: this.eleventyFiles.getGlobWatcherIgnores(), | ||
| }; | ||
| } | ||
| getChokidarConfig() { | ||
| let configOptions = this.config.chokidarConfig; | ||
| // unsupported: using your own `ignored` | ||
| delete configOptions.ignored; | ||
| return Object.assign( | ||
| { | ||
| ignoreInitial: true, | ||
| awaitWriteFinish: { | ||
| stabilityThreshold: 150, | ||
| pollInterval: 25, | ||
| }, | ||
| }, | ||
| configOptions, | ||
| ); | ||
| } | ||
| /** | ||
@@ -543,0 +462,0 @@ * Start watching files |
+17
-59
| import debugUtil from "debug"; | ||
| import { Merge, TemplatePath } from "@11ty/eleventy-utils"; | ||
| import { GlobStripper } from "./Util/GlobStripper.js"; | ||
| import { Watch } from "./Watch.js"; | ||
@@ -42,7 +42,5 @@ function stringifyOptions(options) { | ||
| #initOptionsFetched = false; | ||
| #chokidar; | ||
| // these are *not* normalized | ||
| #watchTargets = new Set(); | ||
| // these *are* normalized | ||
| #queuedWatchTargets = new Set(); | ||
| #defaultWatchIgnores = ["**/node_modules/**", ".git"]; | ||
@@ -207,16 +205,13 @@ constructor() { | ||
| // Fix for missing globs in Chokidar@4 | ||
| if (this.options.module === DEFAULT_SERVER_OPTIONS.module) { | ||
| this.options.chokidarOptions ??= {}; | ||
| ((this.options.chokidarOptions.alwaysStat = true), | ||
| (this.options.chokidarOptions.ignored = (filepath, stats) => { | ||
| if (stats?.isFile()) { | ||
| if ( | ||
| !isGlobMatch(filepath, Array.from(this.#watchTargets)) || | ||
| isGlobMatch(filepath, this.#defaultWatchIgnores) | ||
| ) { | ||
| return true; | ||
| } | ||
| } | ||
| })); | ||
| // Fix for missing globs in Chokidar@4 | ||
| let copyWatch = new Watch(this.eleventyConfig); | ||
| copyWatch.watchTargets(this.#watchTargets); | ||
| // Careful with ignores here: https://github.com/11ty/eleventy/issues/1134 | ||
| // copyWatch.addIgnores(); | ||
| await copyWatch.start(); | ||
| this.#chokidar = copyWatch; | ||
| this.options.chokidar = copyWatch; | ||
| } | ||
@@ -228,6 +223,2 @@ | ||
| this.setAliases(this.#aliases); | ||
| if (this.#queuedWatchTargets.size > 0) { | ||
| this.#watch(this.#watchTargets); | ||
| } | ||
| } | ||
@@ -304,3 +295,2 @@ | ||
| this.#watchTargets = new Set(); | ||
| this.#queuedWatchTargets = new Set(); | ||
| } | ||
@@ -319,40 +309,4 @@ | ||
| await this.serve(this._commandLinePort); | ||
| // rewatch the saved watched files (passthrough copy) | ||
| this.#watch(this.#watchTargets); | ||
| } | ||
| queueWatchTargets(globs = []) { | ||
| for (let glob of globs) { | ||
| this.#queuedWatchTargets.add(glob); | ||
| } | ||
| } | ||
| unqueueWatchTargets(globs = []) { | ||
| for (let glob of globs) { | ||
| this.#queuedWatchTargets.delete(glob); | ||
| } | ||
| } | ||
| #watch(globs = []) { | ||
| let normalizedGlobs = Array.from(globs) | ||
| .map((target) => { | ||
| let { path } = GlobStripper.parse(target); | ||
| if (path && path !== ".") { | ||
| return path; | ||
| } | ||
| }) | ||
| .filter(Boolean); | ||
| if (normalizedGlobs.length > 0) { | ||
| if (this._server && "watchFiles" in this.server) { | ||
| this.server.watchFiles(normalizedGlobs); | ||
| this.unqueueWatchTargets(normalizedGlobs); | ||
| } else { | ||
| // server not yet available | ||
| this.queueWatchTargets(normalizedGlobs); | ||
| } | ||
| } | ||
| } | ||
| // checkPassthroughCopyBehavior check is called upstream in Eleventy.js | ||
@@ -363,3 +317,7 @@ watchPassthroughCopy(globs = []) { | ||
| } | ||
| this.#watch(this.#watchTargets); | ||
| // if the watcher has already started, add the targets | ||
| if (this.#chokidar) { | ||
| this.#chokidar.watchTargets(globs); | ||
| } | ||
| } | ||
@@ -366,0 +324,0 @@ |
| import { TemplatePath } from "@11ty/eleventy-utils"; | ||
| import PathNormalizer from "./Util/PathNormalizer.js"; | ||
| /* Decides when to watch and in what mode to watch | ||
| * Incremental builds don’t batch changes, they queue. | ||
| * Nonincremental builds batch. | ||
| */ | ||
| class EleventyWatch { | ||
| constructor() { | ||
| this.incremental = false; | ||
| this.isActive = false; | ||
| this.activeQueue = []; | ||
| } | ||
| isBuildRunning() { | ||
| return this.isActive; | ||
| } | ||
| setBuildRunning() { | ||
| this.isActive = true; | ||
| // pop waiting queue into the active queue | ||
| this.activeQueue = this.popNextActiveQueue(); | ||
| } | ||
| setBuildFinished() { | ||
| this.isActive = false; | ||
| this.activeQueue = []; | ||
| } | ||
| getIncrementalFile() { | ||
| if (this.incremental) { | ||
| return this.activeQueue.length ? this.activeQueue[0] : false; | ||
| } | ||
| return false; | ||
| } | ||
| /* Returns the changed files currently being operated on in the current `watch` build | ||
| * Works with or without incremental (though in incremental only one file per time will be processed) | ||
| */ | ||
| getActiveQueue() { | ||
| if (!this.isActive) { | ||
| return []; | ||
| } else if (this.incremental && this.activeQueue.length === 0) { | ||
| return []; | ||
| } else if (this.incremental) { | ||
| return [this.activeQueue[0]]; | ||
| } | ||
| return this.activeQueue; | ||
| } | ||
| _queueMatches(file) { | ||
| let filterCallback; | ||
| if (typeof file === "function") { | ||
| filterCallback = file; | ||
| } else { | ||
| filterCallback = (path) => path === file; | ||
| } | ||
| return this.activeQueue.filter(filterCallback); | ||
| } | ||
| hasAllQueueFiles(file) { | ||
| return ( | ||
| this.activeQueue.length > 0 && this.activeQueue.length === this._queueMatches(file).length | ||
| ); | ||
| } | ||
| hasQueuedFile(file) { | ||
| if (file) { | ||
| return this._queueMatches(file).length > 0; | ||
| } | ||
| return false; | ||
| } | ||
| hasQueuedFiles(files) { | ||
| for (const file of files) { | ||
| if (this.hasQueuedFile(file)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| get pendingQueue() { | ||
| if (!this._queue) { | ||
| this._queue = []; | ||
| } | ||
| return this._queue; | ||
| } | ||
| set pendingQueue(value) { | ||
| this._queue = value; | ||
| } | ||
| addToPendingQueue(path) { | ||
| if (path) { | ||
| path = PathNormalizer.normalizeSeperator(TemplatePath.addLeadingDotSlash(path)); | ||
| this.pendingQueue.push(path); | ||
| } | ||
| } | ||
| getPendingQueueSize() { | ||
| return this.pendingQueue.length; | ||
| } | ||
| getPendingQueue() { | ||
| return this.pendingQueue; | ||
| } | ||
| getActiveQueueSize() { | ||
| return this.activeQueue.length; | ||
| } | ||
| // returns array | ||
| popNextActiveQueue() { | ||
| if (this.incremental) { | ||
| return this.pendingQueue.length ? [this.pendingQueue.shift()] : []; | ||
| } | ||
| let ret = this.pendingQueue.slice(); | ||
| this.pendingQueue = []; | ||
| return ret; | ||
| } | ||
| } | ||
| export default EleventyWatch; |
| import { TemplatePath } from "@11ty/eleventy-utils"; | ||
| import { DepGraph } from "dependency-graph"; | ||
| import { isGlobMatch } from "./Util/GlobMatcher.js"; | ||
| import { GlobStripper } from "./Util/GlobStripper.js"; | ||
| import JavaScriptDependencies from "./Util/JavaScriptDependencies.js"; | ||
| import eventBus from "./EventBus.js"; | ||
| export default class WatchTargets { | ||
| #templateConfig; | ||
| constructor(templateConfig) { | ||
| this.targets = new Set(); | ||
| this.dependencies = new Set(); | ||
| this.newTargets = new Set(); | ||
| this.globMatch = new Set(); | ||
| this.isEsm = false; | ||
| this.graph = new DepGraph(); | ||
| this.#templateConfig = templateConfig; | ||
| } | ||
| setProjectUsingEsm(isEsmProject) { | ||
| this.isEsm = !!isEsmProject; | ||
| } | ||
| isJavaScriptDependency(path) { | ||
| return this.dependencies.has(path); | ||
| } | ||
| reset() { | ||
| this.newTargets = new Set(); | ||
| } | ||
| addToDependencyGraph(parent, deps) { | ||
| if (!this.graph.hasNode(parent)) { | ||
| this.graph.addNode(parent); | ||
| } | ||
| for (let dep of deps) { | ||
| if (!this.graph.hasNode(dep)) { | ||
| this.graph.addNode(dep); | ||
| } | ||
| this.graph.addDependency(parent, dep); | ||
| } | ||
| } | ||
| uses(parent, dep) { | ||
| return this.getDependenciesOf(parent).includes(dep); | ||
| } | ||
| getDependenciesOf(parent) { | ||
| if (!this.graph.hasNode(parent)) { | ||
| return []; | ||
| } | ||
| return this.graph.dependenciesOf(parent); | ||
| } | ||
| getDependantsOf(child) { | ||
| if (!this.graph.hasNode(child)) { | ||
| return []; | ||
| } | ||
| return this.graph.dependantsOf(child); | ||
| } | ||
| addRaw(targets, isDependency) { | ||
| for (let target of targets) { | ||
| let path = TemplatePath.addLeadingDotSlash(target); | ||
| if (!this.targets.has(target)) { | ||
| this.newTargets.add(path); | ||
| } | ||
| this.targets.add(path); | ||
| if (isDependency) { | ||
| this.dependencies.add(path); | ||
| } | ||
| } | ||
| } | ||
| static normalize(targets) { | ||
| if (!targets) { | ||
| return []; | ||
| } else if (Array.isArray(targets)) { | ||
| return targets; | ||
| } | ||
| return [targets]; | ||
| } | ||
| isWatchMatch(filepath, ignoreGlobs = []) { | ||
| return ( | ||
| isGlobMatch(filepath, Array.from(this.globMatch)) && | ||
| (ignoreGlobs.length > 0 ? !isGlobMatch(filepath, ignoreGlobs) : true) | ||
| ); | ||
| } | ||
| // add only a target | ||
| add(targets) { | ||
| let arrTargets = WatchTargets.normalize(targets) | ||
| .map((target) => { | ||
| let { path } = GlobStripper.parse(target); | ||
| if (!path || path !== ".") { | ||
| this.globMatch.add(TemplatePath.stripLeadingDotSlash(target)); | ||
| } | ||
| if (path && path !== ".") { | ||
| return path; | ||
| } | ||
| }) | ||
| .filter(Boolean); | ||
| this.addRaw(arrTargets); | ||
| } | ||
| static normalizeToGlobs(targets) { | ||
| return WatchTargets.normalize(targets).map((entry) => | ||
| TemplatePath.convertToRecursiveGlobSync(entry), | ||
| ); | ||
| } | ||
| // add only a target’s dependencies | ||
| async addDependencies(targets, filterCallback) { | ||
| if (this.#templateConfig && !this.#templateConfig.shouldSpiderJavaScriptDependencies()) { | ||
| return; | ||
| } | ||
| targets = WatchTargets.normalize(targets); | ||
| let deps = await JavaScriptDependencies.getDependencies(targets, this.isEsm); | ||
| if (filterCallback) { | ||
| deps = deps.filter(filterCallback); | ||
| } | ||
| for (let target of targets) { | ||
| this.addToDependencyGraph(target, deps); | ||
| } | ||
| this.addRaw(deps, true); | ||
| } | ||
| setWriter(templateWriter) { | ||
| this.writer = templateWriter; | ||
| } | ||
| clearImportCacheFor(filePathArray) { | ||
| let paths = new Set(); | ||
| for (const filePath of filePathArray) { | ||
| paths.add(filePath); | ||
| // Delete from require cache so that updates to the module are re-required | ||
| let importsTheChangedFile = this.getDependantsOf(filePath); | ||
| for (let dep of importsTheChangedFile) { | ||
| paths.add(dep); | ||
| } | ||
| let isImportedInTheChangedFile = this.getDependenciesOf(filePath); | ||
| for (let dep of isImportedInTheChangedFile) { | ||
| paths.add(dep); | ||
| } | ||
| // Use GlobalDependencyMap | ||
| if (this.#templateConfig) { | ||
| for (let dep of this.#templateConfig.usesGraph.getDependantsFor(filePath)) { | ||
| paths.add(dep); | ||
| } | ||
| } | ||
| } | ||
| eventBus.emit("eleventy.importCacheReset", paths); | ||
| } | ||
| getNewTargetsSinceLastReset() { | ||
| return Array.from(this.newTargets); | ||
| } | ||
| getTargets() { | ||
| return Array.from(this.targets); | ||
| } | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
157
0.64%558702
-0.14%16614
-0.13%+ Added
- Removed