1340 lines
36 KiB
JavaScript
1340 lines
36 KiB
JavaScript
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 {}
|
||
|
||
/**
|
||
* Eleventy’s 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 shouldn’t 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 it’s 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. You’ll 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. You’ll 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 can’t 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;
|