Files
fedilearns/node_modules/@11ty/eleventy/src/UserConfig.js
2026-04-29 08:30:52 -07:00

1340 lines
36 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 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;