Files
fedilearns/node_modules/@11ty/eleventy/src/UserConfig.js

1340 lines
36 KiB
JavaScript
Raw Normal View History

2026-04-29 08:30:52 -07:00
import chalk from "kleur";
import { DateTime } from "luxon";
import yaml from "js-yaml";
import matter from "gray-matter";
import debugUtil from "debug";
import { DeepCopy, TemplatePath, isPlainObject } from "@11ty/eleventy-utils";
import HtmlBasePlugin from "./Plugins/HtmlBasePlugin.js";
import RenderPlugin from "./Plugins/RenderPlugin.js";
import InputPathToUrlPlugin from "./Plugins/InputPathToUrl.js";
import isAsyncFunction from "./Util/IsAsyncFunction.js";
import objectFilter from "./Util/Objects/ObjectFilter.js";
import EventEmitter from "./Util/AsyncEventEmitter.js";
import EleventyCompatibility from "./Util/Compatibility.js";
import EleventyBaseError from "./Errors/EleventyBaseError.js";
import BenchmarkManager from "./Benchmark/BenchmarkManager.js";
import JavaScriptFrontMatter from "./Engines/FrontMatter/JavaScript.js";
import { augmentFunction } from "./Engines/Util/ContextAugmenter.js";
const debug = debugUtil("Eleventy:UserConfig");
class UserConfigError extends EleventyBaseError {}
/**
* Eleventys user-land Configuration API
* @module 11ty/eleventy/UserConfig
*/
class UserConfig {
/** @type {boolean} */
#pluginExecution = false;
/** @type {boolean} */
#quietModeLocked = false;
/** @type {boolean} */
#dataDeepMergeModified = false;
/** @type {number|undefined} */
#uniqueId;
/** @type {number} */
#concurrency = 1;
// Before using os.availableParallelism(); see https://github.com/11ty/eleventy/issues/3596
constructor() {
// These are completely unnecessary lines to satisfy TypeScript
this.plugins = [];
this.templateFormatsAdded = [];
this.additionalWatchTargets = [];
this.watchTargetsConfigReset = new Set();
this.extensionMap = new Set();
this.dataExtensions = new Map();
this.urlTransforms = [];
this.customDateParsingCallbacks = new Set();
this.ignores = new Set();
this.events = new EventEmitter();
/** @type {object} */
this.directories = {};
/** @type {undefined} */
this.logger;
/** @type {string} */
this.dir;
/** @type {string} */
this.pathPrefix;
/** @type {object} */
this.errorReporting = {};
/** @type {object} */
this.templateHandling = {};
this.reset();
this.#uniqueId = Math.random();
}
// Internally used in TemplateContent for cache keys
_getUniqueId() {
return this.#uniqueId;
}
reset() {
debug("Resetting EleventyConfig to initial values.");
/** @type {EventEmitter} */
this.events = new EventEmitter();
this.events.setMaxListeners(25); // defaults to 10
/** @type {BenchmarkManager} */
this.benchmarkManager = new BenchmarkManager();
/** @type {object} */
this.benchmarks = {
/** @type {import('./Benchmark/BenchmarkGroup.js')} */
config: this.benchmarkManager.get("Configuration"),
/** @type {import('./Benchmark/BenchmarkGroup.js')} */
aggregate: this.benchmarkManager.get("Aggregate"),
};
/** @type {object} */
this.directoryAssignments = {};
/** @type {object} */
this.collections = {};
/** @type {object} */
this.precompiledCollections = {};
this.templateFormats = undefined;
this.templateFormatsAdded = [];
/** @type {object} */
this.universal = {
filters: {},
shortcodes: {},
pairedShortcodes: {},
};
/** @type {object} */
this.liquid = {
options: {},
tags: {},
filters: {},
shortcodes: {},
pairedShortcodes: {},
parameterParsing: "legacy", // or builtin
};
/** @type {object} */
this.nunjucks = {
// `dev: true` gives us better error messaging
environmentOptions: { dev: true },
precompiledTemplates: {},
filters: {},
asyncFilters: {},
tags: {},
globals: {},
shortcodes: {},
pairedShortcodes: {},
asyncShortcodes: {},
asyncPairedShortcodes: {},
};
/** @type {object} */
this.javascript = {
functions: {},
filters: {},
shortcodes: {},
pairedShortcodes: {},
};
this.markdownHighlighter = null;
/** @type {object} */
this.libraryOverrides = {};
/** @type {object} */
this.passthroughCopies = {};
this.passthroughCopiesHtmlRelative = new Set();
/** @type {object} */
this.layoutAliases = {};
this.layoutResolution = true; // extension-less layout files
/** @type {object} */
this.linters = {};
/** @type {object} */
this.transforms = {};
/** @type {object} */
this.preprocessors = {};
this.activeNamespace = "";
this.DateTime = DateTime;
this.dynamicPermalinks = true;
this.useGitIgnore = true;
let defaultIgnores = new Set();
defaultIgnores.add("**/node_modules/**");
defaultIgnores.add(".git/**"); // TODO `**/.git/**`
this.ignores = new Set(defaultIgnores);
this.watchIgnores = new Set(defaultIgnores);
this.dataDeepMerge = true;
this.extensionMap = new Set();
/** @type {object} */
this.extensionConflictMap = {};
this.watchJavaScriptDependencies = true;
this.additionalWatchTargets = [];
this.watchTargetsConfigReset = new Set();
/** @type {object} */
this.serverOptions = {};
/** @type {object} */
this.globalData = {};
/** @type {object} */
this.chokidarConfig = {};
this.watchThrottleWaitTime = 0; //ms
// using Map to preserve insertion order
this.dataExtensions = new Map();
this.quietMode = false;
this.plugins = [];
this.useTemplateCache = true;
this.dataFilterSelectors = new Set();
/** @type {object} */
this.libraryAmendments = {};
this.serverPassthroughCopyBehavior = "copy"; // or "passthrough"
this.urlTransforms = [];
// Defaults in `defaultConfig.js`
this.dataFileSuffixesOverride = false;
this.dataFileDirBaseNameOverride = false;
/** @type {object} */
this.frontMatterParsingOptions = {
// Set a project-wide default.
// language: "yaml",
// Supplementary engines
engines: {
yaml: yaml.load.bind(yaml),
// Backwards compatible with `js` object front matter
// https://github.com/11ty/eleventy/issues/2819
javascript: JavaScriptFrontMatter,
// Needed for fallback behavior in the new `javascript` engine
// @ts-ignore
jsLegacy: matter.engines.javascript,
node: function () {
throw new Error(
"The `node` front matter type was a 3.0.0-alpha.x only feature, removed for stable release. Rename to `js` or `javascript` instead!",
);
},
},
};
/** @type {object} */
this.virtualTemplates = {};
this.freezeReservedData = true;
this.customDateParsingCallbacks = new Set();
/** @type {object} */
this.errorReporting = {};
/** @type {object} */
this.templateHandling = {};
// Before using os.availableParallelism(); see https://github.com/11ty/eleventy/issues/3596
this.#concurrency = 1;
}
// compatibleRange is optional in 2.0.0-beta.2
versionCheck(compatibleRange) {
let compat = new EleventyCompatibility(compatibleRange);
if (!compat.isCompatible()) {
throw new UserConfigError(compat.getErrorMessage());
}
}
/*
* Events
*/
// Duplicate event bindings are avoided with the `reset` method above.
// A new EventEmitter instance is created when the config is reset.
on(eventName, callback) {
return this.events.on(eventName, callback);
}
once(eventName, callback) {
return this.events.once(eventName, callback);
}
emit(eventName, ...args) {
return this.events.emit(eventName, ...args);
}
setEventEmitterMode(mode) {
this.events.setHandlerMode(mode);
}
/*
* Universal getters
*/
getFilter(name) {
// JavaScript functions are included here for backwards compatibility https://github.com/11ty/eleventy/issues/3365
return this.universal.filters[name] || this.javascript.functions[name];
}
getFilters(options = {}) {
if (options.type) {
return objectFilter(
this.universal.filters,
(entry) => entry.__eleventyInternal?.type === options.type,
);
}
return this.universal.filters;
}
getShortcode(name) {
return this.universal.shortcodes[name];
}
getShortcodes(options = {}) {
if (options.type) {
return objectFilter(
this.universal.shortcodes,
(entry) => entry.__eleventyInternal?.type === options.type,
);
}
return this.universal.shortcodes;
}
getPairedShortcode(name) {
return this.universal.pairedShortcodes[name];
}
getPairedShortcodes(options = {}) {
if (options.type) {
return objectFilter(
this.universal.pairedShortcodes,
(entry) => entry.__eleventyInternal?.type === options.type,
);
}
return this.universal.pairedShortcodes;
}
/*
* Private utilities
*/
#add(target, originalName, callback, options) {
let { description, functionName } = options;
if (typeof callback !== "function") {
throw new Error(`Invalid definition for "${originalName}" ${description}.`);
}
let name = this.getNamespacedName(originalName);
if (target[name]) {
debug(
chalk.yellow(`Warning, overwriting previous ${description} "%o" via \`%o(%o)\``),
name,
functionName,
originalName,
);
} else {
debug(`Adding new ${description} "%o" via \`%o(%o)\``, name, functionName, originalName);
}
target[name] = this.#decorateCallback(`"${name}" ${description}`, callback);
}
#decorateCallback(type, callback) {
return this.benchmarks.config.add(type, callback);
}
/*
* Markdown
*/
// This is a method for plugins, probably shouldnt use this in projects.
// Projects should use `setLibrary` as documented here:
// https://github.com/11ty/eleventy/blob/master/docs/engines/markdown.md#use-your-own-options
addMarkdownHighlighter(highlightFn) {
this.markdownHighlighter = highlightFn;
}
/*
* Filters
*/
addLiquidFilter(name, callback) {
this.#add(this.liquid.filters, name, callback, {
description: "Liquid Filter",
functionName: "addLiquidFilter",
});
}
addNunjucksAsyncFilter(name, callback) {
this.#add(this.nunjucks.asyncFilters, name, callback, {
description: "Nunjucks Filter",
functionName: "addNunjucksAsyncFilter",
});
}
// Support the nunjucks style syntax for asynchronous filter add
addNunjucksFilter(name, callback, isAsync = false) {
if (isAsync) {
// namespacing happens downstream
this.addNunjucksAsyncFilter(name, callback);
} else {
this.#add(this.nunjucks.filters, name, callback, {
description: "Nunjucks Filter",
functionName: "addNunjucksFilter",
});
}
}
addJavaScriptFilter(name, callback) {
this.#add(this.javascript.filters, name, callback, {
description: "JavaScript Filter",
functionName: "addJavaScriptFilter",
});
// Backwards compat for a time before `addJavaScriptFilter` existed.
this.addJavaScriptFunction(name, callback);
}
addFilter(name, callback) {
// This method *requires* `async function` and will not work with `function` that returns a promise
if (isAsyncFunction(callback)) {
this.addAsyncFilter(name, callback);
return;
}
// namespacing happens downstream
this.#add(this.universal.filters, name, callback, {
description: "Universal Filter",
functionName: "addFilter",
});
this.addLiquidFilter(name, callback);
this.addJavaScriptFilter(name, callback);
this.addNunjucksFilter(
name,
/** @this {any} */
function (...args) {
// Note that `callback` is already a function as the `#add` method throws an error if not.
let ret = callback.call(this, ...args);
if (ret instanceof Promise) {
throw new Error(
`Nunjucks *is* async-friendly with \`addFilter("${name}", async function() {})\` but you need to supply an \`async function\`. You returned a promise from \`addFilter("${name}", function() {})\`. Alternatively, use the \`addAsyncFilter("${name}")\` configuration API method.`,
);
}
return ret;
},
);
}
// Liquid, Nunjucks, and JS only
addAsyncFilter(name, callback) {
// namespacing happens downstream
this.#add(this.universal.filters, name, callback, {
description: "Universal Filter",
functionName: "addAsyncFilter",
});
this.addLiquidFilter(name, callback);
this.addJavaScriptFilter(name, callback);
this.addNunjucksAsyncFilter(
name,
/** @this {any} */
async function (...args) {
let cb = args.pop();
// Note that `callback` is already a function as the `#add` method throws an error if not.
let ret = await callback.call(this, ...args);
cb(null, ret);
},
);
}
/*
* Shortcodes
*/
addShortcode(name, callback) {
// This method *requires* `async function` and will not work with `function` that returns a promise
if (isAsyncFunction(callback)) {
this.addAsyncShortcode(name, callback);
return;
}
this.#add(this.universal.shortcodes, name, callback, {
description: "Universal Shortcode",
functionName: "addShortcode",
});
this.addLiquidShortcode(name, callback);
this.addJavaScriptShortcode(name, callback);
this.addNunjucksShortcode(name, callback);
}
addAsyncShortcode(name, callback) {
this.#add(this.universal.shortcodes, name, callback, {
description: "Universal Shortcode",
functionName: "addAsyncShortcode",
});
// Related: #498
this.addNunjucksAsyncShortcode(name, callback);
this.addLiquidShortcode(name, callback);
this.addJavaScriptShortcode(name, callback);
}
addNunjucksAsyncShortcode(name, callback) {
this.#add(this.nunjucks.asyncShortcodes, name, callback, {
description: "Nunjucks Async Shortcode",
functionName: "addNunjucksAsyncShortcode",
});
}
addNunjucksShortcode(name, callback, isAsync = false) {
if (isAsync) {
this.addNunjucksAsyncShortcode(name, callback);
} else {
this.#add(this.nunjucks.shortcodes, name, callback, {
description: "Nunjucks Shortcode",
functionName: "addNunjucksShortcode",
});
}
}
addLiquidShortcode(name, callback) {
this.#add(this.liquid.shortcodes, name, callback, {
description: "Liquid Shortcode",
functionName: "addLiquidShortcode",
});
}
addPairedShortcode(name, callback) {
// This method *requires* `async function` and will not work with `function` that returns a promise
if (isAsyncFunction(callback)) {
this.addPairedAsyncShortcode(name, callback);
return;
}
this.#add(this.universal.pairedShortcodes, name, callback, {
description: "Universal Paired Shortcode",
functionName: "addPairedShortcode",
});
this.addPairedNunjucksShortcode(name, callback);
this.addPairedLiquidShortcode(name, callback);
this.addPairedJavaScriptShortcode(name, callback);
}
// Related: #498
addPairedAsyncShortcode(name, callback) {
this.#add(this.universal.pairedShortcodes, name, callback, {
description: "Universal Paired Async Shortcode",
functionName: "addPairedAsyncShortcode",
});
this.addPairedNunjucksAsyncShortcode(name, callback);
this.addPairedLiquidShortcode(name, callback);
this.addPairedJavaScriptShortcode(name, callback);
}
addPairedNunjucksAsyncShortcode(name, callback) {
this.#add(this.nunjucks.asyncPairedShortcodes, name, callback, {
description: "Nunjucks Async Paired Shortcode",
functionName: "addPairedNunjucksAsyncShortcode",
});
}
addPairedNunjucksShortcode(name, callback, isAsync = false) {
if (isAsync) {
this.addPairedNunjucksAsyncShortcode(name, callback);
} else {
this.#add(this.nunjucks.pairedShortcodes, name, callback, {
description: "Nunjucks Paired Shortcode",
functionName: "addPairedNunjucksShortcode",
});
}
}
addPairedLiquidShortcode(name, callback) {
this.#add(this.liquid.pairedShortcodes, name, callback, {
description: "Liquid Paired Shortcode",
functionName: "addPairedLiquidShortcode",
});
}
addJavaScriptShortcode(name, callback) {
this.#add(this.javascript.shortcodes, name, callback, {
description: "JavaScript Shortcode",
functionName: "addJavaScriptShortcode",
});
// Backwards compat for a time before `addJavaScriptShortcode` existed.
this.addJavaScriptFunction(name, callback);
}
addPairedJavaScriptShortcode(name, callback) {
this.#add(this.javascript.pairedShortcodes, name, callback, {
description: "JavaScript Paired Shortcode",
functionName: "addPairedJavaScriptShortcode",
});
// Backwards compat for a time before `addJavaScriptShortcode` existed.
this.addJavaScriptFunction(name, callback);
}
// Both Filters and shortcodes feed into this
addJavaScriptFunction(name, callback) {
this.#add(this.javascript.functions, name, callback, {
description: "JavaScript Function",
functionName: "addJavaScriptFunction",
});
}
/*
* Custom Tags
*/
// tagCallback: function(liquidEngine) { return { parse: …, render: … }} };
addLiquidTag(name, tagFn) {
if (typeof tagFn !== "function") {
throw new UserConfigError(
`EleventyConfig.addLiquidTag expects a callback function to be passed in for ${name}: addLiquidTag(name, function(liquidEngine) { return { parse: …, render: … } })`,
);
}
this.#add(this.liquid.tags, name, tagFn, {
description: "Liquid Custom Tag",
functionName: "addLiquidTag",
});
}
addNunjucksTag(name, tagFn) {
if (typeof tagFn !== "function") {
throw new UserConfigError(
`EleventyConfig.addNunjucksTag expects a callback function to be passed in for ${name}: addNunjucksTag(name, function(nunjucksEngine) {})`,
);
}
this.#add(this.nunjucks.tags, name, tagFn, {
description: "Nunjucks Custom Tag",
functionName: "addNunjucksTag",
});
}
/*
* Plugins
*/
// Internal method
_enablePluginExecution() {
this.#pluginExecution = true;
}
// Internal method
_disablePluginExecution() {
this.#pluginExecution = false;
}
/* Config is executed in two stages and plugins are the second stage—are we in the plugins stage? */
isPluginExecution() {
return this.#pluginExecution;
}
/**
* @typedef {function|Promise<function>|object} PluginDefinition
* @property {Function} [configFunction]
* @property {string} [eleventyPackage]
* @property {object} [eleventyPluginOptions={}]
* @property {boolean} [eleventyPluginOptions.unique]
*/
/**
* addPlugin: async friendly in 3.0
*
* @param {PluginDefinition} plugin
*/
addPlugin(plugin, options = {}) {
// First addPlugin of a unique plugin wins
if (plugin?.eleventyPluginOptions?.unique && this.hasPlugin(plugin)) {
debug("Skipping duplicate unique addPlugin for %o", this._getPluginName(plugin));
return;
}
if (this.isPluginExecution() || options?.immediate) {
// this might return a promise
return this._executePlugin(plugin, options);
} else {
this.plugins.push({
plugin,
options,
pluginNamespace: this.activeNamespace,
});
}
}
/** @param {string} name */
resolvePlugin(name) {
let filenameLookup = {
"@11ty/eleventy/html-base-plugin": HtmlBasePlugin,
"@11ty/eleventy/render-plugin": RenderPlugin,
"@11ty/eleventy/inputpath-to-url-plugin": InputPathToUrlPlugin,
// Async plugins:
// requires e.g. `await resolvePlugin("@11ty/eleventy/i18n-plugin")` to avoid preloading i18n dependencies.
// see https://github.com/11ty/eleventy-plugin-rss/issues/52
"@11ty/eleventy/i18n-plugin": "./Plugins/I18nPlugin.js",
};
if (!filenameLookup[name]) {
throw new Error(
`Invalid name "${name}" passed to resolvePlugin. Valid options: ${Object.keys(filenameLookup).join(", ")}`,
);
}
// Future improvement: add support for any npm package name?
if (typeof filenameLookup[name] === "string") {
// returns promise
return import(filenameLookup[name]).then((plugin) => plugin.default);
}
// return reference
return filenameLookup[name];
}
/** @param {string|PluginDefinition} plugin */
hasPlugin(plugin) {
let pluginName;
if (typeof plugin === "string") {
pluginName = plugin;
} else {
pluginName = this._getPluginName(plugin);
}
return this.plugins.some((entry) => this._getPluginName(entry.plugin) === pluginName);
}
// Using Function.name https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name#examples
/** @param {PluginDefinition} plugin */
_getPluginName(plugin) {
if (plugin?.eleventyPackage) {
return plugin.eleventyPackage;
}
if (typeof plugin === "function") {
return plugin.name;
}
if (plugin?.configFunction && typeof plugin.configFunction === "function") {
return plugin.configFunction.name;
}
}
// Starting in 3.0 the plugin callback might be asynchronous!
_executePlugin(plugin, options) {
let name = this._getPluginName(plugin);
let ret;
debug(`Adding %o plugin`, name || "anonymous");
let pluginBenchmark = this.benchmarks.aggregate.get("Configuration addPlugin");
if (typeof plugin === "function") {
pluginBenchmark.before();
this.benchmarks.config;
let configFunction = plugin;
ret = configFunction(this, options);
pluginBenchmark.after();
} else if (plugin?.configFunction) {
pluginBenchmark.before();
if (options && typeof options.init === "function") {
// init is not yet async-friendly but its also barely used
options.init.call(this, plugin.initArguments || {});
}
ret = plugin.configFunction(this, options);
pluginBenchmark.after();
} else {
throw new UserConfigError(
"Invalid EleventyConfig.addPlugin signature. Should be a function or a valid Eleventy plugin object.",
);
}
return ret;
}
/** @param {string} name */
getNamespacedName(name) {
return this.activeNamespace + name;
}
async namespace(pluginNamespace, callback) {
let validNamespace = pluginNamespace && typeof pluginNamespace === "string";
if (validNamespace) {
this.activeNamespace = pluginNamespace || "";
}
await callback(this);
if (validNamespace) {
this.activeNamespace = "";
}
}
/**
* Adds a path to a file or directory to the list of pass-through copies
* which are copied as-is to the output.
*
* @param {string|object} fileOrDir The path to the file or directory that should
* be copied. OR an object where the key is the input glob and the property is the output directory
* @param {object} copyOptions options for recursive-copy.
* see https://www.npmjs.com/package/recursive-copy#arguments
* default options are defined in TemplatePassthrough copyOptionsDefault
* @returns {any} a reference to the `EleventyConfig` object.
*/
addPassthroughCopy(fileOrDir, copyOptions = {}) {
if (copyOptions.mode) {
if (copyOptions.mode !== "html-relative") {
throw new Error(
"Invalid `mode` option for `addPassthroughCopy`. Received: '" + copyOptions.mode + "'",
);
}
if (isPlainObject(fileOrDir)) {
throw new Error(
"mode: 'html-relative' does not yet support passthrough copy objects (input -> output mapping). Use a string glob or an Array of string globs.",
);
}
this.passthroughCopiesHtmlRelative?.add({
match: fileOrDir,
...copyOptions,
});
} else if (typeof fileOrDir === "string") {
this.passthroughCopies[fileOrDir] = { outputPath: true, copyOptions };
} else {
for (let [inputPath, outputPath] of Object.entries(fileOrDir)) {
this.passthroughCopies[inputPath] = { outputPath, copyOptions };
}
}
return this;
}
/*
* Template Formats
*/
_normalizeTemplateFormats() {
throw new Error("The internal _normalizeTemplateFormats() method was removed in Eleventy 3.0");
}
setTemplateFormats(templateFormats) {
this.templateFormats = templateFormats;
}
// additive, usually for plugins
addTemplateFormats(templateFormats) {
this.templateFormatsAdded.push(templateFormats);
}
/*
* Library Overrides and Options
*/
setLibrary(engineName, libraryInstance) {
if (engineName === "liquid" && Object.keys(this.liquid.options).length) {
debug(
"WARNING: using `eleventyConfig.setLibrary` will override any configuration set using `.setLiquidOptions` via the config API. Youll need to pass these options to the library yourself.",
);
} else if (engineName === "njk" && Object.keys(this.nunjucks.environmentOptions).length) {
debug(
"WARNING: using `eleventyConfig.setLibrary` will override any configuration set using `.setNunjucksEnvironmentOptions` via the config API. Youll need to pass these options to the library yourself.",
);
}
this.libraryOverrides[engineName.toLowerCase()] = libraryInstance;
}
/* These callbacks run on both libraryOverrides and default library instances */
amendLibrary(engineName, callback) {
let name = engineName.toLowerCase();
if (!this.libraryAmendments[name]) {
this.libraryAmendments[name] = [];
}
this.libraryAmendments[name].push(callback);
}
setLiquidOptions(options) {
this.liquid.options = options;
}
setLiquidParameterParsing(behavior) {
if (behavior !== "legacy" && behavior !== "builtin") {
throw new Error(
`Invalid argument passed to \`setLiquidParameterParsing\`. Expected one of "legacy" or "builtin".`,
);
}
this.liquid.parameterParsing = behavior;
}
setNunjucksEnvironmentOptions(options) {
this.nunjucks.environmentOptions = options;
}
setNunjucksPrecompiledTemplates(templates) {
this.nunjucks.precompiledTemplates = templates;
}
setDynamicPermalinks(enabled) {
this.dynamicPermalinks = !!enabled;
}
setUseGitIgnore(enabled) {
this.useGitIgnore = !!enabled;
}
setDataDeepMerge(deepMerge) {
this.#dataDeepMergeModified = true;
this.dataDeepMerge = !!deepMerge;
}
// Used by the Upgrade Helper Plugin
isDataDeepMergeModified() {
return this.#dataDeepMergeModified;
}
addWatchTarget(additionalWatchTargets, options = {}) {
// Reset the config when the target path changes
if (options.resetConfig) {
this.watchTargetsConfigReset.add(additionalWatchTargets);
}
this.additionalWatchTargets.push(additionalWatchTargets);
}
setWatchJavaScriptDependencies(watchEnabled) {
this.watchJavaScriptDependencies = !!watchEnabled;
}
setServerOptions(options = {}, override = false) {
if (override) {
this.serverOptions = options;
} else {
this.serverOptions = DeepCopy(this.serverOptions, options);
}
}
setBrowserSyncConfig() {
this._attemptedBrowserSyncUse = true;
debug(
"The `setBrowserSyncConfig` method was removed in Eleventy 2.0.0. Use `setServerOptions` with the new Eleventy development server or the `@11ty/eleventy-browser-sync` plugin moving forward.",
);
}
setChokidarConfig(options = {}) {
this.chokidarConfig = options;
}
setWatchThrottleWaitTime(time = 0) {
this.watchThrottleWaitTime = time;
}
// 3.0 change: this does a top level merge instead of reset.
setFrontMatterParsingOptions(options = {}) {
DeepCopy(this.frontMatterParsingOptions, options);
}
/* Internal method for CLI --quiet */
_setQuietModeOverride(quietMode) {
this.setQuietMode(quietMode);
this.#quietModeLocked = true;
}
setQuietMode(quietMode) {
if (this.#quietModeLocked) {
debug(
"Attempt to `setQuietMode(%o)` ignored, --quiet command line argument override in place.",
!!quietMode,
);
// override via CLI takes precedence
return;
}
this.quietMode = !!quietMode;
}
addExtension(fileExtension, options = {}) {
let extensions;
// Array support added in 2.0.0-canary.19
if (Array.isArray(fileExtension)) {
extensions = fileExtension;
} else {
// single string
extensions = [fileExtension];
}
for (let extension of extensions) {
if (this.extensionConflictMap[extension]) {
throw new Error(
`An attempt was made to override the "${extension}" template syntax twice (via the \`addExtension\` configuration API). A maximum of one override is currently supported.`,
);
}
this.extensionConflictMap[extension] = true;
/** @type {object} */
let extensionOptions = Object.assign(
{
// Might be overridden for aliasing in options.key
key: extension,
extension: extension,
},
options,
);
if (extensionOptions.key !== extensionOptions.extension) {
extensionOptions.aliasKey = extensionOptions.extension;
}
this.extensionMap.add(extensionOptions);
}
}
addDataExtension(extensionList, parser) {
let options = {};
// second argument is an object with a `parser` callback
if (typeof parser !== "function") {
if (!("parser" in parser)) {
throw new Error(
"Expected `parser` property in second argument object to `eleventyConfig.addDataExtension`",
);
}
options = parser;
parser = options.parser;
}
let extensions = extensionList.split(",").map((s) => s.trim());
for (let extension of extensions) {
this.dataExtensions.set(extension, {
extension,
parser,
options,
});
}
}
setUseTemplateCache(bypass) {
this.useTemplateCache = !!bypass;
}
setPrecompiledCollections(collections) {
this.precompiledCollections = collections;
}
// "passthrough" is the default, no other value is explicitly required in code
// but opt-out via "copy" is suggested
setServerPassthroughCopyBehavior(behavior) {
this.serverPassthroughCopyBehavior = behavior;
}
// Url transforms change page.url and work good with server side content-negotiation (e.g. i18n plugin)
addUrlTransform(callback) {
this.urlTransforms.push(callback);
}
setDataFileSuffixes(suffixArray) {
this.dataFileSuffixesOverride = suffixArray;
}
setDataFileBaseName(baseName) {
this.dataFileDirBaseNameOverride = baseName;
}
addTemplate(virtualInputPath, content, data) {
// Lookups keys must be normalized
virtualInputPath = TemplatePath.stripLeadingDotSlash(
TemplatePath.standardizeFilePath(virtualInputPath),
);
if (this.virtualTemplates[virtualInputPath]) {
throw new Error(
"Virtual template conflict: you cant add multiple virtual templates that have the same inputPath: " +
virtualInputPath,
);
}
this.virtualTemplates[virtualInputPath] = {
inputPath: virtualInputPath,
data,
content,
};
}
isVirtualTemplate(virtualInputPath) {
return Boolean(this.virtualTemplates[virtualInputPath]);
}
#setDirectory(key, dir) {
if (this.isPluginExecution()) {
throw new Error(
"The `set*Directory` configuration API methods are not yet allowed in plugins.",
);
}
this.directoryAssignments[key] = dir;
}
setInputDirectory(dir) {
this.#setDirectory("input", dir);
}
setOutputDirectory(dir) {
this.#setDirectory("output", dir);
}
setDataDirectory(dir) {
this.#setDirectory("data", dir);
}
setIncludesDirectory(dir) {
this.#setDirectory("includes", dir);
}
setLayoutsDirectory(dir) {
this.#setDirectory("layouts", dir);
}
// Some data keywords in Eleventy are reserved, throw an error if an application tries to set these.
setFreezeReservedData(bool) {
this.freezeReservedData = !!bool;
}
addDateParsing(callback) {
if (typeof callback === "function") {
this.customDateParsingCallbacks.add(callback);
} else {
throw new Error("addDateParsing expects a function argument.");
}
}
// 3.0.0-alpha.18 started merging conflicts here (when possible), issue #3389
addGlobalData(name, data) {
name = this.getNamespacedName(name);
if (this.globalData[name]) {
if (isPlainObject(this.globalData[name]) && isPlainObject(data)) {
DeepCopy(this.globalData[name], data);
} else {
debug("Warning: overwriting a previous value set with addGlobalData(%o)", name);
this.globalData[name] = data;
}
} else {
this.globalData[name] = data;
}
return this;
}
addNunjucksGlobal(name, globalType) {
name = this.getNamespacedName(name);
if (this.nunjucks.globals[name]) {
debug(
chalk.yellow("Warning, overwriting a Nunjucks global with `addNunjucksGlobal(%o)`"),
name,
);
}
if (typeof globalType === "function") {
this.nunjucks.globals[name] = this.#decorateCallback(`"${name}" Nunjucks Global`, globalType);
} else {
this.nunjucks.globals[name] = globalType;
}
}
addTransform(name, callback) {
name = this.getNamespacedName(name);
this.transforms[name] = this.#decorateCallback(`"${name}" Transform`, callback);
}
addPreprocessor(name, fileExtensions, callback) {
name = this.getNamespacedName(name);
this.preprocessors[name] = {
filter: fileExtensions,
callback: this.#decorateCallback(`"${name}" Preprocessor`, callback),
};
}
addLinter(name, callback) {
name = this.getNamespacedName(name);
this.linters[name] = this.#decorateCallback(`"${name}" Linter`, callback);
}
addLayoutAlias(from, to) {
this.layoutAliases[from] = to;
}
setLayoutResolution(resolution) {
this.layoutResolution = !!resolution;
}
// compat
enableLayoutResolution() {
this.layoutResolution = true;
}
configureErrorReporting(options = {}) {
// allowMissingExtensions: true
Object.assign(this.errorReporting, options);
}
configureTemplateHandling(options = {}) {
// writeMode: "sync" // "async"
Object.assign(this.templateHandling, options);
}
/*
* Collections
*/
// get config defined collections
getCollections() {
return this.collections;
}
addCollection(name, callback) {
name = this.getNamespacedName(name);
if (this.collections[name]) {
throw new UserConfigError(
`config.addCollection(${name}) already exists. Try a different name for your collection.`,
);
}
this.collections[name] = callback;
}
augmentFunctionContext(fn, options) {
let t = typeof fn;
if (t !== "function") {
throw new UserConfigError(
"Invalid type passed to `augmentFunctionContext`—function was expected and received: " + t,
);
}
return augmentFunction(fn, options);
}
setConcurrency(number) {
if (typeof number !== "number") {
throw new UserConfigError("Argument passed to `setConcurrency` must be a number.");
}
this.#concurrency = number;
}
getConcurrency() {
return this.#concurrency;
}
getMergingConfigObject() {
let obj = {
// filters removed in 1.0 (use addTransform instead)
transforms: this.transforms,
linters: this.linters,
preprocessors: this.preprocessors,
globalData: this.globalData,
layoutAliases: this.layoutAliases,
layoutResolution: this.layoutResolution,
passthroughCopiesHtmlRelative: this.passthroughCopiesHtmlRelative,
passthroughCopies: this.passthroughCopies,
// Liquid
liquidOptions: this.liquid.options,
liquidTags: this.liquid.tags,
liquidFilters: this.liquid.filters,
liquidShortcodes: this.liquid.shortcodes,
liquidPairedShortcodes: this.liquid.pairedShortcodes,
liquidParameterParsing: this.liquid.parameterParsing,
// Nunjucks
nunjucksEnvironmentOptions: this.nunjucks.environmentOptions,
nunjucksPrecompiledTemplates: this.nunjucks.precompiledTemplates,
nunjucksFilters: this.nunjucks.filters,
nunjucksAsyncFilters: this.nunjucks.asyncFilters,
nunjucksTags: this.nunjucks.tags,
nunjucksGlobals: this.nunjucks.globals,
nunjucksAsyncShortcodes: this.nunjucks.asyncShortcodes,
nunjucksShortcodes: this.nunjucks.shortcodes,
nunjucksAsyncPairedShortcodes: this.nunjucks.asyncPairedShortcodes,
nunjucksPairedShortcodes: this.nunjucks.pairedShortcodes,
// 11ty.js
javascriptFunctions: this.javascript.functions, // filters and shortcodes, combined
javascriptShortcodes: this.javascript.shortcodes,
javascriptPairedShortcodes: this.javascript.pairedShortcodes,
javascriptFilters: this.javascript.filters,
// Markdown
markdownHighlighter: this.markdownHighlighter,
libraryOverrides: this.libraryOverrides,
dynamicPermalinks: this.dynamicPermalinks,
useGitIgnore: this.useGitIgnore,
ignores: this.ignores,
watchIgnores: this.watchIgnores,
dataDeepMerge: this.dataDeepMerge,
watchJavaScriptDependencies: this.watchJavaScriptDependencies,
additionalWatchTargets: this.additionalWatchTargets,
watchTargetsConfigReset: this.watchTargetsConfigReset,
serverOptions: this.serverOptions,
chokidarConfig: this.chokidarConfig,
watchThrottleWaitTime: this.watchThrottleWaitTime,
frontMatterParsingOptions: this.frontMatterParsingOptions,
dataExtensions: this.dataExtensions,
extensionMap: this.extensionMap,
quietMode: this.quietMode,
events: this.events,
benchmarkManager: this.benchmarkManager,
plugins: this.plugins,
useTemplateCache: this.useTemplateCache,
precompiledCollections: this.precompiledCollections,
dataFilterSelectors: this.dataFilterSelectors,
libraryAmendments: this.libraryAmendments,
serverPassthroughCopyBehavior: this.serverPassthroughCopyBehavior,
urlTransforms: this.urlTransforms,
virtualTemplates: this.virtualTemplates,
// `directories` and `directoryAssignments` are merged manually prior to plugin processing
freezeReservedData: this.freezeReservedData,
customDateParsing: this.customDateParsingCallbacks,
errorReporting: this.errorReporting,
templateHandling: this.templateHandling,
};
if (Array.isArray(this.dataFileSuffixesOverride)) {
// no upstream merging of this array, so we add the override: prefix
obj["override:dataFileSuffixes"] = this.dataFileSuffixesOverride;
}
if (this.dataFileDirBaseNameOverride) {
obj.dataFileDirBaseNameOverride = this.dataFileDirBaseNameOverride;
}
return obj;
}
// No-op functions for backwards compat
addHandlebarsHelper() {}
setPugOptions() {}
setEjsOptions() {}
addHandlebarsShortcode() {}
addPairedHandlebarsShortcode() {}
}
export default UserConfig;