summaryrefslogtreecommitdiff
path: root/node_modules/@11ty/eleventy/src/TemplatePassthrough.js
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
committerShipwreckt <me@shipwreckt.co.uk>2025-10-31 20:02:14 +0000
commit7a52ddeba2a68388b544f529d2d92104420f77b0 (patch)
tree15ddd47457a2cb4a96060747437d36474e4f6b4e /node_modules/@11ty/eleventy/src/TemplatePassthrough.js
parent53d6ae2b5568437afa5e4995580a3fb679b7b91b (diff)
Changed from static to 11ty!
Diffstat (limited to 'node_modules/@11ty/eleventy/src/TemplatePassthrough.js')
-rw-r--r--node_modules/@11ty/eleventy/src/TemplatePassthrough.js389
1 files changed, 389 insertions, 0 deletions
diff --git a/node_modules/@11ty/eleventy/src/TemplatePassthrough.js b/node_modules/@11ty/eleventy/src/TemplatePassthrough.js
new file mode 100644
index 0000000..b27b634
--- /dev/null
+++ b/node_modules/@11ty/eleventy/src/TemplatePassthrough.js
@@ -0,0 +1,389 @@
+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;