552 lines
14 KiB
JavaScript
552 lines
14 KiB
JavaScript
import fs from "node:fs";
|
||
import chalk from "kleur";
|
||
import { Merge, TemplatePath, isPlainObject } from "@11ty/eleventy-utils";
|
||
import debugUtil from "debug";
|
||
|
||
import { EleventyImportRaw, EleventyImportRawFromEleventy } from "./Util/Require.js";
|
||
import EleventyBaseError from "./Errors/EleventyBaseError.js";
|
||
import UserConfig from "./UserConfig.js";
|
||
import GlobalDependencyMap from "./GlobalDependencyMap.js";
|
||
import ExistsCache from "./Util/ExistsCache.js";
|
||
import eventBus from "./EventBus.js";
|
||
import ProjectTemplateFormats from "./Util/ProjectTemplateFormats.js";
|
||
|
||
const debug = debugUtil("Eleventy:TemplateConfig");
|
||
const debugDev = debugUtil("Dev:Eleventy:TemplateConfig");
|
||
|
||
/**
|
||
* @module 11ty/eleventy/TemplateConfig
|
||
*/
|
||
|
||
/**
|
||
* Config as used by the template.
|
||
* @typedef {object} module:11ty/eleventy/TemplateConfig~TemplateConfig~config
|
||
* @property {String} [pathPrefix] - The path prefix.
|
||
*/
|
||
|
||
/**
|
||
* Errors in eleventy config.
|
||
* @ignore
|
||
*/
|
||
class EleventyConfigError extends EleventyBaseError {}
|
||
|
||
/**
|
||
* Errors in eleventy plugins.
|
||
* @ignore
|
||
*/
|
||
class EleventyPluginError extends EleventyBaseError {}
|
||
|
||
/**
|
||
* Config for a template.
|
||
* @ignore
|
||
* @param {{}} customRootConfig - tbd.
|
||
* @param {String} projectConfigPath - Path to local project config.
|
||
*/
|
||
class TemplateConfig {
|
||
#templateFormats;
|
||
#runMode;
|
||
#configManuallyDefined = false;
|
||
/** @type {UserConfig} */
|
||
#userConfig = new UserConfig();
|
||
|
||
constructor(customRootConfig, projectConfigPath) {
|
||
/** @type {object} */
|
||
this.overrides = {};
|
||
|
||
/**
|
||
* @type {String}
|
||
* @description Path to local project config.
|
||
* @default .eleventy.js
|
||
*/
|
||
if (projectConfigPath !== undefined) {
|
||
this.#configManuallyDefined = true;
|
||
|
||
if (!projectConfigPath) {
|
||
// falsy skips config files
|
||
this.projectConfigPaths = [];
|
||
} else {
|
||
this.projectConfigPaths = [projectConfigPath];
|
||
}
|
||
} else {
|
||
this.projectConfigPaths = [
|
||
".eleventy.js",
|
||
"eleventy.config.js",
|
||
"eleventy.config.mjs",
|
||
"eleventy.config.cjs",
|
||
];
|
||
}
|
||
|
||
if (customRootConfig) {
|
||
/**
|
||
* @type {object}
|
||
* @description Custom root config.
|
||
*/
|
||
this.customRootConfig = customRootConfig;
|
||
debug("Warning: Using custom root config!");
|
||
} else {
|
||
this.customRootConfig = null;
|
||
}
|
||
|
||
this.hasConfigMerged = false;
|
||
this.isEsm = false;
|
||
}
|
||
|
||
get userConfig() {
|
||
return this.#userConfig;
|
||
}
|
||
|
||
get aggregateBenchmark() {
|
||
return this.userConfig.benchmarks.aggregate;
|
||
}
|
||
|
||
/* Setter for Logger */
|
||
setLogger(logger) {
|
||
this.logger = logger;
|
||
this.userConfig.logger = this.logger;
|
||
}
|
||
|
||
/* Setter for Directories instance */
|
||
setDirectories(directories) {
|
||
this.directories = directories;
|
||
this.userConfig.directories = directories.getUserspaceInstance();
|
||
}
|
||
|
||
/* Setter for TemplateFormats instance */
|
||
setTemplateFormats(templateFormats) {
|
||
this.#templateFormats = templateFormats;
|
||
}
|
||
|
||
get templateFormats() {
|
||
if (!this.#templateFormats) {
|
||
this.#templateFormats = new ProjectTemplateFormats();
|
||
}
|
||
return this.#templateFormats;
|
||
}
|
||
|
||
/* Backwards compat */
|
||
get inputDir() {
|
||
return this.directories.input;
|
||
}
|
||
|
||
setRunMode(runMode) {
|
||
this.#runMode = runMode;
|
||
}
|
||
|
||
shouldSpiderJavaScriptDependencies() {
|
||
// not for a standard build
|
||
return (
|
||
(this.#runMode === "watch" || this.#runMode === "serve") &&
|
||
this.userConfig.watchJavaScriptDependencies
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Normalises local project config file path.
|
||
*
|
||
* @method
|
||
* @returns {String|undefined} - The normalised local project config file path.
|
||
*/
|
||
getLocalProjectConfigFile() {
|
||
let configFiles = this.getLocalProjectConfigFiles();
|
||
// Add the configFiles[0] in case of a test, where no file exists on the file system
|
||
let configFile = configFiles.find((path) => path && fs.existsSync(path)) || configFiles[0];
|
||
if (configFile) {
|
||
return configFile;
|
||
}
|
||
}
|
||
|
||
getLocalProjectConfigFiles() {
|
||
if (this.projectConfigPaths?.length > 0) {
|
||
return TemplatePath.addLeadingDotSlashArray(this.projectConfigPaths.filter((path) => path));
|
||
}
|
||
return [];
|
||
}
|
||
|
||
setProjectUsingEsm(isEsmProject) {
|
||
this.isEsm = !!isEsmProject;
|
||
this.usesGraph.setIsEsm(isEsmProject);
|
||
}
|
||
|
||
getIsProjectUsingEsm() {
|
||
return this.isEsm;
|
||
}
|
||
|
||
/**
|
||
* Resets the configuration.
|
||
*/
|
||
async reset() {
|
||
debugDev("Resetting configuration: TemplateConfig and UserConfig.");
|
||
this.userConfig.reset();
|
||
// await this.initializeRootConfig();
|
||
await this.forceReloadConfig();
|
||
this.usesGraph.reset();
|
||
|
||
// Clear the compile cache
|
||
eventBus.emit("eleventy.compileCacheReset");
|
||
}
|
||
|
||
/**
|
||
* Resets the configuration while in watch mode.
|
||
*
|
||
* @todo Add implementation.
|
||
*/
|
||
resetOnWatch() {
|
||
// nothing yet
|
||
}
|
||
|
||
hasInitialized() {
|
||
return this.hasConfigMerged;
|
||
}
|
||
|
||
/**
|
||
* Async-friendly init method
|
||
*/
|
||
async init(overrides) {
|
||
await this.initializeRootConfig();
|
||
|
||
if (overrides) {
|
||
this.appendToRootConfig(overrides);
|
||
}
|
||
|
||
this.config = await this.mergeConfig();
|
||
this.hasConfigMerged = true;
|
||
}
|
||
|
||
/**
|
||
* Force a reload of the configuration object.
|
||
*/
|
||
async forceReloadConfig() {
|
||
this.hasConfigMerged = false;
|
||
await this.init();
|
||
}
|
||
|
||
/**
|
||
* Returns the config object.
|
||
*
|
||
* @returns {{}} - The config object.
|
||
*/
|
||
getConfig() {
|
||
if (!this.hasConfigMerged) {
|
||
throw new Error("Invalid call to .getConfig(). Needs an .init() first.");
|
||
}
|
||
|
||
return this.config;
|
||
}
|
||
|
||
/**
|
||
* Overwrites the config path.
|
||
*
|
||
* @param {String} path - The new config path.
|
||
*/
|
||
async setProjectConfigPath(path) {
|
||
this.#configManuallyDefined = true;
|
||
|
||
if (path !== undefined) {
|
||
this.projectConfigPaths = [path];
|
||
} else {
|
||
this.projectConfigPaths = [];
|
||
}
|
||
|
||
if (this.hasConfigMerged) {
|
||
// merge it again
|
||
debugDev("Merging in getConfig again after setting the local project config path.");
|
||
await this.forceReloadConfig();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Overwrites the path prefix.
|
||
*
|
||
* @param {String} pathPrefix - The new path prefix.
|
||
*/
|
||
setPathPrefix(pathPrefix) {
|
||
if (pathPrefix && pathPrefix !== "/") {
|
||
debug("Setting pathPrefix to %o", pathPrefix);
|
||
this.overrides.pathPrefix = pathPrefix;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gets the current path prefix denoting the root folder the output will be deployed to
|
||
*
|
||
* @returns {String} - The path prefix string
|
||
*/
|
||
getPathPrefix() {
|
||
if (this.overrides.pathPrefix) {
|
||
return this.overrides.pathPrefix;
|
||
}
|
||
|
||
if (!this.hasConfigMerged) {
|
||
throw new Error("Config has not yet merged. Needs `init()`.");
|
||
}
|
||
|
||
return this.config?.pathPrefix;
|
||
}
|
||
|
||
/**
|
||
* Bootstraps the config object.
|
||
*/
|
||
async initializeRootConfig() {
|
||
this.rootConfig = this.customRootConfig;
|
||
if (!this.rootConfig) {
|
||
let { default: cfg } = await EleventyImportRawFromEleventy("./src/defaultConfig.js");
|
||
this.rootConfig = cfg;
|
||
}
|
||
|
||
if (typeof this.rootConfig === "function") {
|
||
// Not yet using async in defaultConfig.js
|
||
this.rootConfig = this.rootConfig.call(this, this.userConfig);
|
||
}
|
||
|
||
debug("Default Eleventy config %o", this.rootConfig);
|
||
}
|
||
|
||
/*
|
||
* Add additional overrides to the root config object, used for testing
|
||
*
|
||
* @param {object} - a subset of the return Object from the user’s config file.
|
||
*/
|
||
appendToRootConfig(obj) {
|
||
Object.assign(this.rootConfig, obj);
|
||
}
|
||
|
||
/*
|
||
* Process the userland plugins from the Config
|
||
*
|
||
* @param {object} - the return Object from the user’s config file.
|
||
*/
|
||
async processPlugins({ dir, pathPrefix }) {
|
||
this.userConfig.dir = dir;
|
||
this.userConfig.pathPrefix = pathPrefix;
|
||
|
||
// for Nested addPlugin calls, Issue #1925
|
||
this.userConfig._enablePluginExecution();
|
||
|
||
let storedActiveNamespace = this.userConfig.activeNamespace;
|
||
for (let { plugin, options, pluginNamespace } of this.userConfig.plugins) {
|
||
try {
|
||
this.userConfig.activeNamespace = pluginNamespace;
|
||
await this.userConfig._executePlugin(plugin, options);
|
||
} catch (e) {
|
||
let name = this.userConfig._getPluginName(plugin);
|
||
let namespaces = [storedActiveNamespace, pluginNamespace].filter((entry) => !!entry);
|
||
|
||
let namespaceStr = "";
|
||
if (namespaces.length) {
|
||
namespaceStr = ` (namespace: ${namespaces.join(".")})`;
|
||
}
|
||
|
||
throw new EleventyPluginError(
|
||
`Error processing ${name ? `the \`${name}\`` : "a"} plugin${namespaceStr}`,
|
||
e,
|
||
);
|
||
}
|
||
}
|
||
|
||
this.userConfig.activeNamespace = storedActiveNamespace;
|
||
|
||
this.userConfig._disablePluginExecution();
|
||
}
|
||
|
||
/**
|
||
* Fetches and executes the local configuration file
|
||
*
|
||
* @returns {Promise<object>} merged - The merged config file object.
|
||
*/
|
||
async requireLocalConfigFile() {
|
||
let localConfig = {};
|
||
let exportedConfig = {};
|
||
|
||
let path = this.projectConfigPaths.filter((path) => path).find((path) => fs.existsSync(path));
|
||
|
||
if (this.projectConfigPaths.length > 0 && this.#configManuallyDefined && !path) {
|
||
throw new EleventyConfigError(
|
||
"A configuration file was specified but not found: " + this.projectConfigPaths.join(", "),
|
||
);
|
||
}
|
||
|
||
debug(`Merging default config with ${path}`);
|
||
if (path) {
|
||
try {
|
||
let { default: configDefaultReturn, config: exportedConfigObject } =
|
||
await EleventyImportRaw(path, this.isEsm ? "esm" : "cjs");
|
||
|
||
exportedConfig = exportedConfigObject || {};
|
||
|
||
if (this.directories && Object.keys(exportedConfigObject?.dir || {}).length > 0) {
|
||
debug(
|
||
"Setting directories via `config.dir` export from config file: %o",
|
||
exportedConfigObject.dir,
|
||
);
|
||
this.directories.setViaConfigObject(exportedConfigObject.dir);
|
||
}
|
||
|
||
if (typeof configDefaultReturn === "function") {
|
||
localConfig = await configDefaultReturn(this.userConfig);
|
||
} else {
|
||
localConfig = configDefaultReturn;
|
||
}
|
||
|
||
// Removed a check for `filters` in 3.0.0-alpha.6 (now using addTransform instead) https://v3.11ty.dev/docs/config/#transforms
|
||
} catch (err) {
|
||
let isModuleError =
|
||
err instanceof Error && (err?.message || "").includes("Cannot find module");
|
||
|
||
// TODO the error message here is bad and I feel bad (needs more accurate info)
|
||
return Promise.reject(
|
||
new EleventyConfigError(
|
||
`Error in your Eleventy config file '${path}'.` +
|
||
(isModuleError ? chalk.cyan(" You may need to run `npm install`.") : ""),
|
||
err,
|
||
),
|
||
);
|
||
}
|
||
} else {
|
||
debug(
|
||
"Project config file not found (not an error—skipping). Looked in: %o",
|
||
this.projectConfigPaths,
|
||
);
|
||
}
|
||
|
||
return {
|
||
localConfig,
|
||
exportedConfig,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Merges different config files together.
|
||
*
|
||
* @returns {Promise<object>} merged - The merged config file.
|
||
*/
|
||
async mergeConfig() {
|
||
let { localConfig, exportedConfig } = await this.requireLocalConfigFile();
|
||
|
||
// Merge `export const config = {}` with `return {}` in config callback
|
||
if (isPlainObject(exportedConfig)) {
|
||
localConfig = Merge(localConfig || {}, exportedConfig);
|
||
}
|
||
|
||
if (this.directories) {
|
||
if (Object.keys(this.userConfig.directoryAssignments || {}).length > 0) {
|
||
debug(
|
||
"Setting directories via set*Directory configuration APIs %o",
|
||
this.userConfig.directoryAssignments,
|
||
);
|
||
this.directories.setViaConfigObject(this.userConfig.directoryAssignments);
|
||
}
|
||
|
||
if (localConfig && Object.keys(localConfig?.dir || {}).length > 0) {
|
||
debug(
|
||
"Setting directories via `dir` object return from configuration file: %o",
|
||
localConfig.dir,
|
||
);
|
||
this.directories.setViaConfigObject(localConfig.dir);
|
||
}
|
||
}
|
||
|
||
// `templateFormats` is an override via `setTemplateFormats`
|
||
if (this.userConfig?.templateFormats) {
|
||
this.templateFormats.setViaConfig(this.userConfig.templateFormats);
|
||
} else if (localConfig?.templateFormats || this.rootConfig?.templateFormats) {
|
||
// Local project config or defaultConfig.js
|
||
this.templateFormats.setViaConfig(
|
||
localConfig.templateFormats || this.rootConfig?.templateFormats,
|
||
);
|
||
}
|
||
|
||
// `templateFormatsAdded` is additive via `addTemplateFormats`
|
||
if (this.userConfig?.templateFormatsAdded) {
|
||
this.templateFormats.addViaConfig(this.userConfig.templateFormatsAdded);
|
||
}
|
||
|
||
let mergedConfig = Merge({}, this.rootConfig, localConfig);
|
||
|
||
// Setup a few properties for plugins:
|
||
|
||
// Set frozen templateFormats
|
||
mergedConfig.templateFormats = Object.freeze(this.templateFormats.getTemplateFormats());
|
||
|
||
// Setup pathPrefix set via command line for plugin consumption
|
||
if (this.overrides.pathPrefix) {
|
||
mergedConfig.pathPrefix = this.overrides.pathPrefix;
|
||
}
|
||
|
||
// Returning a falsy value (e.g. "") from user config should reset to the default value.
|
||
if (!mergedConfig.pathPrefix) {
|
||
mergedConfig.pathPrefix = this.rootConfig.pathPrefix;
|
||
}
|
||
|
||
// This is not set in UserConfig.js so that getters aren’t converted to strings
|
||
// We want to error if someone attempts to use a setter there.
|
||
if (this.directories) {
|
||
mergedConfig.directories = this.directories.getUserspaceInstance();
|
||
}
|
||
|
||
// Delay processing plugins until after the result of localConfig is returned
|
||
// But BEFORE the rest of the config options are merged
|
||
// this way we can pass directories and other template information to plugins
|
||
|
||
await this.userConfig.events.emit("eleventy.beforeConfig", this.userConfig);
|
||
|
||
let pluginsBench = this.aggregateBenchmark.get("Processing plugins in config");
|
||
pluginsBench.before();
|
||
await this.processPlugins(mergedConfig);
|
||
pluginsBench.after();
|
||
|
||
// Template formats added via plugins
|
||
if (this.userConfig?.templateFormatsAdded) {
|
||
this.templateFormats.addViaConfig(this.userConfig.templateFormatsAdded);
|
||
mergedConfig.templateFormats = Object.freeze(this.templateFormats.getTemplateFormats());
|
||
}
|
||
|
||
let eleventyConfigApiMergingObject = this.userConfig.getMergingConfigObject();
|
||
|
||
if ("templateFormats" in eleventyConfigApiMergingObject) {
|
||
throw new Error(
|
||
"Internal error: templateFormats should not return from `getMergingConfigObject`",
|
||
);
|
||
}
|
||
|
||
// Overrides are only used by pathPrefix
|
||
debug("Configuration overrides: %o", this.overrides);
|
||
Merge(mergedConfig, eleventyConfigApiMergingObject, this.overrides);
|
||
|
||
debug("Current configuration: %o", mergedConfig);
|
||
|
||
// Add to the merged config too
|
||
mergedConfig.uses = this.usesGraph;
|
||
|
||
// this is used for the layouts event
|
||
this.usesGraph.setConfig(mergedConfig);
|
||
|
||
return mergedConfig;
|
||
}
|
||
|
||
get usesGraph() {
|
||
if (!this._usesGraph) {
|
||
this._usesGraph = new GlobalDependencyMap();
|
||
this._usesGraph.setIsEsm(this.isEsm);
|
||
this._usesGraph.setTemplateConfig(this);
|
||
}
|
||
return this._usesGraph;
|
||
}
|
||
|
||
get uses() {
|
||
if (!this.usesGraph) {
|
||
throw new Error("The Eleventy Global Dependency Graph has not yet been initialized.");
|
||
}
|
||
return this.usesGraph;
|
||
}
|
||
|
||
get existsCache() {
|
||
if (!this._existsCache) {
|
||
this._existsCache = new ExistsCache();
|
||
this._existsCache.setDirectoryCheck(true);
|
||
}
|
||
return this._existsCache;
|
||
}
|
||
}
|
||
|
||
export default TemplateConfig;
|