390 lines
9.9 KiB
JavaScript
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;
|