Files
beall-11ty/node_modules/@11ty/eleventy/src/Util/ProjectDirectories.js
2026-03-31 16:38:22 -07:00

370 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 dont 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;