370 lines
9.7 KiB
JavaScript
370 lines
9.7 KiB
JavaScript
|
|
import fs from "node:fs";
|
|||
|
|
import path from "node:path";
|
|||
|
|
import { TemplatePath } from "@11ty/eleventy-utils";
|
|||
|
|
import { isDynamicPattern } from "tinyglobby";
|
|||
|
|
|
|||
|
|
import DirContains from "./DirContains.js";
|
|||
|
|
|
|||
|
|
/* Directories internally should always use *nix forward slashes */
|
|||
|
|
class ProjectDirectories {
|
|||
|
|
static defaults = {
|
|||
|
|
input: "./",
|
|||
|
|
data: "./_data/", // Relative to input directory
|
|||
|
|
includes: "./_includes/", // Relative to input directory
|
|||
|
|
layouts: "./_layouts/", // Relative to input directory
|
|||
|
|
output: "./_site/",
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// no updates allowed, input/output set via CLI
|
|||
|
|
#frozen = false;
|
|||
|
|
|
|||
|
|
#raw = {};
|
|||
|
|
|
|||
|
|
#dirs = {};
|
|||
|
|
|
|||
|
|
inputFile = undefined;
|
|||
|
|
inputGlob = undefined;
|
|||
|
|
|
|||
|
|
// Add leading dot slash
|
|||
|
|
// Use forward slashes
|
|||
|
|
static normalizePath(fileOrDir) {
|
|||
|
|
return TemplatePath.standardizeFilePath(fileOrDir);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Must be a directory
|
|||
|
|
// Always include a trailing slash
|
|||
|
|
static normalizeDirectory(dir) {
|
|||
|
|
return this.addTrailingSlash(this.normalizePath(dir));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
normalizeDirectoryPathRelativeToInputDirectory(filePath) {
|
|||
|
|
return ProjectDirectories.normalizeDirectory(path.join(this.input, filePath));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static addTrailingSlash(path) {
|
|||
|
|
if (path.slice(-1) === "/") {
|
|||
|
|
return path;
|
|||
|
|
}
|
|||
|
|
return path + "/";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If input/output are set via CLI, they take precedence over all other configuration values.
|
|||
|
|
freeze() {
|
|||
|
|
this.#frozen = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setViaConfigObject(configDirs = {}) {
|
|||
|
|
// input must come last
|
|||
|
|
let inputChanged = false;
|
|||
|
|
if (
|
|||
|
|
configDirs.input &&
|
|||
|
|
ProjectDirectories.normalizeDirectory(configDirs.input) !== this.input
|
|||
|
|
) {
|
|||
|
|
this.#setInputRaw(configDirs.input);
|
|||
|
|
inputChanged = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// If falsy or an empty string, the current directory is used.
|
|||
|
|
if (configDirs.output !== undefined) {
|
|||
|
|
if (ProjectDirectories.normalizeDirectory(configDirs.output) !== this.output) {
|
|||
|
|
this.setOutput(configDirs.output);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Input relative directory, if falsy or an empty string, inputDir is used!
|
|||
|
|
// Always set if input changed, e.g. input is `src` and data is `../_data` (resulting in `./_data`) we still want to set data to this new value
|
|||
|
|
if (configDirs.data !== undefined) {
|
|||
|
|
if (
|
|||
|
|
inputChanged ||
|
|||
|
|
this.normalizeDirectoryPathRelativeToInputDirectory(configDirs.data || "") !== this.data
|
|||
|
|
) {
|
|||
|
|
this.setData(configDirs.data);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Input relative directory, if falsy or an empty string, inputDir is used!
|
|||
|
|
if (configDirs.includes !== undefined) {
|
|||
|
|
if (
|
|||
|
|
inputChanged ||
|
|||
|
|
this.normalizeDirectoryPathRelativeToInputDirectory(configDirs.includes || "") !==
|
|||
|
|
this.includes
|
|||
|
|
) {
|
|||
|
|
this.setIncludes(configDirs.includes);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Input relative directory, if falsy or an empty string, inputDir is used!
|
|||
|
|
if (configDirs.layouts !== undefined) {
|
|||
|
|
if (
|
|||
|
|
inputChanged ||
|
|||
|
|
this.normalizeDirectoryPathRelativeToInputDirectory(configDirs.layouts || "") !==
|
|||
|
|
this.layouts
|
|||
|
|
) {
|
|||
|
|
this.setLayouts(configDirs.layouts);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (inputChanged) {
|
|||
|
|
this.updateInputDependencies();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateInputDependencies() {
|
|||
|
|
// raw first, fall back to Eleventy defaults if not yet set
|
|||
|
|
this.setData(this.#raw.data ?? ProjectDirectories.defaults.data);
|
|||
|
|
this.setIncludes(this.#raw.includes ?? ProjectDirectories.defaults.includes);
|
|||
|
|
|
|||
|
|
// Should not include this if not explicitly opted-in
|
|||
|
|
if (this.#raw.layouts !== undefined) {
|
|||
|
|
this.setLayouts(this.#raw.layouts ?? ProjectDirectories.defaults.layouts);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Relative to project root, must exist */
|
|||
|
|
#setInputRaw(dirOrFile, inputDir = undefined) {
|
|||
|
|
// is frozen and was defined previously
|
|||
|
|
if (this.#frozen && this.#raw.input !== undefined) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.#raw.input = dirOrFile;
|
|||
|
|
|
|||
|
|
if (!dirOrFile) {
|
|||
|
|
// input must exist if inputDir is not set.
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Normalize absolute paths to relative, #3805
|
|||
|
|
// if(path.isAbsolute(dirOrFile)) {
|
|||
|
|
// dirOrFile = path.relative(".", dirOrFile);
|
|||
|
|
// }
|
|||
|
|
|
|||
|
|
// Input has to exist (assumed glob if it does not exist)
|
|||
|
|
let inputExists = fs.existsSync(dirOrFile);
|
|||
|
|
let inputExistsAndIsDirectory = inputExists && fs.statSync(dirOrFile).isDirectory();
|
|||
|
|
|
|||
|
|
if (inputExistsAndIsDirectory) {
|
|||
|
|
// is not a file or glob
|
|||
|
|
this.#dirs.input = ProjectDirectories.normalizeDirectory(dirOrFile);
|
|||
|
|
} else {
|
|||
|
|
if (inputExists) {
|
|||
|
|
this.inputFile = ProjectDirectories.normalizePath(dirOrFile);
|
|||
|
|
} else {
|
|||
|
|
if (!isDynamicPattern(dirOrFile)) {
|
|||
|
|
throw new Error(
|
|||
|
|
`The "${dirOrFile}" \`input\` parameter (directory or file path) must exist on the file system (unless detected as a glob by the \`tinyglobby\` package)`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.inputGlob = dirOrFile;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Explicit Eleventy option for inputDir
|
|||
|
|
if (inputDir) {
|
|||
|
|
// Changed in 3.0: must exist
|
|||
|
|
if (!fs.existsSync(inputDir)) {
|
|||
|
|
throw new Error("Directory must exist (via inputDir option to Eleventy constructor).");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.#dirs.input = ProjectDirectories.normalizeDirectory(inputDir);
|
|||
|
|
} else {
|
|||
|
|
// the input directory is implied to be the parent directory of the
|
|||
|
|
// file, unless inputDir is explicitly specified (via Eleventy constructor `options`)
|
|||
|
|
this.#dirs.input = ProjectDirectories.normalizeDirectory(
|
|||
|
|
TemplatePath.getDirFromFilePath(dirOrFile), // works with globs
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setInput(dirOrFile, inputDir = undefined) {
|
|||
|
|
this.#setInputRaw(dirOrFile, inputDir); // does not update
|
|||
|
|
this.updateInputDependencies();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Relative to input dir */
|
|||
|
|
setIncludes(dir) {
|
|||
|
|
if (dir !== undefined) {
|
|||
|
|
// falsy or an empty string is valid (falls back to input dir)
|
|||
|
|
this.#raw.includes = dir;
|
|||
|
|
this.#dirs.includes = ProjectDirectories.normalizeDirectory(
|
|||
|
|
TemplatePath.join(this.input, dir || ""),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Relative to input dir */
|
|||
|
|
/* Optional */
|
|||
|
|
setLayouts(dir) {
|
|||
|
|
if (dir !== undefined) {
|
|||
|
|
// falsy or an empty string is valid (falls back to input dir)
|
|||
|
|
this.#raw.layouts = dir;
|
|||
|
|
this.#dirs.layouts = ProjectDirectories.normalizeDirectory(
|
|||
|
|
TemplatePath.join(this.input, dir || ""),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Relative to input dir */
|
|||
|
|
setData(dir) {
|
|||
|
|
if (dir !== undefined) {
|
|||
|
|
// falsy or an empty string is valid (falls back to input dir)
|
|||
|
|
// TODO must exist if specified
|
|||
|
|
this.#raw.data = dir;
|
|||
|
|
this.#dirs.data = ProjectDirectories.normalizeDirectory(
|
|||
|
|
TemplatePath.join(this.input, dir || ""),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Relative to project root */
|
|||
|
|
setOutput(dir) {
|
|||
|
|
// is frozen and was defined previously
|
|||
|
|
if (this.#frozen && this.#raw.output !== undefined) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (dir !== undefined) {
|
|||
|
|
this.#raw.output = dir;
|
|||
|
|
this.#dirs.output = ProjectDirectories.normalizeDirectory(dir || "");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get input() {
|
|||
|
|
return this.#dirs.input || ProjectDirectories.defaults.input;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get data() {
|
|||
|
|
return this.#dirs.data || ProjectDirectories.defaults.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get includes() {
|
|||
|
|
return this.#dirs.includes || ProjectDirectories.defaults.includes;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get layouts() {
|
|||
|
|
// explicit opt-in, no fallback.
|
|||
|
|
return this.#dirs.layouts;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
get output() {
|
|||
|
|
return this.#dirs.output || ProjectDirectories.defaults.output;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isTemplateFile(filePath) {
|
|||
|
|
let inputPath = this.getInputPath(filePath);
|
|||
|
|
// TODO use DirContains
|
|||
|
|
if (this.layouts && inputPath.startsWith(this.layouts)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// if this.includes is "" (and thus is the same directory as this.input)
|
|||
|
|
// we don’t actually know if this is a template file, so defer
|
|||
|
|
if (this.includes && this.includes !== this.input) {
|
|||
|
|
if (inputPath.startsWith(this.includes)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TODO use DirContains
|
|||
|
|
return inputPath.startsWith(this.input);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// for a hypothetical template file
|
|||
|
|
getInputPath(filePathRelativeToInputDir) {
|
|||
|
|
// TODO change ~/ to project root dir
|
|||
|
|
return TemplatePath.addLeadingDotSlash(
|
|||
|
|
TemplatePath.join(this.input, TemplatePath.standardizeFilePath(filePathRelativeToInputDir)),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Inverse of getInputPath
|
|||
|
|
// Removes input dir from path
|
|||
|
|
getInputPathRelativeToInputDirectory(filePathRelativeToInputDir) {
|
|||
|
|
let inputDir = TemplatePath.addLeadingDotSlash(TemplatePath.join(this.input));
|
|||
|
|
|
|||
|
|
// No leading dot slash
|
|||
|
|
return TemplatePath.stripLeadingSubPath(filePathRelativeToInputDir, inputDir);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// for a hypothetical Eleventy layout file
|
|||
|
|
getLayoutPath(filePathRelativeToLayoutDir) {
|
|||
|
|
return TemplatePath.addLeadingDotSlash(
|
|||
|
|
TemplatePath.join(
|
|||
|
|
this.layouts || this.includes,
|
|||
|
|
TemplatePath.standardizeFilePath(filePathRelativeToLayoutDir),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Removes layout dir from path
|
|||
|
|
getLayoutPathRelativeToInputDirectory(filePathRelativeToLayoutDir) {
|
|||
|
|
let layoutPath = this.getLayoutPath(filePathRelativeToLayoutDir);
|
|||
|
|
let inputDir = TemplatePath.addLeadingDotSlash(TemplatePath.join(this.input));
|
|||
|
|
|
|||
|
|
// No leading dot slash
|
|||
|
|
return TemplatePath.stripLeadingSubPath(layoutPath, inputDir);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getProjectPath(filePath) {
|
|||
|
|
return TemplatePath.addLeadingDotSlash(
|
|||
|
|
TemplatePath.join(".", TemplatePath.standardizeFilePath(filePath)),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isFileInProjectFolder(filePath) {
|
|||
|
|
return DirContains(TemplatePath.getWorkingDir(), filePath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isFileInOutputFolder(filePath) {
|
|||
|
|
return DirContains(this.output, filePath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static getRelativeTo(targetPath, cwd) {
|
|||
|
|
return path.relative(cwd, path.join(path.resolve("."), targetPath));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Access the data without being able to set the data.
|
|||
|
|
getUserspaceInstance() {
|
|||
|
|
let d = this;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
get input() {
|
|||
|
|
return d.input;
|
|||
|
|
},
|
|||
|
|
get inputFile() {
|
|||
|
|
return d.inputFile;
|
|||
|
|
},
|
|||
|
|
get inputGlob() {
|
|||
|
|
return d.inputGlob;
|
|||
|
|
},
|
|||
|
|
get data() {
|
|||
|
|
return d.data;
|
|||
|
|
},
|
|||
|
|
get includes() {
|
|||
|
|
return d.includes;
|
|||
|
|
},
|
|||
|
|
get layouts() {
|
|||
|
|
return d.layouts;
|
|||
|
|
},
|
|||
|
|
get output() {
|
|||
|
|
return d.output;
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toString() {
|
|||
|
|
return {
|
|||
|
|
input: this.input,
|
|||
|
|
inputFile: this.inputFile,
|
|||
|
|
inputGlob: this.inputGlob,
|
|||
|
|
data: this.data,
|
|||
|
|
includes: this.includes,
|
|||
|
|
layouts: this.layouts,
|
|||
|
|
output: this.output,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default ProjectDirectories;
|