Files
firefrost-website/node_modules/@11ty/eleventy/src/TemplatePassthrough.js
2026-04-02 18:39:00 -05:00

390 lines
9.9 KiB
JavaScript

import path from "node:path";
import { isDynamicPattern } from "tinyglobby";
import { filesize } from "filesize";
import copy from "@11ty/recursive-copy";
import { TemplatePath } from "@11ty/eleventy-utils";
import debugUtil from "debug";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import checkPassthroughCopyBehavior from "./Util/PassthroughCopyBehaviorCheck.js";
import ProjectDirectories from "./Util/ProjectDirectories.js";
const debug = debugUtil("Eleventy:TemplatePassthrough");
class TemplatePassthroughError extends EleventyBaseError {}
class TemplatePassthrough {
isDryRun = false;
#isInputPathGlob;
#benchmarks;
#isAlreadyNormalized = false;
#projectDirCheck = false;
// paths already guaranteed from the autocopy plugin
static factory(inputPath, outputPath, opts = {}) {
let p = new TemplatePassthrough(
{
inputPath,
outputPath,
copyOptions: opts.copyOptions,
},
opts.templateConfig,
);
return p;
}
constructor(path, templateConfig) {
if (!templateConfig || templateConfig.constructor.name !== "TemplateConfig") {
throw new Error(
"Internal error: Missing `templateConfig` or was not an instance of `TemplateConfig`.",
);
}
this.templateConfig = templateConfig;
this.rawPath = path;
// inputPath is relative to the root of your project and not your Eleventy input directory.
// TODO normalize these with forward slashes
this.inputPath = this.normalizeIfDirectory(path.inputPath);
this.#isInputPathGlob = isDynamicPattern(this.inputPath);
this.outputPath = path.outputPath;
this.copyOptions = path.copyOptions; // custom options for recursive-copy
}
get benchmarks() {
if (!this.#benchmarks) {
this.#benchmarks = {
aggregate: this.config.benchmarkManager.get("Aggregate"),
};
}
return this.#benchmarks;
}
get config() {
return this.templateConfig.getConfig();
}
get directories() {
return this.templateConfig.directories;
}
// inputDir is used when stripping from output path in `getOutputPath`
get inputDir() {
return this.templateConfig.directories.input;
}
get outputDir() {
return this.templateConfig.directories.output;
}
// Skips `getFiles()` normalization
setIsAlreadyNormalized(isNormalized) {
this.#isAlreadyNormalized = Boolean(isNormalized);
}
setCheckSourceDirectory(check) {
this.#projectDirCheck = Boolean(check);
}
/* { inputPath, outputPath } though outputPath is *not* the full path: just the output directory */
getPath() {
return this.rawPath;
}
async getOutputPath(inputFileFromGlob) {
let { inputDir, outputDir, outputPath, inputPath } = this;
if (outputPath === true) {
// no explicit target, implied target
if (this.isDirectory(inputPath)) {
let inputRelativePath = TemplatePath.stripLeadingSubPath(
inputFileFromGlob || inputPath,
inputDir,
);
return ProjectDirectories.normalizeDirectory(
TemplatePath.join(outputDir, inputRelativePath),
);
}
return TemplatePath.normalize(
TemplatePath.join(
outputDir,
TemplatePath.stripLeadingSubPath(inputFileFromGlob || inputPath, inputDir),
),
);
}
if (inputFileFromGlob) {
return this.getOutputPathForGlobFile(inputFileFromGlob);
}
// Has explicit target
// Bug when copying incremental file overwriting output directory (and making it a file)
// e.g. public/test.css -> _site
// https://github.com/11ty/eleventy/issues/2278
let fullOutputPath = TemplatePath.normalize(TemplatePath.join(outputDir, outputPath));
if (outputPath === "" || this.isDirectory(inputPath)) {
fullOutputPath = ProjectDirectories.normalizeDirectory(fullOutputPath);
}
// TODO room for improvement here:
if (
!this.#isInputPathGlob &&
this.isExists(inputPath) &&
!this.isDirectory(inputPath) &&
this.isDirectory(fullOutputPath)
) {
let filename = path.parse(inputPath).base;
return TemplatePath.normalize(TemplatePath.join(fullOutputPath, filename));
}
return fullOutputPath;
}
async getOutputPathForGlobFile(inputFileFromGlob) {
return TemplatePath.join(
await this.getOutputPath(),
TemplatePath.getLastPathSegment(inputFileFromGlob),
);
}
setDryRun(isDryRun) {
this.isDryRun = Boolean(isDryRun);
}
setRunMode(runMode) {
this.runMode = runMode;
}
setFileSystemSearch(fileSystemSearch) {
this.fileSystemSearch = fileSystemSearch;
}
async getFiles(glob) {
debug("Searching for: %o", glob);
let b = this.benchmarks.aggregate.get("Searching the file system (passthrough)");
b.before();
if (!this.fileSystemSearch) {
throw new Error("Internal error: Missing `fileSystemSearch` property.");
}
// TODO perf this globs once per addPassthroughCopy entry
let files = TemplatePath.addLeadingDotSlashArray(
await this.fileSystemSearch.search("passthrough", glob, {
ignore: [
// *only* ignores output dir (not node_modules!)
this.outputDir,
],
}),
);
b.after();
return files;
}
isExists(filePath) {
return this.templateConfig.existsCache.exists(filePath);
}
isDirectory(filePath) {
return this.templateConfig.existsCache.isDirectory(filePath);
}
// dir is guaranteed to exist by context
// dir may not be a directory
normalizeIfDirectory(input) {
if (typeof input === "string") {
if (input.endsWith(path.sep) || input.endsWith("/")) {
return input;
}
// When inputPath is a directory, make sure it has a slash for passthrough copy aliasing
// https://github.com/11ty/eleventy/issues/2709
if (this.isDirectory(input)) {
return `${input}/`;
}
}
return input;
}
// maps input paths to output paths
async getFileMap() {
if (this.#isAlreadyNormalized) {
return [
{
inputPath: this.inputPath,
outputPath: this.outputPath,
},
];
}
// TODO VirtualFileSystem candidate
if (!isDynamicPattern(this.inputPath) && this.isExists(this.inputPath)) {
return [
{
inputPath: this.inputPath,
outputPath: await this.getOutputPath(),
},
];
}
let paths = [];
// If not directory or file, attempt to get globs
let files = await this.getFiles(this.inputPath);
for (let filePathFromGlob of files) {
paths.push({
inputPath: filePathFromGlob,
outputPath: await this.getOutputPath(filePathFromGlob),
});
}
return paths;
}
/* Types:
* 1. via glob, individual files found
* 2. directory, triggers an event for each file
* 3. individual file
*/
async copy(src, dest, copyOptions) {
if (this.#projectDirCheck && !this.directories.isFileInProjectFolder(src)) {
return Promise.reject(
new TemplatePassthroughError(
"Source file is not in the project directory. Check your passthrough paths.",
),
);
}
if (!this.directories.isFileInOutputFolder(dest)) {
return Promise.reject(
new TemplatePassthroughError(
"Destination is not in the site output directory. Check your passthrough paths.",
),
);
}
let fileCopyCount = 0;
let fileSizeCount = 0;
let map = {};
let b = this.benchmarks.aggregate.get("Passthrough Copy File");
// returns a promise
return copy(src, dest, copyOptions)
.on(copy.events.COPY_FILE_START, (copyOp) => {
// Access to individual files at `copyOp.src`
map[copyOp.src] = copyOp.dest;
b.before();
})
.on(copy.events.COPY_FILE_COMPLETE, (copyOp) => {
fileCopyCount++;
fileSizeCount += copyOp.stats.size;
if (copyOp.stats.size > 5000000) {
debug(`Copied %o (⚠️ large) file from %o`, filesize(copyOp.stats.size), copyOp.src);
} else {
debug(`Copied %o file from %o`, filesize(copyOp.stats.size), copyOp.src);
}
b.after();
})
.then(
() => {
return {
count: fileCopyCount,
size: fileSizeCount,
map,
};
},
(error) => {
if (copyOptions.overwrite === false && error.code === "EEXIST") {
// just ignore if the output already exists and overwrite: false
debug("Overwrite error ignored: %O", error);
return {
count: 0,
size: 0,
map,
};
}
return Promise.reject(error);
},
);
}
async write() {
if (this.isDryRun) {
return Promise.resolve({
count: 0,
map: {},
});
}
debug("Copying %o", this.inputPath);
let fileMap = await this.getFileMap();
// default options for recursive-copy
// see https://www.npmjs.com/package/recursive-copy#arguments
let copyOptionsDefault = {
overwrite: true, // overwrite output. fails when input is directory (mkdir) and output is file
dot: true, // copy dotfiles
junk: false, // copy cache files like Thumbs.db
results: false,
expand: false, // follow symlinks (matches recursive-copy default)
debug: false, // (matches recursive-copy default)
// Note: `filter` callback function only passes in a relative path, which is unreliable
// See https://github.com/timkendrick/recursive-copy/blob/4c9a8b8a4bf573285e9c4a649a30a2b59ccf441c/lib/copy.js#L59
// e.g. `{ filePaths: [ './img/coolkid.jpg' ], relativePaths: [ '' ] }`
};
let copyOptions = Object.assign(copyOptionsDefault, this.copyOptions);
let promises = fileMap.map((entry) => {
// For-free passthrough copy
if (checkPassthroughCopyBehavior(this.config, this.runMode)) {
let aliasMap = {};
aliasMap[entry.inputPath] = entry.outputPath;
return Promise.resolve({
count: 0,
map: aliasMap,
});
}
// Copy the files (only in build mode)
return this.copy(entry.inputPath, entry.outputPath, copyOptions);
});
// IMPORTANT: this returns an array of promises, does not await for promise to finish
return Promise.all(promises).then(
(results) => {
// collate the count and input/output map results from the array.
let count = 0;
let size = 0;
let map = {};
for (let result of results) {
count += result.count;
size += result.size;
Object.assign(map, result.map);
}
return {
count,
size,
map,
};
},
(err) => {
throw new TemplatePassthroughError(`Error copying passthrough files: ${err.message}`, err);
},
);
}
}
export default TemplatePassthrough;