devsite/node_modules/@11ty/eleventy/src/TemplatePassthroughManager.js
2024-07-07 18:49:38 -07:00

300 lines
9.0 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.

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<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(({ count, map }) => {
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 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;
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 arent a
// bottleneck to eleventy. The copies are performed asynchronously and dont 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 isnt 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;