150 lines
3.2 KiB
JavaScript
150 lines
3.2 KiB
JavaScript
import path from "node:path";
|
||
import { TemplatePath } from "@11ty/eleventy-utils";
|
||
import isValidUrl from "./ValidUrl.js";
|
||
import { isGlobMatch } from "./GlobMatcher.js";
|
||
|
||
class HtmlRelativeCopy {
|
||
#userConfig;
|
||
#matchingGlobs = new Set();
|
||
#matchingGlobsArray;
|
||
#dirty = false;
|
||
#paths = new Set();
|
||
#failOnError = true;
|
||
#copyOptions = {
|
||
dot: false, // differs from standard passthrough copy
|
||
};
|
||
|
||
isEnabled() {
|
||
return this.#matchingGlobs.size > 0;
|
||
}
|
||
|
||
setFailOnError(failOnError) {
|
||
this.#failOnError = Boolean(failOnError);
|
||
}
|
||
|
||
setCopyOptions(opts) {
|
||
if (opts) {
|
||
Object.assign(this.#copyOptions, opts);
|
||
}
|
||
}
|
||
|
||
setUserConfig(userConfig) {
|
||
if (!userConfig || userConfig.constructor.name !== "UserConfig") {
|
||
throw new Error(
|
||
"Internal error: Missing `userConfig` or was not an instance of `UserConfig`.",
|
||
);
|
||
}
|
||
this.#userConfig = userConfig;
|
||
}
|
||
|
||
addPaths(paths = []) {
|
||
for (let path of paths) {
|
||
this.#paths.add(TemplatePath.getDir(path));
|
||
}
|
||
}
|
||
|
||
get matchingGlobs() {
|
||
if (this.#dirty || !this.#matchingGlobsArray) {
|
||
this.#matchingGlobsArray = Array.from(this.#matchingGlobs);
|
||
this.#dirty = false;
|
||
}
|
||
|
||
return this.#matchingGlobsArray;
|
||
}
|
||
|
||
addMatchingGlob(glob) {
|
||
if (glob) {
|
||
if (Array.isArray(glob)) {
|
||
for (let g of glob) {
|
||
this.#matchingGlobs.add(g);
|
||
}
|
||
} else {
|
||
this.#matchingGlobs.add(glob);
|
||
}
|
||
this.#dirty = true;
|
||
}
|
||
}
|
||
|
||
isSkippableHref(rawRef) {
|
||
if (
|
||
this.#matchingGlobs.size === 0 ||
|
||
!rawRef ||
|
||
path.isAbsolute(rawRef) ||
|
||
isValidUrl(rawRef)
|
||
) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
isCopyableTarget(target) {
|
||
if (!isGlobMatch(target, this.matchingGlobs)) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
exists(filePath) {
|
||
return this.#userConfig.exists(filePath);
|
||
}
|
||
|
||
getAliasedPath(ref) {
|
||
for (let dir of this.#paths) {
|
||
let found = TemplatePath.join(dir, ref);
|
||
if (this.isCopyableTarget(found) && this.exists(found)) {
|
||
return found;
|
||
}
|
||
}
|
||
}
|
||
|
||
getFilePathRelativeToProjectRoot(ref, contextFilePath) {
|
||
let dir = TemplatePath.getDirFromFilePath(contextFilePath);
|
||
return TemplatePath.join(dir, ref);
|
||
}
|
||
|
||
copy(fileRef, tmplInputPath, tmplOutputPath) {
|
||
// original ref is a full URL or no globs exist
|
||
if (this.isSkippableHref(fileRef)) {
|
||
return;
|
||
}
|
||
|
||
// Relative to source file’s input path
|
||
let source = this.getFilePathRelativeToProjectRoot(fileRef, tmplInputPath);
|
||
if (!this.isCopyableTarget(source)) {
|
||
return;
|
||
}
|
||
|
||
if (!this.exists(source)) {
|
||
// Try to alias using `options.paths`
|
||
let alias = this.getAliasedPath(fileRef);
|
||
if (!alias) {
|
||
if (this.#failOnError) {
|
||
throw new Error(
|
||
"Missing input file for `html-relative` Passthrough Copy file: " +
|
||
TemplatePath.absolutePath(source),
|
||
);
|
||
}
|
||
|
||
// don’t fail on error
|
||
return;
|
||
}
|
||
|
||
source = alias;
|
||
}
|
||
|
||
let target = this.getFilePathRelativeToProjectRoot(fileRef, tmplOutputPath);
|
||
|
||
// We use a Set here to allow passthrough copy manager to properly error on conflicts upstream
|
||
// Only errors when different inputs write to the same output
|
||
// Also errors if attempts to write outside the output folder.
|
||
this.#userConfig.emit("eleventy#copy", {
|
||
source,
|
||
target,
|
||
options: this.#copyOptions,
|
||
});
|
||
}
|
||
}
|
||
|
||
export { HtmlRelativeCopy };
|