Files
beall/node_modules/@11ty/eleventy/src/TemplatePassthroughManager.js
2026-03-31 16:38:22 -07:00

369 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { isDynamicPattern } from "tinyglobby";
import { TemplatePath } from "@11ty/eleventy-utils";
import debugUtil from "debug";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import TemplatePassthrough from "./TemplatePassthrough.js";
import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js";
import { isGlobMatch } from "./Util/GlobMatcher.js";
import { withResolvers } from "./Util/PromiseUtil.js";
const debug = debugUtil("Eleventy:TemplatePassthroughManager");
class TemplatePassthroughManagerCopyError extends EleventyBaseError {}
class TemplatePassthroughManager {
#isDryRun = false;
#afterBuild;
#queue = new Map();
#extensionMap;
constructor(templateConfig) {
if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") {
throw new Error("Internal error: Missing or invalid `templateConfig` argument.");
}
this.templateConfig = templateConfig;
this.config = templateConfig.getConfig();
// eleventy# event listeners are removed on each build
this.config.events.on("eleventy#copy", ({ source, target, options }) => {
this.enqueueCopy(source, target, options);
});
this.config.events.on("eleventy#beforerender", () => {
this.#afterBuild = withResolvers();
});
this.config.events.on("eleventy#render", () => {
let { resolve } = this.#afterBuild;
resolve();
});
this.reset();
}
reset() {
this.count = 0;
this.size = 0;
this.conflictMap = {};
this.incrementalFile;
this.#queue = new Map();
}
set extensionMap(extensionMap) {
this.#extensionMap = extensionMap;
}
get extensionMap() {
if (!this.#extensionMap) {
throw new Error("Internal error: missing `extensionMap` in TemplatePassthroughManager.");
}
return this.#extensionMap;
}
get inputDir() {
return this.templateConfig.directories.input;
}
get outputDir() {
return this.templateConfig.directories.output;
}
setDryRun(isDryRun) {
this.#isDryRun = Boolean(isDryRun);
}
setRunMode(runMode) {
this.runMode = runMode;
}
setIncrementalFile(path) {
if (path) {
this.incrementalFile = path;
}
}
resetIncrementalFile() {
this.incrementalFile = undefined;
}
_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;
}
getCopySize() {
return this.size;
}
getMetadata() {
return {
copyCount: this.getCopyCount(),
copySize: this.getCopySize(),
};
}
setFileSystemSearch(fileSystemSearch) {
this.fileSystemSearch = fileSystemSearch;
}
getTemplatePassthroughForPath(path) {
let inst = new TemplatePassthrough(path, this.templateConfig);
inst.setFileSystemSearch(this.fileSystemSearch);
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<k; j++) { copy("coolkid.jpg", "_site/coolkid.jpg"); }`
// Eventually well want to move all of this to use Nodes fs.cp, which is experimental and only on Node 16+
return pass.write().then(
({ size, count, map }) => {
for (let src in map) {
let dest = map[src];
if (this.conflictMap[dest]) {
if (src !== this.conflictMap[dest]) {
let paths = [src, this.conflictMap[dest]].sort();
throw new TemplatePassthroughManagerCopyError(
`Multiple passthrough copy files are trying to write to the same output file (${TemplatePath.standardizeFilePath(dest)}). ${paths.map((p) => TemplatePath.standardizeFilePath(p)).join(" and ")}`,
);
} 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,
);
}
}
this.conflictMap[dest] = src;
}
if (pass.isDryRun) {
// We dont 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;
this.size += size;
debug("Copied %o (%d files, %d size)", inputPath, count || 0, size || 0);
} else {
debug("Skipped copying %o (emulated passthrough copy)", inputPath);
}
}
return {
count,
map,
};
},
function (e) {
return Promise.reject(
new TemplatePassthroughManagerCopyError(`Having trouble copying '${inputPath}'`, e),
);
},
);
}
isPassthroughCopyFile(paths, changedFile) {
if (!changedFile) {
return false;
}
// 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 &&
isDynamicPattern(path.inputPath) &&
isGlobMatch(changedFile, [path.inputPath])
) {
return path;
}
}
return false;
}
getAllNormalizedPaths(paths = []) {
if (this.incrementalFile) {
let isPassthrough = this.isPassthroughCopyFile(paths, this.incrementalFile);
if (isPassthrough) {
if (isPassthrough.outputPath) {
return [isPassthrough];
}
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?.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["/" + encodeURI(dest)] = src;
}
}
return entries;
}
async #waitForTemplatesRendered() {
if (!this.#afterBuild) {
return Promise.resolve(); // immediately resolve
}
let { promise } = this.#afterBuild;
return promise;
}
enqueueCopy(source, target, copyOptions) {
let key = `${source}=>${target}`;
// light de-dupe the same source/target combo (might be in the same file, might be viaTransforms)
if (this.#queue.has(key)) {
return;
}
let passthrough = TemplatePassthrough.factory(source, target, {
templateConfig: this.templateConfig,
copyOptions,
});
passthrough.setCheckSourceDirectory(true);
passthrough.setIsAlreadyNormalized(true);
passthrough.setRunMode(this.runMode);
passthrough.setDryRun(this.#isDryRun);
this.#queue.set(key, this.copyPassthrough(passthrough));
}
async copyAll(templateExtensionPaths) {
debug("TemplatePassthrough copy started.");
let normalizedPaths = this.getAllNormalizedPaths(templateExtensionPaths);
let passthroughs = normalizedPaths.map((path) => this.getTemplatePassthroughForPath(path));
let promises = passthroughs.map((pass) => this.copyPassthrough(pass));
await this.#waitForTemplatesRendered();
for (let [key, afterBuildCopyPromises] of this.#queue) {
promises.push(afterBuildCopyPromises);
}
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} (size: ${this.size})`);
return results;
});
}
}
export default TemplatePassthroughManager;