first
This commit is contained in:
389
node_modules/@11ty/eleventy/src/TemplatePassthrough.js
generated
vendored
Normal file
389
node_modules/@11ty/eleventy/src/TemplatePassthrough.js
generated
vendored
Normal file
@ -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;
|
||||
Reference in New Issue
Block a user