diff options
Diffstat (limited to 'node_modules/@11ty/eleventy/src/TemplatePassthrough.js')
| -rw-r--r-- | node_modules/@11ty/eleventy/src/TemplatePassthrough.js | 389 |
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; |
