const multimatch = require("multimatch"); const isGlob = require("is-glob"); const { TemplatePath } = require("@11ty/eleventy-utils"); const EleventyExtensionMap = require("./EleventyExtensionMap"); const EleventyBaseError = require("./EleventyBaseError"); const TemplatePassthrough = require("./TemplatePassthrough"); const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck"); const debug = require("debug")("Eleventy:TemplatePassthroughManager"); const debugDev = require("debug")("Dev:Eleventy:TemplatePassthroughManager"); class TemplatePassthroughManagerConfigError extends EleventyBaseError {} class TemplatePassthroughManagerCopyError extends EleventyBaseError {} class TemplatePassthroughManager { constructor(eleventyConfig) { if (!eleventyConfig) { throw new TemplatePassthroughManagerConfigError("Missing `config` argument."); } this.eleventyConfig = eleventyConfig; this.config = eleventyConfig.getConfig(); this.reset(); } reset() { this.count = 0; this.conflictMap = {}; this.incrementalFile = null; debug("Resetting counts to 0"); } set extensionMap(extensionMap) { this._extensionMap = extensionMap; } get extensionMap() { if (!this._extensionMap) { this._extensionMap = new EleventyExtensionMap([], this.eleventyConfig); } return this._extensionMap; } setOutputDir(outputDir) { this.outputDir = outputDir; } setInputDir(inputDir) { this.inputDir = inputDir; } setDryRun(isDryRun) { this.isDryRun = !!isDryRun; } setRunMode(runMode) { this.runMode = runMode; } setIncrementalFile(path) { if (path) { this.incrementalFile = path; } } _normalizePaths(path, outputPath, copyOptions = {}) { return { inputPath: TemplatePath.addLeadingDotSlash(path), outputPath: outputPath ? TemplatePath.stripLeadingDotSlash(outputPath) : true, copyOptions, }; } getConfigPaths() { let paths = []; let pathsRaw = this.config.passthroughCopies || {}; debug("`addPassthroughCopy` config API paths: %o", pathsRaw); for (let [inputPath, { outputPath, copyOptions }] of Object.entries(pathsRaw)) { paths.push(this._normalizePaths(inputPath, outputPath, copyOptions)); } debug("`addPassthroughCopy` config API normalized paths: %o", paths); return paths; } getConfigPathGlobs() { return this.getConfigPaths().map((path) => { return TemplatePath.convertToRecursiveGlobSync(path.inputPath); }); } getNonTemplatePaths(paths) { let matches = []; for (let path of paths) { if (!this.extensionMap.hasEngine(path)) { matches.push(path); } } return matches; } getCopyCount() { return this.count; } setFileSystemSearch(fileSystemSearch) { this.fileSystemSearch = fileSystemSearch; } getTemplatePassthroughForPath(path, isIncremental = false) { let inst = new TemplatePassthrough(path, this.outputDir, this.inputDir, this.config); inst.setFileSystemSearch(this.fileSystemSearch); inst.setIsIncremental(isIncremental); inst.setDryRun(this.isDryRun); inst.setRunMode(this.runMode); return inst; } async copyPassthrough(pass) { if (!(pass instanceof TemplatePassthrough)) { throw new TemplatePassthroughManagerCopyError( "copyPassthrough expects an instance of TemplatePassthrough" ); } let { inputPath } = pass.getPath(); // TODO https://github.com/11ty/eleventy/issues/2452 // De-dupe both the input and output paired together to avoid the case // where an input/output pair has been added via multiple passthrough methods (glob, file suffix, etc) // Probably start with the `filter` callback in recursive-copy but it only passes relative paths // See the note in TemplatePassthrough.js->write() // Also note that `recursive-copy` handles repeated overwrite copy to the same destination just fine. // e.g. `for(let j=0, k=1000; j { for (let src in map) { let dest = map[src]; if (this.conflictMap[dest]) { if (src !== this.conflictMap[dest]) { throw new TemplatePassthroughManagerCopyError( `Multiple passthrough copy files are trying to write to the same output file (${dest}). ${src} and ${this.conflictMap[dest]}` ); } else { // Multiple entries from the same source debug( "A passthrough copy entry (%o) caused the same file (%o) to be copied more than once to the output (%o). This is atomically safe but a waste of build resources.", inputPath, src, dest ); } } debugDev("Adding %o to passthrough copy conflict map, from %o", dest, src); this.conflictMap[dest] = src; } if (pass.isDryRun) { // We don’t count the skipped files as we need to iterate over them debug( "Skipped %o (either from --dryrun or --incremental or for-free passthrough copy)", inputPath ); } else { if (count) { this.count += count; debug("Copied %o (%d files)", inputPath, count || 0); } else { debug("Skipped copying %o (emulated passthrough copy)", inputPath); } } return { count, map, }; }) .catch(function (e) { return Promise.reject( new TemplatePassthroughManagerCopyError(`Having trouble copying '${inputPath}'`, e) ); }); } isPassthroughCopyFile(paths, changedFile) { // passthrough copy by non-matching engine extension (via templateFormats) for (let path of paths) { if (path === changedFile && !this.extensionMap.hasEngine(path)) { return true; } } for (let path of this.getConfigPaths()) { if (TemplatePath.startsWithSubPath(changedFile, path.inputPath)) { return path; } if ( changedFile && isGlob(path.inputPath) && multimatch([changedFile], [path.inputPath]).length ) { return path; } } return false; } getAllNormalizedPaths(paths) { if (this.incrementalFile) { let isPassthrough = this.isPassthroughCopyFile(paths, this.incrementalFile); if (isPassthrough) { if (isPassthrough.outputPath) { return [this._normalizePaths(this.incrementalFile, isPassthrough.outputPath)]; } return [this._normalizePaths(this.incrementalFile)]; } // Fixes https://github.com/11ty/eleventy/issues/2491 if (!checkPassthroughCopyBehavior(this.config, this.runMode)) { return []; } } let normalizedPaths = this.getConfigPaths(); if (debug.enabled) { for (let path of normalizedPaths) { debug("TemplatePassthrough copying from config: %o", path); } } if (paths && paths.length) { let passthroughPaths = this.getNonTemplatePaths(paths); for (let path of passthroughPaths) { let normalizedPath = this._normalizePaths(path); debug( `TemplatePassthrough copying from non-matching file extension: ${normalizedPath.inputPath}` ); normalizedPaths.push(normalizedPath); } } return normalizedPaths; } // keys: output // values: input getAliasesFromPassthroughResults(result) { let entries = {}; for (let entry of result) { for (let src in entry.map) { let dest = TemplatePath.stripLeadingSubPath(entry.map[src], this.outputDir); entries["/" + dest] = src; } } return entries; } // Performance note: these can actually take a fair bit of time, but aren’t a // bottleneck to eleventy. The copies are performed asynchronously and don’t affect eleventy // write times in a significant way. async copyAll(templateExtensionPaths) { debug("TemplatePassthrough copy started."); let normalizedPaths = this.getAllNormalizedPaths(templateExtensionPaths); let passthroughs = normalizedPaths.map((path) => { // if incrementalFile is set but it isn’t a passthrough copy, normalizedPaths will be an empty array let isIncremental = !!this.incrementalFile; return this.getTemplatePassthroughForPath(path, isIncremental); }); let promises = passthroughs.map((pass) => this.copyPassthrough(pass)); return Promise.all(promises).then(async (results) => { let aliases = this.getAliasesFromPassthroughResults(results); await this.config.events.emit("eleventy.passthrough", { map: aliases, }); debug(`TemplatePassthrough copy finished. Current count: ${this.count}`); return results; }); } } module.exports = TemplatePassthroughManager;