Socket
Socket
Sign inDemoInstall

@11ty/eleventy

Package Overview
Dependencies
40
Maintainers
1
Versions
180
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.2 to 2.0.0

src/EleventyShortcodeError.js

31

cmd.js

@@ -20,11 +20,3 @@ #!/usr/bin/env node

const argv = require("minimist")(process.argv.slice(2), {
string: [
"input",
"output",
"formats",
"config",
"pathprefix",
"port",
"to",
],
string: ["input", "output", "formats", "config", "pathprefix", "port", "to"],
boolean: [

@@ -37,7 +29,8 @@ "quiet",

"serve",
"passthroughall",
"incremental",
"ignore-initial",
],
default: {
quiet: null,
"ignore-initial": false,
},

@@ -61,6 +54,3 @@ unknown: function (unknownArgument) {

process.on("rejectionHandled", (promise) => {
errorHandler.warn(
promise,
"A promise rejection was handled asynchronously"
);
errorHandler.warn(promise, "A promise rejection was handled asynchronously");
});

@@ -74,6 +64,8 @@

let elev = new Eleventy(argv.input, argv.output, {
source: "cli",
// --quiet and --quiet=true both resolve to true
quietMode: argv.quiet,
configPath: argv.config,
source: "cli",
pathPrefix: argv.pathprefix,
runMode: argv.serve ? "serve" : argv.watch ? "watch" : "build",
});

@@ -89,6 +81,5 @@

elev.setPathPrefix(argv.pathprefix);
elev.setDryRun(argv.dryrun);
elev.setIgnoreInitial(argv["ignore-initial"]);
elev.setIncrementalBuild(argv.incremental);
elev.setPassthroughAll(argv.passthroughall);
elev.setFormats(argv.formats);

@@ -103,3 +94,3 @@

if (argv.serve) {
let startBrowsersync = true;
let shouldStartServer = true;
elev

@@ -109,7 +100,7 @@ .watch()

// Build failed but error message already displayed.
startBrowsersync = false;
shouldStartServer = false;
// A build error occurred and we aren’t going to --serve
})
.then(function () {
if (startBrowsersync) {
if (shouldStartServer) {
elev.serve(argv.port);

@@ -116,0 +107,0 @@ }

{
"name": "@11ty/eleventy",
"version": "1.0.2",
"version": "2.0.0",
"description": "Transform a directory of templates into HTML.",

@@ -9,2 +9,3 @@ "publishConfig": {

"main": "src/Eleventy.js",
"types": "src/index.d.ts",
"bin": {

@@ -15,3 +16,3 @@ "eleventy": "./cmd.js"

"engines": {
"node": ">=12"
"node": ">=14"
},

@@ -66,5 +67,6 @@ "funding": {

"environmentVariables": {},
"failFast": false,
"failFast": true,
"files": [
"./test/*.js"
"./test/*.js",
"./test/_issues/**/*test.js"
],

@@ -82,24 +84,25 @@ "ignoredByWatcher": [

"devDependencies": {
"@11ty/eleventy-plugin-syntaxhighlight": "^4.1.0",
"@11ty/eleventy-plugin-syntaxhighlight": "^4.2.0",
"@11ty/eleventy-plugin-vue": "1.0.0-canary.8",
"@vue/server-renderer": "^3.2.37",
"ava": "^3.15.0",
"husky": "^8.0.1",
"@vue/server-renderer": "^3.2.47",
"ava": "^5.2.0",
"husky": "^8.0.3",
"js-yaml": "^4.1.0",
"lint-staged": "^13.0.3",
"lint-staged": "^13.1.1",
"markdown-it-emoji": "^2.0.2",
"marked": "^4.0.18",
"marked": "^4.2.12",
"nyc": "^15.1.0",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"sass": "^1.54.4",
"toml": "^3.0.0",
"vue": "^3.2.37"
"prettier": "^2.8.4",
"pretty": "^2.0.0",
"rimraf": "^4.1.2",
"sass": "^1.58.0",
"vue": "^3.2.47"
},
"dependencies": {
"@11ty/dependency-tree": "^2.0.1",
"@11ty/eleventy-dev-server": "^1.0.3",
"@11ty/eleventy-utils": "^1.0.1",
"@iarna/toml": "^2.2.5",
"@sindresorhus/slugify": "^1.1.2",
"browser-sync": "^2.27.10",
"bcp-47-normalize": "^1.1.1",
"chokidar": "^3.5.3",

@@ -110,3 +113,3 @@ "cross-spawn": "^7.0.3",

"ejs": "^3.1.8",
"fast-glob": "^3.2.11",
"fast-glob": "^3.2.12",
"graceful-fs": "^4.2.10",

@@ -117,9 +120,13 @@ "gray-matter": "^4.0.3",

"is-glob": "^4.0.3",
"iso-639-1": "^2.1.15",
"kleur": "^4.1.5",
"liquidjs": "^9.40.0",
"lodash": "^4.17.21",
"luxon": "^2.5.0",
"markdown-it": "^12.3.2",
"minimist": "^1.2.6",
"moo": "^0.5.1",
"liquidjs": "^10.4.0",
"lodash.chunk": "^4.2.0",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"luxon": "^3.2.1",
"markdown-it": "^13.0.1",
"micromatch": "^4.0.5",
"minimist": "^1.2.7",
"moo": "^0.5.2",
"multimatch": "^5.0.0",

@@ -131,8 +138,9 @@ "mustache": "^4.2.0",

"please-upgrade-node": "^3.2.0",
"pretty": "^2.0.0",
"posthtml": "^0.16.6",
"posthtml-urls": "^1.0.0",
"pug": "^3.0.2",
"recursive-copy": "^2.0.14",
"semver": "^7.3.7",
"semver": "^7.3.8",
"slugify": "^1.6.5"
}
}

@@ -1,8 +0,8 @@

<p align="center"><img src="https://www.11ty.dev/img/logo-github.png" alt="eleventy Logo"></p>
<p align="center"><img src="https://www.11ty.dev/img/logo-github.svg" width="200" height="200" alt="eleventy Logo"></p>
# eleventy 🕚⚡️
# eleventy 🕚⚡️🎈🐀
A simpler static site generator. An alternative to Jekyll. Written in JavaScript. Transforms a directory of templates (of varying types) into HTML.
Works with HTML, Markdown, Liquid, Nunjucks, Handlebars, Mustache, EJS, Haml, Pug, and JavaScript Template Literals.
Works with HTML, Markdown, JavaScript, Liquid, Nunjucks, Handlebars, Mustache, EJS, Haml, and Pug.

@@ -9,0 +9,0 @@ ## ➡ [Documentation](https://www.11ty.dev/docs/)

@@ -95,2 +95,3 @@ const ConsoleLogger = require("./Util/ConsoleLogger");

let percent = Math.round((totalForBenchmark * 100) / totalTimeSpent);
let callCount = bench.getTimesCalled();

@@ -100,3 +101,3 @@ let output = {

percent: this.padNumber(percent, 3),
calls: this.padNumber(bench.getTimesCalled(), 5),
calls: this.padNumber(callCount, 5),
};

@@ -113,3 +114,8 @@ let str = `Benchmark ${output.ms}ms ${output.percent}% ${output.calls}× (${label}) ${type}`;

if (totalForBenchmark.toFixed(0) > 0) {
// Opt out of logging if low count (1× or 2×) or 0ms / 1%
if (
totalForBenchmark.toFixed(0) > 1 || // more than 1ms
callCount > 2 || // more than 2×
percent > 1 // more than 1%
) {
debugBenchmark(str);

@@ -116,0 +122,0 @@ }

@@ -1,3 +0,3 @@

const lodashGet = require("lodash/get");
const lodashSet = require("lodash/set");
const lodashGet = require("lodash.get");
const lodashSet = require("lodash.set");

@@ -28,2 +28,3 @@ const ComputedDataQueue = require("./ComputedDataQueue");

let fns = {};
// TODO bug? no access to non-universal config things?
if (this.config) {

@@ -94,5 +95,5 @@ fns = this.config.javascriptFunctions;

debug("Computed data order of execution: %o", order);
for (let key of order) {
let computed = lodashGet(this.computed, key);
if (typeof computed === "function") {

@@ -99,0 +100,0 @@ let ret = await computed(data);

@@ -1,4 +0,4 @@

const lodashSet = require("lodash/set");
const lodashGet = require("lodash/get");
const isPlainObject = require("./Util/IsPlainObject");
const lodashSet = require("lodash.set");
const lodashGet = require("lodash.get");
const { isPlainObject } = require("@11ty/eleventy-utils");

@@ -20,3 +20,7 @@ /* Calculates computed data using Proxies */

getProxyData(data, keyRef) {
// WARNING: SIDE EFFECTS
// Set defaults for keys not already set on parent data
// TODO should make another effort to get rid of this,
// See the ProxyWrap util for more proxy handlers that will likely fix this
let undefinedValue = "__11TY_UNDEFINED__";

@@ -23,0 +27,0 @@ if (this.computedKeys) {

@@ -1,2 +0,2 @@

const lodashSet = require("lodash/set");
const lodashSet = require("lodash.set");
const debug = require("debug")("Eleventy:ComputedDataTemplateString");

@@ -41,3 +41,6 @@

for (let split of splits) {
let varName = split.substr(0, split.indexOf(this.suffix));
let varName = split.slice(
0,
split.indexOf(this.suffix) < 0 ? 0 : split.indexOf(this.suffix)
);
if (varName) {

@@ -44,0 +47,0 @@ vars.add(varName);

@@ -5,6 +5,7 @@ const urlFilter = require("./Filters/Url");

const slugifyFilter = require("./Filters/Slugify");
const getCollectionItem = require("./Filters/GetCollectionItem");
const getLocaleCollectionItem = require("./Filters/GetLocaleCollectionItem");
const getCollectionItemIndex = require("./Filters/GetCollectionItemIndex");
module.exports = function (config) {
let eleventyConfig = this;
let templateConfig = this;

@@ -14,21 +15,35 @@ config.addFilter("slug", slugFilter);

config.addFilter("url", function (url, pathPrefixOverride) {
let pathPrefix =
pathPrefixOverride || eleventyConfig.getConfig().pathPrefix;
// Add pathPrefix manually to a URL
config.addFilter("url", function addPathPrefix(url, pathPrefixOverride) {
let pathPrefix;
if (pathPrefixOverride && typeof pathPrefixOverride === "string") {
pathPrefix = pathPrefixOverride;
} else {
pathPrefix = templateConfig.getPathPrefix();
}
return urlFilter.call(this, url, pathPrefix);
});
config.addFilter("log", console.log);
config.addFilter("log", (input, ...messages) => {
console.log(input, ...messages);
return input;
});
config.addFilter("serverlessUrl", serverlessUrlFilter);
config.addFilter("getCollectionItem", (collection, page) =>
getCollectionItem(collection, page)
);
config.addFilter("getPreviousCollectionItem", (collection, page) =>
getCollectionItem(collection, page, -1)
);
config.addFilter("getNextCollectionItem", (collection, page) =>
getCollectionItem(collection, page, 1)
);
config.addFilter("getCollectionItemIndex", function (collection, pageOverride) {
return getCollectionItemIndex.call(this, collection, pageOverride);
});
config.addFilter("getCollectionItem", function (collection, pageOverride, langCode) {
return getLocaleCollectionItem.call(this, config, collection, pageOverride, langCode, 0);
});
config.addFilter("getPreviousCollectionItem", function (collection, pageOverride, langCode) {
return getLocaleCollectionItem.call(this, config, collection, pageOverride, langCode, -1);
});
config.addFilter("getNextCollectionItem", function (collection, pageOverride, langCode) {
return getLocaleCollectionItem.call(this, config, collection, pageOverride, langCode, 1);
});
return {

@@ -51,5 +66,11 @@ templateFormats: [

htmlTemplateEngine: "liquid",
dataTemplateEngine: false, // change in 1.0
htmlOutputSuffix: "-o",
jsDataFileSuffix: ".11tydata",
// Renamed from `jsDataFileSuffix` in 2.0 (and swapped to an Array)
// If you remove "" we won’t look for dir/dir.json or file.json
dataFileSuffixes: [".11tydata", ""],
// "index" will look for `directory/index.*` directory data files instead of `directory/directory.*`
dataFileDirBaseNameOverride: false,
keys: {

@@ -56,0 +77,0 @@ package: "pkg",

@@ -15,7 +15,8 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

const ConsoleLogger = require("./Util/ConsoleLogger");
const PathPrefixer = require("./Util/PathPrefixer");
const TemplateConfig = require("./TemplateConfig");
const FileSystemSearch = require("./FileSystemSearch");
const templateCache = require("./TemplateCache");
const simplePlural = require("./Util/Pluralize");
const deleteRequireCache = require("./Util/DeleteRequireCache");
const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck");
const debug = require("debug")("Eleventy");

@@ -48,2 +49,13 @@ const eventBus = require("./EventBus");

/**
* @member {String} - The top level directory the site pretends to reside in
* @default "/"
*/
this.pathPrefix = options.pathPrefix || "/";
if (this.pathPrefix || this.pathPrefix === "") {
this.eleventyConfig.setPathPrefix(this.pathPrefix);
}
/* Programmatic API config */
if (options.config && typeof options.config === "function") {

@@ -67,5 +79,22 @@ // TODO use return object here?

/**
* @member {Boolean} - Running in serverless mode
* @default false
*/
if ("isServerless" in options) {
this.isServerless = !!options.isServerless;
} else {
this.isServerless = !!process.env.AWS_LAMBDA_FUNCTION_NAME;
}
/**
* @member {String} - One of build, serve, or watch
* @default "build"
*/
this.runMode = options.runMode || "build";
/**
* @member {Object} - Initialize Eleventy environment variables
* @default null
*/
// both this.isServerless and this.runMode need to be set before this
this.env = this.getEnvironmentVariableValues();

@@ -139,2 +168,3 @@ this.initializeEnvironmentVariables(this.env);

this.eleventyServe.config = this.config;
this.eleventyServe.eleventyConfig = this.eleventyConfig;

@@ -153,4 +183,10 @@ /** @member {String} - Holds the path to the input directory. */

this.watchTargets.addAndMakeGlob(this.config.additionalWatchTargets);
this.watchTargets.watchJavaScriptDependencies =
this.config.watchJavaScriptDependencies;
this.watchTargets.watchJavaScriptDependencies = this.config.watchJavaScriptDependencies;
/** @member {Object} - tbd. */
this.fileSystemSearch = new FileSystemSearch();
this.isIncremental = false;
this.programmaticApiIncrementalFile = undefined;
this.isRunInitialBuild = true;
}

@@ -186,9 +222,3 @@

get outputDir() {
let dir = this.rawOutput || this.config.dir.output;
if (dir !== this._savedOutputDir) {
this.eleventyServe.setOutputDir(dir);
}
this._savedOutputDir = dir;
return dir;
return this.rawOutput || this.config.dir.output;
}

@@ -218,9 +248,10 @@

/**
* Updates the passthrough mode of Eleventy.
* Set whether or not to do an initial build
*
* @method
* @param {Boolean} isPassthroughAll - Shall Eleventy passthrough everything?
* @param {Boolean} ignoreInitialBuild - Shall Eleventy ignore the default initial build before watching in watch/serve mode?
* @default true
*/
setPassthroughAll(isPassthroughAll) {
this.isPassthroughAll = !!isPassthroughAll;
setIgnoreInitial(ignoreInitialBuild) {
this.isRunInitialBuild = !ignoreInitialBuild;
}

@@ -260,12 +291,6 @@

this.start = this.getNewTimestamp();
templateCache.clear();
this.bench.reset();
this.eleventyFiles.restart();
this.extensionMap.reset();
// reload package.json values (if applicable)
// TODO only reset this if it changed
deleteRequireCache(TemplatePath.absolutePath("package.json"));
await this.init();
}

@@ -295,5 +320,3 @@

if (copyCount) {
slashRet.push(
`Copied ${copyCount} ${simplePlural(copyCount, "file", "files")}`
);
slashRet.push(`Copied ${copyCount} ${simplePlural(copyCount, "file", "files")}`);
}

@@ -316,5 +339,3 @@

if (writeCount >= 10) {
ret.push(
`(${((time * 1000) / writeCount).toFixed(1)}ms each, ${versionStr})`
);
ret.push(`(${((time * 1000) / writeCount).toFixed(1)}ms each, ${versionStr})`);
} else {

@@ -324,8 +345,28 @@ ret.push(`(${versionStr})`);

let pathPrefix = this.config.pathPrefix;
if (pathPrefix && pathPrefix !== "/") {
return `Using pathPrefix: ${pathPrefix}\n${ret.join(" ")}`;
return ret.join(" ");
}
_cache(key, inst) {
if (!this._privateCaches) {
this._privateCaches = new Map();
}
return ret.join(" ");
if (!("caches" in inst)) {
throw new Error("To use _cache you need a `caches` getter object");
}
// Restore from cache
if (this._privateCaches.has(key)) {
let c = this._privateCaches.get(key);
for (let cacheKey in c) {
inst[cacheKey] = c[cacheKey];
}
} else {
// Set cache
let c = {};
for (let cacheKey of inst.caches || []) {
c[cacheKey] = inst[cacheKey];
}
this._privateCaches.set(key, c);
}
}

@@ -340,3 +381,5 @@

*/
async init() {
async init(options = {}) {
options = Object.assign({ viaConfigReset: false }, options);
await this.config.events.emit("eleventy.config", this.eleventyConfig);

@@ -354,2 +397,15 @@

// eleventyServe is always available, even when not in --serve mode
this.eleventyServe.setOutputDir(this.outputDir);
// TODO
// this.eleventyServe.setWatcherOptions(this.getChokidarConfig());
this.templateData = new TemplateData(this.inputDir, this.eleventyConfig);
this.templateData.extensionMap = this.extensionMap;
if (this.env) {
this.templateData.environmentVariables = this.env;
}
this.templateData.setFileSystemSearch(this.fileSystemSearch);
this.eleventyFiles = new EleventyFiles(

@@ -361,13 +417,15 @@ this.inputDir,

);
this.eleventyFiles.setPassthroughAll(this.isPassthroughAll);
this.eleventyFiles.setFileSystemSearch(this.fileSystemSearch);
this.eleventyFiles.setInput(this.inputDir, this.input);
this.eleventyFiles.setRunMode(this.runMode);
this.eleventyFiles.extensionMap = this.extensionMap;
// This needs to be set before init or it’ll construct a new one
this.eleventyFiles.templateData = this.templateData;
this.eleventyFiles.init();
this.templateData = new TemplateData(this.inputDir, this.eleventyConfig);
this.templateData.extensionMap = this.extensionMap;
if (this.env) {
this.templateData.environmentVariables = this.env;
if (checkPassthroughCopyBehavior(this.config, this.runMode)) {
this.eleventyServe.watchPassthroughCopy(
this.eleventyFiles.getGlobWatcherFilesForPassthroughCopy()
);
}
this.eleventyFiles.templateData = this.templateData;

@@ -381,2 +439,7 @@ this.writer = new TemplateWriter(

);
if (!options.viaConfigReset) {
this._cache("TemplateWriter", this.writer);
}
this.writer.setInput(this.inputDir, this.input);

@@ -387,2 +450,5 @@ this.writer.logger = this.logger;

this.writer.setRunInitialBuild(this.isRunInitialBuild);
this.writer.setIncrementalBuild(this.isIncremental);
let dirs = {

@@ -411,8 +477,3 @@ input: this.inputDir,

let data = this.templateData.cacheData();
this.needsInit = false;
// …why does it return this
return data;
}

@@ -422,12 +483,20 @@

getEnvironmentVariableValues() {
let values = {
source: this.source,
runMode: this.runMode,
};
let configPath = this.eleventyConfig.getLocalProjectConfigFile();
let absolutePathToConfig = TemplatePath.absolutePath(configPath);
// TODO(zachleat): if config is not in root (e.g. using --config=)
let root = TemplatePath.getDirFromFilePath(absolutePathToConfig);
if (configPath) {
let absolutePathToConfig = TemplatePath.absolutePath(configPath);
values.config = absolutePathToConfig;
return {
config: absolutePathToConfig,
root,
source: this.source,
};
// TODO(zachleat): if config is not in root (e.g. using --config=)
let root = TemplatePath.getDirFromFilePath(absolutePathToConfig);
values.root = root;
}
values.source = this.source;
values.isServerless = this.isServerless;
return values;
}

@@ -444,9 +513,10 @@

process.env.ELEVENTY_SOURCE = this.source;
process.env.ELEVENTY_SOURCE = env.source;
process.env.ELEVENTY_RUN_MODE = env.runMode;
// TODO (@zachleat) this needs to be extensible. https://github.com/11ty/eleventy/issues/1957
// Note: when using --serve, ELEVENTY_SERVERLESS is set manually in Serverless.js
// https://github.com/11ty/eleventy/issues/1957
// Note: when using --serve, ELEVENTY_SERVERLESS is also set in Serverless.js
// Careful here, setting to false will cast to string "false" which is truthy.
if (process.env.AWS_LAMBDA_FUNCTION_NAME) {
if (env.isServerless) {
process.env.ELEVENTY_SERVERLESS = true;

@@ -547,2 +617,29 @@ debug("Setting process.env.ELEVENTY_SERVERLESS: %o", true);

/**
* Updates the run mode of Eleventy.
*
* @method
* @param {String} runMode - One of "build", "watch", or "serve"
*/
setRunMode(runMode) {
this.runMode = runMode;
}
/**
* Set the file that needs to be rendered/compiled/written for an incremental build.
* This method is part of the programmatic API and is not used internally.
*
* @method
* @param {String} incrementalFile - File path (added or modified in a project)
*/
setIncrementalFile(incrementalFile) {
if (incrementalFile) {
// This is used for collections-friendly serverless mode.
this.setIgnoreInitial(true);
this.setIncrementalBuild(true);
this.programmaticApiIncrementalFile = incrementalFile;
}
}
/**
* Reads the version of Eleventy.

@@ -594,2 +691,5 @@ *

--ignore-initial
Start without a build; build when files change. Works best with watch/serve/incremental.
--formats=liquid,md

@@ -637,2 +737,3 @@ Whitelist only certain template types (default: \`*\`)

this.eleventyServe.config = this.config;
this.eleventyServe.eleventyConfig = this.eleventyConfig;

@@ -653,2 +754,3 @@ // only use config quietMode if --quiet not set on CLI

async _addFileToWatchQueue(changedFilePath) {
// Note: this is a sync event!
eventBus.emit("eleventy.resourceModified", changedFilePath);

@@ -658,2 +760,24 @@ this.watchManager.addToPendingQueue(changedFilePath);

_shouldResetConfig() {
let configFilePaths = this.eleventyConfig.getLocalProjectConfigFiles();
let configFilesChanged = this.watchManager.hasQueuedFiles(configFilePaths);
if (configFilesChanged) {
return true;
}
for (const configFilePath of configFilePaths) {
// Any dependencies of the config file changed
let configFileDependencies = this.watchTargets.getDependenciesOf(configFilePath);
for (let dep of configFileDependencies) {
if (this.watchManager.hasQueuedFile(dep)) {
return true;
}
}
}
return false;
}
/**

@@ -676,8 +800,8 @@ * tbd.

// reset and reload global configuration :O
if (
this.watchManager.hasQueuedFile(
this.eleventyConfig.getLocalProjectConfigFile()
)
) {
// Clear `require` cache for all files that triggered the rebuild
this.watchTargets.clearRequireCacheFor(queue);
// reset and reload global configuration
let isResetConfig = this._shouldResetConfig();
if (isResetConfig) {
this.resetConfig();

@@ -687,5 +811,4 @@ }

await this.restart();
await this.init({ viaConfigReset: isResetConfig });
this.watchTargets.clearDependencyRequireCache();
let incrementalFile = this.watchManager.getIncrementalFile();

@@ -696,5 +819,8 @@ if (incrementalFile) {

await this.write();
// let writeResult = await this.write();
// let hasError = !!writeResult.error;
let writeResults = await this.write();
let passthroughCopyResults;
let templateResults;
if (!writeResults.error) {
[passthroughCopyResults, ...templateResults] = writeResults;
}

@@ -717,13 +843,25 @@ this.writer.resetIncrementalFile();

// TODO how to make this work with relative includes?
!TemplatePath.startsWithSubPath(
path,
this.eleventyFiles.getIncludesDir()
)
!TemplatePath.startsWithSubPath(path, this.eleventyFiles.getIncludesDir())
);
});
if (onlyCssChanges) {
this.eleventyServe.reload("*.css");
if (writeResults.error) {
this.eleventyServe.sendError({
error: writeResults.error,
});
} else {
this.eleventyServe.reload();
let normalizedPathPrefix = PathPrefixer.normalizePathPrefix(this.config.pathPrefix);
await this.eleventyServe.reload({
files: this.watchManager.getActiveQueue(),
subtype: onlyCssChanges ? "css" : undefined,
build: {
templates: templateResults
.flat()
.filter((entry) => !!entry)
.map((entry) => {
entry.url = PathPrefixer.joinUrlParts(normalizedPathPrefix, entry.url);
return entry;
}),
},
});
}

@@ -733,5 +871,8 @@

if (this.watchManager.getPendingQueueSize() > 0) {
let queueSize = this.watchManager.getPendingQueueSize();
if (queueSize > 0) {
this.logger.log(
`You saved while Eleventy was running, let’s run again. (${this.watchManager.getPendingQueueSize()} remain)`
`You saved while Eleventy was running, let’s run again. (${queueSize} change${
queueSize !== 1 ? "s" : ""
})`
);

@@ -763,11 +904,11 @@ await this._watch();

this.watchTargets.add(["./package.json"]);
this.watchTargets.add(this.eleventyFiles.getGlobWatcherFiles());
this.watchTargets.add(this.eleventyFiles.getIgnoreFiles());
// Watch the local project config file
this.watchTargets.add(this.eleventyConfig.getLocalProjectConfigFile());
this.watchTargets.add(this.eleventyConfig.getLocalProjectConfigFiles());
// Template and Directory Data Files
this.watchTargets.add(
await this.eleventyFiles.getGlobWatcherTemplateDataFiles()
);
this.watchTargets.add(await this.eleventyFiles.getGlobWatcherTemplateDataFiles());

@@ -794,5 +935,5 @@ let benchmark = this.watcherBench.get(

let dataDir = this.templateData.getDataDir();
let dataDir = TemplatePath.stripLeadingDotSlash(this.templateData.getDataDir());
function filterOutGlobalDataFiles(path) {
return !dataDir || path.indexOf(dataDir) === -1;
return !dataDir || !TemplatePath.stripLeadingDotSlash(path).startsWith(dataDir);
}

@@ -806,3 +947,3 @@

this.watchTargets.addDependencies(
this.eleventyConfig.getLocalProjectConfigFile(),
this.eleventyConfig.getLocalProjectConfigFiles(),
filterOutGlobalDataFiles

@@ -812,6 +953,4 @@ );

// Deps from Global Data (that aren’t in the global data directory, everything is watched there)
this.watchTargets.addDependencies(
this.templateData.getWatchPathCache(),
filterOutGlobalDataFiles
);
let globalDataDeps = this.templateData.getWatchPathCache();
this.watchTargets.addDependencies(globalDataDeps, filterOutGlobalDataFiles);

@@ -858,3 +997,3 @@ this.watchTargets.addDependencies(

/**
* Start the watching of files.
* Start the watching of files
*

@@ -907,2 +1046,3 @@ * @async

let watchRun = async (path) => {
path = TemplatePath.normalize(path);
try {

@@ -935,5 +1075,11 @@ this._addFileToWatchQueue(path);

this.logger.forceLog(`File added: ${path}`);
this.fileSystemSearch.add(path);
await watchRun(path);
});
watcher.on("unlink", (path) => {
// this.logger.forceLog(`File removed: ${path}`);
this.fileSystemSearch.delete(path);
});
process.on("SIGINT", () => this.stopWatch());

@@ -943,5 +1089,6 @@ }

stopWatch() {
debug("Cleaning up chokidar and browsersync (if exists) instances.");
debug("Cleaning up chokidar and server instances, if they exist.");
this.eleventyServe.close();
this.watcher.close();
process.exit();

@@ -955,4 +1102,6 @@ }

*/
serve(port) {
this.eleventyServe.serve(port);
async serve(port) {
// Port is optional and in this case likely via --port on the command line
// May defer to configuration API options `port` property
return this.eleventyServe.serve(port);
}

@@ -1018,2 +1167,6 @@

if (this.programmaticApiIncrementalFile) {
this.writer.setIncrementalFile(this.programmaticApiIncrementalFile);
}
let ret;

@@ -1025,2 +1178,6 @@ let hasError = false;

inputDir: this.config.inputDir,
dir: this.config.dir,
runMode: this.runMode,
outputMode: to,
incremental: this.isIncremental,
};

@@ -1045,2 +1202,10 @@ await this.config.events.emit("beforeBuild", eventsArg);

// Passing the processed output to the eleventy.after event is new in 2.0
let [passthroughCopyResults, ...templateResults] = ret;
if (to === "fs") {
eventsArg.results = templateResults.flat().filter((entry) => !!entry);
} else {
eventsArg.results = templateResults.filter((entry) => !!entry);
}
if (to === "ndjson") {

@@ -1059,12 +1224,14 @@ // return a stream

};
this.errorHandler.fatal(e, "Problem writing Eleventy templates");
// Issue #2405
if (this.source === "script") {
this.errorHandler.error(e, "Problem writing Eleventy templates");
throw e;
} else {
this.errorHandler.fatal(e, "Problem writing Eleventy templates");
}
} finally {
this.bench.finish();
if (to === "fs") {
this.logger.message(
this.logFinished(),
"info",
hasError ? "red" : "green",
true
);
this.logger.message(this.logFinished(), "info", hasError ? "red" : "green", true);
}

@@ -1086,1 +1253,4 @@ debug("Finished writing templates.");

module.exports.EleventyRenderPlugin = require("./Plugins/RenderPlugin");
module.exports.EleventyEdgePlugin = require("./Plugins/EdgePlugin");
module.exports.EleventyI18nPlugin = require("./Plugins/I18nPlugin");
module.exports.EleventyHtmlBasePlugin = require("./Plugins/HtmlBasePlugin");

@@ -31,3 +31,8 @@ const TemplateContentPrematureUseError = require("./Errors/TemplateContentPrematureUseError");

return msg.substr(0, msg.indexOf(EleventyErrorUtil.prefix));
return msg.slice(
0,
msg.indexOf(EleventyErrorUtil.prefix) < 0
? 0
: msg.indexOf(EleventyErrorUtil.prefix)
);
}

@@ -71,3 +76,3 @@

TemplateContentPrematureUseError) || // Liquid
e.message.indexOf("TemplateContentPrematureUseError") > -1
(e.message || "").indexOf("TemplateContentPrematureUseError") > -1
); // Nunjucks

@@ -74,0 +79,0 @@ }

@@ -35,5 +35,3 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

this.passthroughCopyKeys = this.unfilteredFormatKeys.filter(
(key) => !this.hasExtension(key)
);
this.passthroughCopyKeys = this.unfilteredFormatKeys.filter((key) => !this.hasExtension(key));
}

@@ -156,12 +154,20 @@

let dir = TemplatePath.convertToRecursiveGlobSync(inputDir);
let globs = [];
let extensions = [];
for (let key of formatKeys) {
if (this.hasExtension(key)) {
for (let extension of this.getExtensionsFromKey(key)) {
globs.push(dir + "/*." + extension);
extensions.push(extension);
}
} else {
globs.push(dir + "/*." + key);
extensions.push(key);
}
}
let globs = [];
if (extensions.length === 1) {
globs.push(`${dir}/*.${extensions[0]}`);
} else if (extensions.length > 1) {
globs.push(`${dir}/*.{${extensions.join(",")}}`);
}
return globs;

@@ -222,3 +228,6 @@ }

if (path === extension || path.endsWith("." + extension)) {
return path.substr(0, path.length - 1 - extension.length);
return path.slice(
0,
path.length - 1 - extension.length < 0 ? 0 : path.length - 1 - extension.length
);
}

@@ -225,0 +234,0 @@ }

const fs = require("fs");
const fastglob = require("fast-glob");
const { TemplatePath } = require("@11ty/eleventy-utils");

@@ -10,2 +10,3 @@

const EleventyBaseError = require("./EleventyBaseError");
const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck");

@@ -33,4 +34,2 @@ class EleventyFilesError extends EleventyBaseError {}

this.passthroughAll = false;
this.formats = formats;

@@ -43,2 +42,6 @@ this.eleventyIgnoreContent = false;

setFileSystemSearch(fileSystemSearch) {
this.fileSystemSearch = fileSystemSearch;
}
/* Overrides this.input and this.inputDir,

@@ -58,12 +61,6 @@ * Useful when input is a file and inputDir is not its direct parent */

initConfig() {
this.includesDir = TemplatePath.join(
this.inputDir,
this.config.dir.includes
);
this.includesDir = TemplatePath.join(this.inputDir, this.config.dir.includes);
if ("layouts" in this.config.dir) {
this.layoutsDir = TemplatePath.join(
this.inputDir,
this.config.dir.layouts
);
this.layoutsDir = TemplatePath.join(this.inputDir, this.config.dir.layouts);
}

@@ -124,3 +121,3 @@ }

config.ignores = new Set();
config.ignores.add("node_modules/**");
config.ignores.add("**/node_modules/**");
}

@@ -144,6 +141,3 @@ this.config = config;

if (!this._extensionMap) {
this._extensionMap = new EleventyExtensionMap(
this.formats,
this.eleventyConfig
);
this._extensionMap = new EleventyExtensionMap(this.formats, this.eleventyConfig);
this._extensionMap.config = this.config;

@@ -154,4 +148,4 @@ }

setPassthroughAll(passthroughAll) {
this.passthroughAll = !!passthroughAll;
setRunMode(runMode) {
this.runMode = runMode;
}

@@ -163,3 +157,5 @@

mgr.setOutputDir(this.outputDir);
mgr.setRunMode(this.runMode);
mgr.extensionMap = this.extensionMap;
mgr.setFileSystemSearch(this.fileSystemSearch);
this.passthroughManager = mgr;

@@ -204,9 +200,3 @@ }

if (this.passthroughAll) {
this.normalizedTemplateGlobs = TemplateGlob.map([
TemplateGlob.normalizePath(this.input, "/**"),
]);
} else {
this.normalizedTemplateGlobs = this.templateGlobs;
}
this.normalizedTemplateGlobs = this.templateGlobs;
}

@@ -222,2 +212,6 @@

}
// Placing the config ignores last here is important to the tests
for (let ignore of this.config.ignores) {
uniqueIgnores.add(TemplateGlob.normalizePath(this.localPathRoot || ".", ignore));
}
return Array.from(uniqueIgnores);

@@ -240,5 +234,3 @@ }

ignores = ignores.concat(
EleventyFiles.normalizeIgnoreContent(dir, ignoreContent)
);
ignores = ignores.concat(EleventyFiles.normalizeIgnoreContent(dir, ignoreContent));
}

@@ -267,17 +259,11 @@ }

debug(">>>", line);
debug(
"Follow along at https://github.com/11ty/eleventy/issues/693 to track support."
);
debug("Follow along at https://github.com/11ty/eleventy/issues/693 to track support.");
}
// empty lines or comments get filtered out
return (
line.length > 0 && line.charAt(0) !== "#" && line.charAt(0) !== "!"
);
return line.length > 0 && line.charAt(0) !== "#" && line.charAt(0) !== "!";
})
.map((line) => {
let path = TemplateGlob.normalizePath(dir, "/", line);
path = TemplatePath.addLeadingDotSlash(
TemplatePath.relativePath(path)
);
path = TemplatePath.addLeadingDotSlash(TemplatePath.relativePath(path));

@@ -305,39 +291,38 @@ try {

getIgnores() {
let rootDirectory = this.localPathRoot || ".";
let files = [];
let files = new Set();
for (let ignore of this.config.ignores) {
files = files.concat(TemplateGlob.normalizePath(rootDirectory, ignore));
for (let ignore of EleventyFiles.getFileIgnores(this.getIgnoreFiles())) {
files.add(ignore);
}
// testing API
if (this.eleventyIgnoreContent !== false) {
files.add(this.eleventyIgnoreContent);
}
// ignore output dir unless that would exclude all input
if (!TemplatePath.startsWithSubPath(this.inputDir, this.outputDir)) {
files.add(TemplateGlob.map(this.outputDir + "/**"));
}
return Array.from(files);
}
getIgnoreFiles() {
let ignoreFiles = [];
let rootDirectory = this.localPathRoot || ".";
if (this.config.useGitIgnore) {
files = files.concat(
EleventyFiles.getFileIgnores([
TemplatePath.join(rootDirectory, ".gitignore"),
])
);
ignoreFiles.push(TemplatePath.join(rootDirectory, ".gitignore"));
}
if (this.eleventyIgnoreContent !== false) {
files = files.concat(this.eleventyIgnoreContent);
} else {
if (this.eleventyIgnoreContent === false) {
let absoluteInputDir = TemplatePath.absolutePath(this.inputDir);
let eleventyIgnoreFiles = [
TemplatePath.join(rootDirectory, ".eleventyignore"),
];
ignoreFiles.push(TemplatePath.join(rootDirectory, ".eleventyignore"));
if (rootDirectory !== absoluteInputDir) {
eleventyIgnoreFiles.push(
TemplatePath.join(this.inputDir, ".eleventyignore")
);
ignoreFiles.push(TemplatePath.join(this.inputDir, ".eleventyignore"));
}
files = files.concat(EleventyFiles.getFileIgnores(eleventyIgnoreFiles));
}
// ignore output dir unless that would exclude all input
if (!TemplatePath.startsWithSubPath(this.inputDir, this.outputDir)) {
files = files.concat(TemplateGlob.map(this.outputDir + "/**"));
}
return files;
return ignoreFiles;
}

@@ -364,5 +349,3 @@

if (!this.pathCache) {
throw new Error(
"Watching requires `.getFiles()` to be called first in EleventyFiles"
);
throw new Error("Watching requires `.getFiles()` to be called first in EleventyFiles");
}

@@ -380,6 +363,2 @@

_globSearch() {
if (this._glob) {
return this._glob;
}
let globs = this.getFileGlobs();

@@ -389,14 +368,9 @@

debug("Searching for: %o", globs);
this._glob = fastglob(globs, {
caseSensitiveMatch: false,
dot: true,
return this.fileSystemSearch.search("templates", globs, {
ignore: this.uniqueIgnores,
});
return this._glob;
}
async getFiles() {
let bench = this.aggregateBench.get("Searching the file system");
let bench = this.aggregateBench.get("Searching the file system (templates)");
bench.before();

@@ -407,17 +381,3 @@ let globResults = await this._globSearch();

// filter individual paths in the new config-specified extension
// where is this used?
if ("extensionMap" in this.config) {
let extensions = this.config.extensionMap;
if (Array.from(extensions).filter((entry) => !!entry.filter).length) {
paths = paths.filter(function (path) {
for (let entry of extensions) {
if (entry.filter && path.endsWith(`.${entry.extension}`)) {
return entry.filter(path);
}
}
return true;
});
}
}
// Note 2.0.0-canary.19 removed a `filter` option for custom template syntax here that was unpublished and unused.

@@ -430,2 +390,6 @@ this.pathCache = paths;

isFullTemplateFile(paths, filePath) {
if (!filePath) {
return false;
}
for (let path of paths) {

@@ -442,9 +406,19 @@ if (path === filePath) {

getGlobWatcherFiles() {
// TODO is it better to tie the includes and data to specific file extensions or keep the **?
return this.validTemplateGlobs
.concat(this.passthroughGlobs)
.concat(this._getIncludesAndDataDirs());
// TODO improvement: tie the includes and data to specific file extensions (currently using `**`)
let directoryGlobs = this._getIncludesAndDataDirs();
if (checkPassthroughCopyBehavior(this.config, this.runMode)) {
return this.validTemplateGlobs.concat(directoryGlobs);
}
// Revert to old passthroughcopy copy files behavior
return this.validTemplateGlobs.concat(this.passthroughGlobs).concat(directoryGlobs);
}
/* For `eleventy --watch` */
getGlobWatcherFilesForPassthroughCopy() {
return this.passthroughGlobs;
}
/* For `eleventy --watch` */
async getGlobWatcherTemplateDataFiles() {

@@ -459,9 +433,7 @@ let templateData = this.templateData;

let globs = await this.templateData.getTemplateJavaScriptDataFileGlob();
let bench = this.aggregateBench.get("Searching the file system");
let bench = this.aggregateBench.get("Searching the file system (watching)");
bench.before();
let results = TemplatePath.addLeadingDotSlashArray(
await fastglob(globs, {
await this.fileSystemSearch.search("js-dependencies", globs, {
ignore: ["**/node_modules/**"],
caseSensitiveMatch: false,
dot: true,
})

@@ -476,5 +448,12 @@ );

// convert to format without ! since they are passed in as a separate argument to glob watcher
return this.fileIgnores.map((ignore) =>
TemplatePath.stripLeadingDotSlash(ignore)
let entries = new Set(
this.fileIgnores.map((ignore) => TemplatePath.stripLeadingDotSlash(ignore))
);
for (let ignore of this.config.watchIgnores) {
entries.add(TemplateGlob.normalizePath(this.localPathRoot || ".", ignore));
}
// de-duplicated
return Array.from(entries);
}

@@ -481,0 +460,0 @@

@@ -1,17 +0,32 @@

const fs = require("fs");
const path = require("path");
const { TemplatePath } = require("@11ty/eleventy-utils");
const { TemplatePath } = require("@11ty/eleventy-utils");
const EleventyBaseError = require("./EleventyBaseError");
const ConsoleLogger = require("./Util/ConsoleLogger");
const PathPrefixer = require("./Util/PathPrefixer");
const merge = require("./Util/Merge");
const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck");
const debug = require("debug")("EleventyServe");
class EleventyServeConfigError extends EleventyBaseError {}
const DEFAULT_SERVER_OPTIONS = {
module: "@11ty/eleventy-dev-server",
port: 8080,
// pathPrefix: "/",
// setup: function() {},
// logger: { info: function() {}, error: function() {} }
};
class EleventyServe {
constructor() {}
constructor() {
this.logger = new ConsoleLogger(true);
this._initOptionsFetched = false;
this._aliases = undefined;
this._watchedFiles = new Set();
}
get config() {
if (!this._config) {
throw new EleventyServeConfigError(
"You need to set the config property on EleventyServe."
);
throw new EleventyServeConfigError("You need to set the config property on EleventyServe.");
}

@@ -23,176 +38,220 @@

set config(config) {
this._options = null;
this._config = config;
}
setOutputDir(outputDir) {
this.outputDir = outputDir;
setAliases(aliases) {
this._aliases = aliases;
if (this._server && "setAliases" in this._server) {
this._server.setAliases(aliases);
}
}
getPathPrefix() {
let cfgPrefix = this.config.pathPrefix;
if (cfgPrefix) {
// add leading / (for browsersync), see #1454
// path.join uses \\ for Windows so we split and rejoin
return path.join("/", cfgPrefix).split(path.sep).join("/");
get eleventyConfig() {
if (!this._eleventyConfig) {
throw new EleventyServeConfigError(
"You need to set the eleventyConfig property on EleventyServe."
);
}
return "/";
return this._eleventyConfig;
}
getRedirectDir(dirName) {
return TemplatePath.join(this.outputDir, dirName);
}
getRedirectDirOverride() {
// has a pathPrefix, add a /index.html template to redirect to /pathPrefix/
if (this.getPathPrefix() !== "/") {
return "_eleventy_redirect";
set eleventyConfig(config) {
this._eleventyConfig = config;
if (checkPassthroughCopyBehavior(this._eleventyConfig.userConfig, "serve")) {
this._eleventyConfig.userConfig.events.on("eleventy.passthrough", ({ map }) => {
// for-free passthrough copy
this.setAliases(map);
});
}
}
getRedirectFilename(dirName) {
return TemplatePath.join(this.getRedirectDir(dirName), "index.html");
async setOutputDir(outputDir) {
// TODO check if this is different and if so, restart server (if already running)
// This applies if you change the output directory in your config file during watch/serve
this.outputDir = outputDir;
}
getOptions(port) {
let pathPrefix = this.getPathPrefix();
getServerModule(name) {
try {
if (!name || name === DEFAULT_SERVER_OPTIONS.module) {
return require(DEFAULT_SERVER_OPTIONS.module);
}
// TODO customize this in Configuration API?
let serverConfig = {
baseDir: this.outputDir,
};
// Look for peer dep in local project
let projectNodeModulesPath = TemplatePath.absolutePath("./node_modules/");
let serverPath = TemplatePath.absolutePath(projectNodeModulesPath, name);
let redirectDirName = this.getRedirectDirOverride();
// has a pathPrefix, add a /index.html template to redirect to /pathPrefix/
if (redirectDirName) {
serverConfig.baseDir = this.getRedirectDir(redirectDirName);
serverConfig.routes = {};
serverConfig.routes[pathPrefix] = this.outputDir;
// No references outside of the project node_modules are allowed
if (!serverPath.startsWith(projectNodeModulesPath)) {
throw new Error("Invalid node_modules name for Eleventy server instance, received:" + name);
}
// if has a savedPathPrefix, use the /savedPathPrefix/index.html template to redirect to /pathPrefix/
if (this.savedPathPrefix) {
serverConfig.routes[this.savedPathPrefix] = TemplatePath.join(
this.outputDir,
this.savedPathPrefix
let module = require(serverPath);
if (!("getServer" in module)) {
throw new Error(
`Eleventy server module requires a \`getServer\` static method. Could not find one on module: \`${name}\``
);
}
let serverPackageJsonPath = TemplatePath.absolutePath(serverPath, "package.json");
let serverPackageJson = require(serverPackageJsonPath);
if (serverPackageJson["11ty"] && serverPackageJson["11ty"].compatibility) {
try {
this.eleventyConfig.userConfig.versionCheck(serverPackageJson["11ty"].compatibility);
} catch (e) {
this.logger.warn(`Warning: \`${name}\` Plugin Compatibility: ${e.message}`);
}
}
return module;
} catch (e) {
this.logger.error(
"There was an error with your custom Eleventy server. We’re using the default server instead.\n" +
e.message
);
debug("Eleventy server error %o", e);
return require(DEFAULT_SERVER_OPTIONS.module);
}
}
return Object.assign(
get options() {
if (this._options) {
return this._options;
}
this._options = Object.assign(
{
server: serverConfig,
port: port || 8080,
ignore: ["node_modules"],
watch: false,
open: false,
notify: false,
ui: false, // Default changed in 1.0
ghostMode: false, // Default changed in 1.0
index: "index.html",
pathPrefix: PathPrefixer.normalizePathPrefix(this.config.pathPrefix),
logger: this.logger,
},
this.config.browserSyncConfig
DEFAULT_SERVER_OPTIONS,
this.config.serverOptions
);
// TODO improve by sorting keys here
this._savedConfigOptions = JSON.stringify(this.config.serverOptions);
if (!this._initOptionsFetched && this.getSetupCallback()) {
throw new Error(
"Init options have not yet been fetched in the setup callback. This probably means that `init()` has not yet been called."
);
}
return this._options;
}
cleanupRedirect(dirName) {
if (dirName && dirName !== "/") {
let savedPathFilename = this.getRedirectFilename(dirName);
get server() {
if (this._server) {
return this._server;
}
setTimeout(function () {
if (!fs.existsSync(savedPathFilename)) {
debug(`Cleanup redirect: Could not find ${savedPathFilename}`);
return;
}
let serverModule = this.getServerModule(this.options.module);
let savedPathContent = fs.readFileSync(savedPathFilename, "utf8");
if (
savedPathContent.indexOf("Browsersync pathPrefix Redirect") === -1
) {
debug(
`Cleanup redirect: Found ${savedPathFilename} but it wasn’t an eleventy redirect.`
);
return;
}
// Static method `getServer` was already checked in `getServerModule`
this._server = serverModule.getServer("eleventy-server", this.outputDir, this.options);
fs.unlink(savedPathFilename, (err) => {
if (!err) {
debug(`Cleanup redirect: Deleted ${savedPathFilename}`);
}
});
}, 2000);
}
this.setAliases(this._aliases);
return this._server;
}
serveRedirect(dirName) {
fs.mkdirSync(this.getRedirectDir(dirName), {
recursive: true,
});
fs.writeFileSync(
this.getRedirectFilename(dirName),
`<!doctype html>
<meta http-equiv="refresh" content="0; url=${this.config.pathPrefix}">
<title>Browsersync pathPrefix Redirect</title>
<a href="${this.config.pathPrefix}">Go to ${this.config.pathPrefix}</a>`
);
set server(val) {
this._server = val;
}
serve(port) {
// Only load on serve—this is pretty expensive
// We use a string module name and try/catch here to hide this from the zisi and esbuild serverless bundlers
let server;
// eslint-disable-next-line no-useless-catch
try {
let moduleName = "browser-sync";
server = require(moduleName);
} catch (e) {
throw e;
getSetupCallback() {
let setupCallback = this.config.serverOptions.setup;
if (setupCallback && typeof setupCallback === "function") {
return setupCallback;
}
}
this.server = server.create("eleventy-server");
async init() {
if (!this._initPromise) {
this._initPromise = new Promise(async (resolve) => {
let setupCallback = this.getSetupCallback();
if (setupCallback) {
let opts = await setupCallback();
this._initOptionsFetched = true;
let pathPrefix = this.getPathPrefix();
if (opts) {
merge(this.options, opts);
}
}
if (this.savedPathPrefix && pathPrefix !== this.savedPathPrefix) {
let redirectFilename = this.getRedirectFilename(this.savedPathPrefix);
if (!fs.existsSync(redirectFilename)) {
debug(
`Redirecting BrowserSync from ${this.savedPathPrefix} to ${pathPrefix}`
);
this.serveRedirect(this.savedPathPrefix);
} else {
debug(
`Config updated with a new pathPrefix. Tried to set up a transparent redirect but found a template already existing at ${redirectFilename}. You’ll have to navigate manually.`
);
}
resolve();
});
}
let redirectDirName = this.getRedirectDirOverride();
// has a pathPrefix, add a /index.html template to redirect to /pathPrefix/
if (redirectDirName) {
this.serveRedirect(redirectDirName);
return this._initPromise;
}
// Port comes in here from --port on the command line
async serve(port) {
this._commandLinePort = port;
await this.init();
this.server.serve(port || this.options.port);
}
async close() {
if (this._server) {
await this.server.close();
this.server = undefined;
}
}
this.cleanupRedirect(this.savedPathPrefix);
async sendError({ error }) {
if (this._server) {
await this.server.sendError({
error,
});
}
}
let options = this.getOptions(port);
this.server.init(options);
// Restart the server entirely
// We don’t want to use a native `restart` method (e.g. restart() in Vite) so that
// we can correctly handle a `module` property change (changing the server type)
async restart() {
await this.close();
// this needs to happen after `.getOptions`
this.savedPathPrefix = pathPrefix;
// saved --port in `serve()`
await this.serve(this._commandLinePort);
// rewatch the saved watched files (passthrough copy)
if ("watchFiles" in this.server) {
this.server.watchFiles(this._watchedFiles);
}
}
close() {
if (this.server) {
this.server.exit();
// checkPassthroughCopyBehavior check is called upstream in Eleventy.js
// TODO globs are not removed from watcher
watchPassthroughCopy(globs) {
this._watchedFiles = globs;
if ("watchFiles" in this.server) {
this.server.watchFiles(globs);
}
}
/* filesToReload is optional */
reload(filesToReload) {
if (this.server) {
if (this.getPathPrefix() !== this.savedPathPrefix) {
this.server.exit();
this.serve();
} else {
this.server.reload(filesToReload);
}
// Live reload the server
async reload(reloadEvent = {}) {
if (!this._server) {
return;
}
// Restart the server if the options have changed
if (JSON.stringify(this.config.serverOptions) !== this._savedConfigOptions) {
debug("Server options changed, we’re restarting the server");
await this.restart();
} else {
await this.server.reload(reloadEvent);
}
}

@@ -199,0 +258,0 @@ }

const { TemplatePath } = require("@11ty/eleventy-utils");
const PathNormalizer = require("./Util/PathNormalizer.js");

@@ -32,7 +33,7 @@ /* Decides when to watch and in what mode to watch

getIncrementalFile() {
if (!this.isActive || !this.incremental || this.activeQueue.length === 0) {
return false;
if (this.incremental) {
return this.activeQueue.length ? this.activeQueue[0] : false;
}
return this.activeQueue[0];
return false;
}

@@ -66,4 +67,3 @@

return (
this.activeQueue.length > 0 &&
this.activeQueue.length === this._queueMatches(file).length
this.activeQueue.length > 0 && this.activeQueue.length === this._queueMatches(file).length
);

@@ -73,5 +73,17 @@ }

hasQueuedFile(file) {
return this._queueMatches(file).length > 0;
if (file) {
return this._queueMatches(file).length > 0;
}
return false;
}
hasQueuedFiles(files) {
for (const file of files) {
if (this.hasQueuedFile(file)) {
return true;
}
}
return false;
}
get pendingQueue() {

@@ -90,3 +102,3 @@ if (!this._queue) {

if (path) {
path = TemplatePath.addLeadingDotSlash(path);
path = PathNormalizer.normalizeSeperator(TemplatePath.addLeadingDotSlash(path));
this.pendingQueue.push(path);

@@ -93,0 +105,0 @@ }

@@ -1,5 +0,6 @@

const dependencyTree = require("@11ty/dependency-tree");
const { TemplatePath } = require("@11ty/eleventy-utils");
const { DepGraph } = require("dependency-graph");
const deleteRequireCache = require("./Util/DeleteRequireCache");
const JavaScriptDependencies = require("./Util/JavaScriptDependencies");

@@ -12,2 +13,4 @@ class EleventyWatchTargets {

this._watchJavaScriptDependencies = true;
this.graph = new DepGraph();
}

@@ -27,12 +30,2 @@

_normalizeTargets(targets) {
if (!targets) {
return [];
} else if (Array.isArray(targets)) {
return targets;
}
return [targets];
}
reset() {

@@ -46,2 +39,25 @@ this.newTargets = new Set();

addToDependencyGraph(parent, deps) {
if (!this.graph.hasNode(parent)) {
this.graph.addNode(parent);
}
for (let dep of deps) {
if (!this.graph.hasNode(dep)) {
this.graph.addNode(dep);
}
this.graph.addDependency(parent, dep);
}
}
uses(parent, dep) {
return this.getDependenciesOf(parent).includes(dep);
}
getDependenciesOf(parent) {
if (!this.graph.hasNode(parent)) {
return [];
}
return this.graph.dependenciesOf(parent);
}
addRaw(targets, isDependency) {

@@ -62,15 +78,27 @@ for (let target of targets) {

static normalize(targets) {
if (!targets) {
return [];
} else if (Array.isArray(targets)) {
return targets;
}
return [targets];
}
// add only a target
add(targets) {
targets = this._normalizeTargets(targets);
this.addRaw(targets);
this.addRaw(EleventyWatchTargets.normalize(targets));
}
addAndMakeGlob(targets) {
targets = this._normalizeTargets(targets).map((entry) =>
static normalizeToGlobs(targets) {
return EleventyWatchTargets.normalize(targets).map((entry) =>
TemplatePath.convertToRecursiveGlobSync(entry)
);
this.addRaw(targets);
}
addAndMakeGlob(targets) {
this.addRaw(EleventyWatchTargets.normalizeToGlobs(targets));
}
// add only a target’s dependencies

@@ -82,4 +110,4 @@ addDependencies(targets, filterCallback) {

targets = this._normalizeTargets(targets);
let deps = this.getJavaScriptDependenciesFromList(targets);
targets = EleventyWatchTargets.normalize(targets);
let deps = JavaScriptDependencies.getDependencies(targets);
if (filterCallback) {

@@ -89,2 +117,5 @@ deps = deps.filter(filterCallback);

for (let target of targets) {
this.addToDependencyGraph(target, deps);
}
this.addRaw(deps, true);

@@ -97,24 +128,12 @@ }

getJavaScriptDependenciesFromList(files = []) {
let depSet = new Set();
files
.filter((file) => file.endsWith(".js") || file.endsWith(".cjs")) // TODO does this need to work with aliasing? what other JS extensions will have deps?
.forEach((file) => {
dependencyTree(file, { allowNotFound: true })
.map((dependency) => {
return TemplatePath.addLeadingDotSlash(
TemplatePath.relativePath(dependency)
);
})
.forEach((dependency) => {
depSet.add(dependency);
});
});
clearRequireCacheFor(filePathArray) {
for (const filePath of filePathArray) {
deleteRequireCache(filePath);
return Array.from(depSet);
}
clearDependencyRequireCache() {
for (let path of this.dependencies) {
deleteRequireCache(TemplatePath.absolutePath(path));
// Any dependencies of the config file changed
let fileDeps = this.getDependenciesOf(filePath);
for (let dep of fileDeps) {
// Delete from require cache so that updates to the module are re-required
deleteRequireCache(dep);
}
}

@@ -121,0 +140,0 @@ }

const TemplateEngine = require("./TemplateEngine");
const getJavaScriptData = require("../Util/GetJavaScriptData");
const eventBus = require("../EventBus.js");
let lastModifiedFile = undefined;
eventBus.on("eleventy.resourceModified", (path) => {
lastModifiedFile = path;
});
class CustomEngine extends TemplateEngine {

@@ -24,2 +30,3 @@ constructor(name, dirs, config) {

if ("extensionMap" in this.config) {
// Iterates over only the user config `addExtension` entries
for (let entry of this.config.extensionMap) {

@@ -41,2 +48,5 @@ if (entry.key.toLowerCase() === this.name.toLowerCase()) {

/**
* @override
*/
needsToReadFileContents() {

@@ -46,2 +56,13 @@ if ("read" in this.entry) {

}
// Handle aliases to `11ty.js` templates, avoid reading files in the alias, see #2279
// Here, we are short circuiting fallback to defaultRenderer, does not account for compile
// functions that call defaultRenderer explicitly
if (
this._defaultEngine &&
"needsToReadFileContents" in this._defaultEngine
) {
return this._defaultEngine.needsToReadFileContents();
}
return true;

@@ -74,6 +95,18 @@ }

async getExtraDataFromFile(inputPath) {
if (!("getData" in this.entry) || this.entry.getData === false) {
if (this.entry.getData === false) {
return;
}
if (!("getData" in this.entry)) {
// Handle aliases to `11ty.js` templates, use upstream default engine data fetch, see #2279
if (
this._defaultEngine &&
"getExtraDataFromFile" in this._defaultEngine
) {
return this._defaultEngine.getExtraDataFromFile(inputPath);
}
return;
}
await this._runningInit();

@@ -148,3 +181,2 @@

await this._runningInit();
let defaultRenderer;

@@ -170,2 +202,5 @@ if (this._defaultEngine) {

config: this.config,
addDependencies: (from, toArray = []) => {
this.config.uses.addDependency(from, toArray);
},
defaultRenderer, // bind defaultRenderer to compile function

@@ -179,3 +214,8 @@ })(str, inputPath);

// Promise, wait to bind
return fn.then((fn) => fn.bind({ defaultRenderer }));
return fn.then((fn) => {
if (typeof fn === "function") {
return fn.bind({ defaultRenderer });
}
return fn;
});
} else if ("bind" in fn && typeof fn.bind === "function") {

@@ -193,3 +233,22 @@ return fn.bind({ defaultRenderer });

hasDependencies(inputPath) {
if (this.config.uses.getDependencies(inputPath) === false) {
return false;
}
return true;
}
isFileRelevantTo(inputPath, comparisonFile, includeLayouts) {
return this.config.uses.isFileRelevantTo(
inputPath,
comparisonFile,
includeLayouts
);
}
getCompileCacheKey(str, inputPath) {
// Return this separately so we know whether or not to use the cached version
// but still return a key to cache this new render for next time
let useCache = !this.isFileRelevantTo(inputPath, lastModifiedFile, false);
if (

@@ -205,5 +264,13 @@ this.entry.compileOptions &&

return this.entry.compileOptions.getCacheKey(str, inputPath);
return {
useCache,
key: this.entry.compileOptions.getCacheKey(str, inputPath),
};
}
return super.getCompileCacheKey(str, inputPath);
let { key } = super.getCompileCacheKey(str, inputPath);
return {
useCache,
key,
};
}

@@ -210,0 +277,0 @@

@@ -15,7 +15,2 @@ const HandlebarsLib = require("handlebars");

let partials = super.getPartials();
for (let name in partials) {
this.handlebarsLib.registerPartial(name, partials[name]);
}
// TODO these all go to the same place (addHelper), add warnings for overwrites

@@ -33,2 +28,3 @@ this.addHelpers(this.config.handlebarsHelpers);

for (let name in helpers) {
// We don’t need to wrap helpers for `page` or `eleventy`, this is provided for free by Handlebars
this.addHelper(name, helpers[name]);

@@ -59,3 +55,15 @@ }

/**
* @override
*/
async cachePartialFiles() {
let partials = await super.cachePartialFiles();
this.handlebarsLib.registerPartial(partials);
return partials;
}
async compile(str) {
// Ensure partials are cached and registered.
await this.getPartials();
let fn = this.handlebarsLib.compile(str);

@@ -62,0 +70,0 @@ return function (data) {

@@ -5,4 +5,4 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

const EleventyBaseError = require("../EleventyBaseError");
const deleteRequireCache = require("../Util/DeleteRequireCache");
const getJavaScriptData = require("../Util/GetJavaScriptData");
const eventBus = require("../EventBus");

@@ -17,2 +17,11 @@ class JavaScriptTemplateNotDefined extends EleventyBaseError {}

this.cacheable = false;
eventBus.on("eleventy.resourceModified", (inputPath) => {
inputPath = TemplatePath.addLeadingDotSlash(inputPath);
// Remove from cached instances when modified
if (inputPath in this.instances) {
delete this.instances[inputPath];
}
});
}

@@ -39,6 +48,3 @@

} else if (typeof mod === "function") {
if (
mod.prototype &&
("data" in mod.prototype || "render" in mod.prototype)
) {
if (mod.prototype && ("data" in mod.prototype || "render" in mod.prototype)) {
if (!("render" in mod.prototype)) {

@@ -64,3 +70,3 @@ mod.prototype.render = noop;

const mod = this._getRequire(inputPath);
const mod = require(TemplatePath.absolutePath(inputPath));
let inst = this._getInstance(mod);

@@ -78,7 +84,7 @@

_getRequire(inputPath) {
let requirePath = TemplatePath.absolutePath(inputPath);
return require(requirePath);
}
/**
* JavaScript files defer to the module loader rather than read the files to strings
*
* @override
*/
needsToReadFileContents() {

@@ -88,14 +94,2 @@ return false;

// only remove from cache once on startup (if it already exists)
initRequireCache(inputPath) {
let requirePath = TemplatePath.absolutePath(inputPath);
if (requirePath) {
deleteRequireCache(requirePath);
}
if (inputPath in this.instances) {
delete this.instances[inputPath];
}
}
async getExtraDataFromFile(inputPath) {

@@ -115,4 +109,4 @@ let inst = this.getInstanceFromInputPath(inputPath);

} else {
// note: bind creates a new function
fns[key] = configFns[key].bind(inst);
// note: wrapping creates a new function
fns[key] = JavaScript.wrapJavaScriptFunction(inst, configFns[key]);
}

@@ -123,2 +117,15 @@ }

static wrapJavaScriptFunction(inst, fn) {
return function (...args) {
if (inst && inst.page) {
this.page = inst.page;
}
if (inst && inst.eleventy) {
this.eleventy = inst.eleventy;
}
return fn.call(this, ...args);
};
}
async compile(str, inputPath) {

@@ -133,4 +140,8 @@ let inst;

}
if (inst && "render" in inst) {
return function (data) {
// TODO does this do anything meaningful for non-classes?
// `inst` should have a normalized `render` function from _getInstance
// only blow away existing inst.page if it has a page.url

@@ -137,0 +148,0 @@ if (!inst.page || inst.page.url) {

@@ -56,2 +56,24 @@ const moo = require("moo");

static wrapFilter(fn) {
return function (...args) {
if (this.context && "get" in this.context) {
this.page = this.context.get(["page"]);
this.eleventy = this.context.get(["eleventy"]);
}
return fn.call(this, ...args);
};
}
// Shortcodes
static normalizeScope(context) {
let obj = {};
if (context) {
obj.ctx = context;
obj.page = context.get(["page"]);
obj.eleventy = context.get(["eleventy"]);
}
return obj;
}
addCustomTags(tags) {

@@ -70,3 +92,3 @@ for (let name in tags) {

addFilter(name, filter) {
this.liquidLib.registerFilter(name, filter);
this.liquidLib.registerFilter(name, Liquid.wrapFilter(filter));
}

@@ -98,3 +120,3 @@

static async parseArguments(lexer, str, scope, engine) {
static parseArguments(lexer, str) {
let argArray = [];

@@ -107,5 +129,4 @@

if (typeof str === "string") {
// TODO key=value key2=value
// TODO JSON?
lexer.reset(str);
let arg = lexer.next();

@@ -126,3 +147,4 @@ while (arg) {

// Otherwise they run out of order and can lead to undefined values for arguments in layout template shortcodes.
argArray.push(engine.evalValue(arg.value, scope));
// console.log( arg.value, scope, engine );
argArray.push(arg.value);
}

@@ -133,35 +155,27 @@ arg = lexer.next();

return await Promise.all(argArray);
return argArray;
}
static _normalizeShortcodeScope(ctx) {
let obj = {};
if (ctx) {
obj.page = ctx.get(["page"]);
}
return obj;
}
addShortcode(shortcodeName, shortcodeFn) {
let _t = this;
this.addTag(shortcodeName, function () {
this.addTag(shortcodeName, function (liquidEngine) {
return {
parse: function (tagToken) {
parse(tagToken) {
this.name = tagToken.name;
this.args = tagToken.args;
},
render: async function (scope) {
let argArray = await Liquid.parseArguments(
_t.argLexer,
this.args,
scope,
this.liquid
);
render: function* (ctx) {
let rawArgs = Liquid.parseArguments(_t.argLexer, this.args);
let argArray = [];
let contextScope = ctx.getAll();
for (let arg of rawArgs) {
let b = yield liquidEngine.evalValue(arg, contextScope);
argArray.push(b);
}
return Promise.resolve(
shortcodeFn.call(
Liquid._normalizeShortcodeScope(scope),
...argArray
)
let ret = yield shortcodeFn.call(
Liquid.normalizeScope(ctx),
...argArray
);
return ret;
},

@@ -176,3 +190,3 @@ };

return {
parse: function (tagToken, remainTokens) {
parse(tagToken, remainTokens) {
this.name = tagToken.name;

@@ -192,18 +206,23 @@ this.args = tagToken.args;

},
render: function* (ctx) {
let argArray = yield Liquid.parseArguments(
_t.argLexer,
this.args,
ctx,
this.liquid
);
const html = yield this.liquid.renderer.renderTemplates(
render: function* (ctx, emitter) {
let rawArgs = Liquid.parseArguments(_t.argLexer, this.args);
let argArray = [];
let contextScope = ctx.getAll();
for (let arg of rawArgs) {
let b = yield liquidEngine.evalValue(arg, contextScope);
argArray.push(b);
}
const html = yield liquidEngine.renderer.renderTemplates(
this.templates,
ctx
);
return shortcodeFn.call(
Liquid._normalizeShortcodeScope(ctx),
let ret = yield shortcodeFn.call(
Liquid.normalizeScope(ctx),
html,
...argArray
);
// emitter.write(ret);
return ret;
},

@@ -226,4 +245,7 @@ };

// Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink)
permalinkNeedsCompilation(str) {
return this.needsCompilation(str);
if (typeof str === "string") {
return this.needsCompilation(str);
}
}

@@ -230,0 +252,0 @@

@@ -21,3 +21,3 @@ const markdownIt = require("markdown-it");

// This is separate so devs can pass in a new mdLib and still use the official eleventy plugin for markdown highlighting
if (this.config.markdownHighlighter) {
if (this.config.markdownHighlighter && typeof this.mdLib.set === "function") {
this.mdLib.set({

@@ -28,2 +28,7 @@ highlight: this.config.markdownHighlighter,

if (typeof this.mdLib.disable === "function") {
// Disable indented code blocks by default (Issue #2438)
this.mdLib.disable("code");
}
this.setEngineLib(this.mdLib);

@@ -30,0 +35,0 @@ }

@@ -17,3 +17,3 @@ const MustacheLib = require("mustache");

async compile(str) {
let partials = super.getPartials();
let partials = await super.getPartials();

@@ -20,0 +20,0 @@ return function (data) {

@@ -6,122 +6,5 @@ const NunjucksLib = require("nunjucks");

const EleventyErrorUtil = require("../EleventyErrorUtil");
const EleventyBaseError = require("../EleventyBaseError");
const EleventyShortcodeError = require("../EleventyShortcodeError");
const eventBus = require("../EventBus");
/*
* The IFFE below apply a monkey-patch to Nunjucks internals to cache
* compiled templates and re-use them where possible.
*/
(function () {
if (!process.env.ELEVENTY_NUNJUCKS_SPEEDBOOST_OPTIN) {
return;
}
let templateCache = new Map();
let getKey = (obj) => {
return [
obj.path || obj.tmplStr,
obj.tmplStr.length,
obj.env.asyncFilters.length,
obj.env.extensionsList
.map((e) => {
return e.__id || "";
})
.join(":"),
].join(" :: ");
};
let evictByPath = (path) => {
let keys = templateCache.keys();
// Likely to be slow; do we care?
for (let k of keys) {
if (k.indexOf(path) >= 0) {
templateCache.delete(k);
}
}
};
eventBus.on("eleventy.resourceModified", evictByPath);
let _compile = NunjucksLib.Template.prototype._compile;
NunjucksLib.Template.prototype._compile = function _wrap_compile(...args) {
if (!this.compiled && !this.tmplProps && templateCache.has(getKey(this))) {
let pathProps = templateCache.get(getKey(this));
this.blocks = pathProps.blocks;
this.rootRenderFunc = pathProps.rootRenderFunc;
this.compiled = true;
} else {
_compile.call(this, ...args);
templateCache.set(getKey(this), {
blocks: this.blocks,
rootRenderFunc: this.rootRenderFunc,
});
}
};
let extensionIdCounter = 0;
let addExtension = NunjucksLib.Environment.prototype.addExtension;
NunjucksLib.Environment.prototype.addExtension = function _wrap_addExtension(
name,
ext
) {
if (!("__id" in ext)) {
ext.__id = extensionIdCounter++;
}
return addExtension.call(this, name, ext);
};
// NunjucksLib.runtime.Frame.prototype.set is the hotest in-template method.
// We replace it with a version that doesn't allocate a `parts` array on
// repeat key use.
let partsCache = new Map();
let partsFromCache = (name) => {
if (partsCache.has(name)) {
return partsCache.get(name);
}
let parts = name.split(".");
partsCache.set(name, parts);
return parts;
};
let frameSet = NunjucksLib.runtime.Frame.prototype.set;
NunjucksLib.runtime.Frame.prototype.set = function _replacement_set(
name,
val,
resolveUp
) {
let parts = partsFromCache(name);
let frame = this;
let obj = frame.variables;
if (resolveUp) {
if ((frame = this.resolve(parts[0], true))) {
frame.set(name, val);
return;
}
}
// A slightly faster version of the intermediate object allocation loop
let count = parts.length - 1;
let i = 0;
let id = parts[0];
while (i < count) {
// TODO(zachleat) use Object.hasOwn when supported
if ("hasOwnProperty" in obj) {
if (!obj.hasOwnProperty(id)) {
obj = obj[id] = {};
}
} else if (!(id in obj)) {
// Handle Objects with null prototypes (Nunjucks looping stuff)
obj = obj[id] = {};
}
id = parts[++i];
}
obj[id] = val;
};
})();
class EleventyShortcodeError extends EleventyBaseError {}
class Nunjucks extends TemplateEngine {

@@ -133,2 +16,7 @@ constructor(name, dirs, config) {

this.nunjucksPrecompiledTemplates =
this.config.nunjucksPrecompiledTemplates || {};
this._usingPrecompiled =
Object.keys(this.nunjucksPrecompiledTemplates).length > 0;
this.setLibrary(this.config.libraryOverrides.njk);

@@ -139,12 +27,46 @@

_setEnv(override) {
if (override) {
this.njkEnv = override;
} else if (this._usingPrecompiled) {
// Precompiled templates to avoid eval!
function NodePrecompiledLoader() {}
NodePrecompiledLoader.prototype.getSource = (name) => {
// https://github.com/mozilla/nunjucks/blob/fd500902d7c88672470c87170796de52fc0f791a/nunjucks/src/precompiled-loader.js#L5
return {
src: {
type: "code",
obj: this.nunjucksPrecompiledTemplates[name],
},
// Maybe add this?
// path,
// noCache: true
};
};
this.njkEnv = new NunjucksLib.Environment(
new NodePrecompiledLoader(),
this.nunjucksEnvironmentOptions
);
} else {
let fsLoader = new NunjucksLib.FileSystemLoader([
super.getIncludesDir(),
TemplatePath.getWorkingDir(),
]);
this.njkEnv = new NunjucksLib.Environment(
fsLoader,
this.nunjucksEnvironmentOptions
);
}
this.config.events.emit("eleventy.engine.njk", {
nunjucks: NunjucksLib,
environment: this.njkEnv,
});
}
setLibrary(override) {
let fsLoader = new NunjucksLib.FileSystemLoader([
super.getIncludesDir(),
TemplatePath.getWorkingDir(),
]);
this._setEnv(override);
this.njkEnv =
override ||
new NunjucksLib.Environment(fsLoader, this.nunjucksEnvironmentOptions);
// Correct, but overbroad. Better would be to evict more granularly, but

@@ -179,8 +101,38 @@ // resolution from paths isn't straightforward.

addFilters(helpers, isAsync) {
for (let name in helpers) {
this.njkEnv.addFilter(name, helpers[name], isAsync);
addFilters(filters, isAsync) {
for (let name in filters) {
this.njkEnv.addFilter(name, Nunjucks.wrapFilter(filters[name]), isAsync);
}
}
static wrapFilter(fn) {
return function (...args) {
if (this.ctx && this.ctx.page) {
this.page = this.ctx.page;
}
if (this.ctx && this.ctx.eleventy) {
this.eleventy = this.ctx.eleventy;
}
return fn.call(this, ...args);
};
}
// Shortcodes
static normalizeContext(context) {
let obj = {};
if (context.ctx) {
obj.ctx = context.ctx;
if (context.ctx.page) {
obj.page = context.ctx.page;
}
if (context.ctx.eleventy) {
obj.eleventy = context.ctx.eleventy;
}
}
return obj;
}
addCustomTags(tags) {

@@ -227,11 +179,2 @@ for (let name in tags) {

static _normalizeShortcodeContext(context) {
let obj = {};
if (context.ctx && context.ctx.page) {
obj.ctx = context.ctx;
obj.page = context.ctx.page;
}
return obj;
}
_getShortcodeFn(shortcodeName, shortcodeFn, isAsync = false) {

@@ -271,5 +214,8 @@ return function ShortcodeFunction() {

shortcodeFn
.call(Nunjucks._normalizeShortcodeContext(context), ...argArray)
.call(Nunjucks.normalizeContext(context), ...argArray)
.then(function (returnValue) {
resolve(null, new NunjucksLib.runtime.SafeString(returnValue));
resolve(
null,
new NunjucksLib.runtime.SafeString("" + returnValue)
);
})

@@ -288,8 +234,7 @@ .catch(function (e) {

try {
return new NunjucksLib.runtime.SafeString(
shortcodeFn.call(
Nunjucks._normalizeShortcodeContext(context),
...argArray
)
let ret = shortcodeFn.call(
Nunjucks.normalizeContext(context),
...argArray
);
return new NunjucksLib.runtime.SafeString("" + ret);
} catch (e) {

@@ -342,3 +287,3 @@ throw new EleventyShortcodeError(

.call(
Nunjucks._normalizeShortcodeContext(context),
Nunjucks.normalizeContext(context),
bodyContent,

@@ -366,3 +311,3 @@ ...argArray

shortcodeFn.call(
Nunjucks._normalizeShortcodeContext(context),
Nunjucks.normalizeContext(context),
bodyContent,

@@ -398,4 +343,7 @@ ...argArray

// Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink)
permalinkNeedsCompilation(str) {
return this.needsCompilation(str);
if (typeof str === "string") {
return this.needsCompilation(str);
}
}

@@ -410,2 +358,3 @@

let commentStart = optsTags.variableStart || "{#";
return (

@@ -479,12 +428,12 @@ str.indexOf(blockStart) !== -1 ||

return Array.from(new Set(symbols));
let uniqueSymbols = Array.from(new Set(symbols));
return uniqueSymbols;
}
async compile(str, inputPath) {
// for(let loader of this.njkEnv.loaders) {
// loader.cache = {};
// }
let tmpl;
let tmpl;
if (!inputPath || inputPath === "njk" || inputPath === "md") {
if (this._usingPrecompiled) {
tmpl = this.njkEnv.getTemplate(str, true);
} else if (!inputPath || inputPath === "njk" || inputPath === "md") {
tmpl = new NunjucksLib.Template(str, this.njkEnv, null, true);

@@ -491,0 +440,0 @@ } else {

@@ -1,2 +0,1 @@

const fastglob = require("fast-glob");
const fs = require("fs");

@@ -83,5 +82,3 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

if (!this._extensionEntries) {
this._extensionEntries = this.extensionMap.getExtensionEntriesFromKey(
this.name
);
this._extensionEntries = this.extensionMap.getExtensionEntriesFromKey(this.name);
}

@@ -99,6 +96,5 @@ return this._extensionEntries;

// TODO make async
getPartials() {
async getPartials() {
if (!this.partialsHaveBeenCached) {
this.partials = this.cachePartialFiles();
this.partials = await this.cachePartialFiles();
}

@@ -109,41 +105,63 @@

// TODO make async
cachePartialFiles() {
// This only runs if getPartials() is called, which is only for Mustache/Handlebars
/**
* Search for and cache partial files.
*
* This only runs if getPartials() is called, which only runs if you compile a Mustache/Handlebars template.
*
* @protected
*/
async cachePartialFiles() {
this.partialsHaveBeenCached = true;
let partials = {};
let prefix = this.includesDir + "/**/*.";
// TODO: reuse mustache partials in handlebars?
let partialFiles = [];
let results = [];
if (this.includesDir) {
let bench = this.benchmarks.aggregate.get("Searching the file system");
// TODO move this to use FileSystemSearch instead.
const fastglob = require("fast-glob");
let bench = this.benchmarks.aggregate.get("Searching the file system (partials)");
bench.before();
this.extensions.forEach(function (extension) {
partialFiles = partialFiles.concat(
fastglob.sync(prefix + extension, {
caseSensitiveMatch: false,
dot: true,
})
);
});
let prefix = this.includesDir + "/**/*.";
let partialFiles = [];
await Promise.all(
this.extensions.map(async function (extension) {
partialFiles = partialFiles.concat(
await fastglob(prefix + extension, {
caseSensitiveMatch: false,
dot: true,
})
);
})
);
bench.after();
}
partialFiles = TemplatePath.addLeadingDotSlashArray(partialFiles);
results = await Promise.all(
partialFiles.map((partialFile) => {
partialFile = TemplatePath.addLeadingDotSlash(partialFile);
let partialPath = TemplatePath.stripLeadingSubPath(partialFile, this.includesDir);
let partialPathNoExt = partialPath;
this.extensions.forEach(function (extension) {
partialPathNoExt = TemplatePath.removeExtension(partialPathNoExt, "." + extension);
});
for (let j = 0, k = partialFiles.length; j < k; j++) {
let partialPath = TemplatePath.stripLeadingSubPath(
partialFiles[j],
this.includesDir
return fs.promises
.readFile(partialFile, {
encoding: "utf8",
})
.then((content) => {
return {
content,
path: partialPathNoExt,
};
});
})
);
let partialPathNoExt = partialPath;
this.extensions.forEach(function (extension) {
partialPathNoExt = TemplatePath.removeExtension(
partialPathNoExt,
"." + extension
);
});
partials[partialPathNoExt] = fs.readFileSync(partialFiles[j], "utf8");
}
let partials = {};
for (let result of results) {
partials[result.path] = result.content;
}
debug(

@@ -157,4 +175,13 @@ `${this.includesDir}/*.{${this.extensions}} found partials for: %o`,

/**
* @protected
*/
setEngineLib(engineLib) {
this.engineLib = engineLib;
// Run engine amendments (via issue #2438)
for (let amendment of this.config.libraryAmendments[this.name] || []) {
// TODO it’d be nice if this were async friendly
amendment(engineLib);
}
}

@@ -185,8 +212,12 @@

initRequireCache() {
// do nothing
}
getCompileCacheKey(str, inputPath) {
// Changing to use inputPath and contents, using only file contents (`str`) caused issues when two
// different files had identical content (2.0.0-canary.16)
getCompileCacheKey(str, inputPath) {
return str;
// Caches are now segmented based on inputPath so using inputPath here is superfluous (2.0.0-canary.19)
// But we do want a non-falsy value here even if `str` is an empty string.
return {
useCache: true,
key: inputPath + str,
};
}

@@ -207,2 +238,11 @@

/**
* Make sure compile is implemented downstream.
* @abstract
* @return {Promise}
*/
async compile() {
throw new Error("compile() must be implemented by engine");
}
// See https://www.11ty.dev/docs/watch-serve/#watch-javascript-dependencies

@@ -212,4 +252,15 @@ static shouldSpiderJavaScriptDependencies() {

}
hasDependencies(inputPath) {
if (this.config.uses.getDependencies(inputPath) === false) {
return false;
}
return true;
}
isFileRelevantTo(inputPath, comparisonFile) {
return this.config.uses.isFileRelevantTo(inputPath, comparisonFile);
}
}
module.exports = TemplateEngine;

@@ -7,3 +7,3 @@ module.exports = function getCollectionItem(collection, page, modifier = 0) {

item.inputPath === page.inputPath &&
item.outputPath === page.outputPath
(item.outputPath === page.outputPath || item.url === page.url)
) {

@@ -10,0 +10,0 @@ index = j;

const { compile } = require("path-to-regexp");
const normalizeServerlessUrl = require("../Util/NormalizeServerlessUrl");
function stringify(url, urlData = {}) {
let fn = compile(url, { encode: encodeURIComponent });
url = normalizeServerlessUrl(url);
let fn = compile(url, {
encode: encodeURIComponent,
});
return fn(urlData);

@@ -6,0 +11,0 @@ }

@@ -8,2 +8,3 @@ const slugify = require("@sindresorhus/slugify");

{
// lowercase: true, // default
decamelize: false,

@@ -10,0 +11,0 @@ },

@@ -13,3 +13,3 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

// This is also used in the Eleventy Navigation plugin
// Note: This filter is used in the Eleventy Navigation plugin in versions prior to 0.3.4
module.exports = function (url, pathPrefix) {

@@ -19,3 +19,3 @@ // work with undefined

if (isValidUrl(url) || (url.indexOf("//") === 0 && url !== "//")) {
if (isValidUrl(url) || (url.startsWith("//") && url !== "//")) {
return url;

@@ -26,3 +26,3 @@ }

// When you retrieve this with config.getFilter("url") it
// grabs the pathPrefix argument from your config for you.
// grabs the pathPrefix argument from your config for you (see defaultConfig.js)
throw new Error("pathPrefix (String) is required in the `url` filter.");

@@ -29,0 +29,0 @@ }

@@ -10,3 +10,3 @@ const { EleventyServerless } = require("@11ty/eleventy");

path: new URL(event.rawUrl).pathname,
query: event.queryStringParameters,
query: event.multiValueQueryStringParameters || event.queryStringParameters,
functionsDir: "%%FUNCTIONS_DIR%%",

@@ -13,0 +13,0 @@ });

@@ -1,5 +0,9 @@

const lodashChunk = require("lodash/chunk");
const lodashGet = require("lodash/get");
const lodashSet = require("lodash/set");
const lodashChunk = require("lodash.chunk");
const lodashGet = require("lodash.get");
const lodashSet = require("lodash.set");
const { isPlainObject } = require("@11ty/eleventy-utils");
const EleventyBaseError = require("../EleventyBaseError");
const { DeepCopy } = require("../Util/Merge");
const { ProxyWrap } = require("../Util/ProxyWrap");

@@ -10,3 +14,3 @@ class PaginationConfigError extends EleventyBaseError {}

class Pagination {
constructor(data, config) {
constructor(tmpl, data, config) {
if (!config) {

@@ -20,5 +24,13 @@ throw new PaginationConfigError(

this.setTemplate(tmpl);
this.setData(data);
}
get inputPathForErrorMessages() {
if (this.template) {
return ` (${this.template.inputPath})`;
}
return "";
}
static hasPagination(data) {

@@ -30,3 +42,5 @@ return "pagination" in data;

if (!this.data) {
throw new Error("Missing `setData` call for Pagination object.");
throw new Error(
`Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`
);
}

@@ -46,5 +60,3 @@ return Pagination.hasPagination(this.data);

throw new PaginationError(
`Pagination circular reference${
this.template ? ` on ${this.template.inputPath}` : ""
}, data:\`${key}\` iterates over both the \`${tag}\` tag and also supplies pages to that tag.`
`Pagination circular reference${this.inputPathForErrorMessages}, data:\`${key}\` iterates over both the \`${tag}\` tag and also supplies pages to that tag.`
);

@@ -65,6 +77,8 @@ }

throw new Error(
"Misconfigured pagination data in template front matter (YAML front matter precaution: did you use tabs and not spaces for indentation?)."
`Misconfigured pagination data in template front matter${this.inputPathForErrorMessages} (YAML front matter precaution: did you use tabs and not spaces for indentation?).`
);
} else if (!("size" in data.pagination)) {
throw new Error("Missing pagination size in front matter data.");
throw new Error(
`Missing pagination size in front matter data${this.inputPathForErrorMessages}`
);
}

@@ -125,3 +139,3 @@ this.circularReferenceCheck(data);

resolveDataToObjectValues() {
shouldResolveDataToObjectValues() {
if ("resolve" in this.data.pagination) {

@@ -157,3 +171,3 @@ return this.data.pagination.resolve === "values";

throw new Error(
`Could not find pagination data, went looking for: ${key}`
`Could not find pagination data${this.inputPathForErrorMessages}, went looking for: ${key}`
);

@@ -168,6 +182,12 @@ }

keys = this.fullDataSet;
} else if (this.resolveDataToObjectValues()) {
keys = Object.values(this.fullDataSet);
} else if (isPlainObject(this.fullDataSet)) {
if (this.shouldResolveDataToObjectValues()) {
keys = Object.values(this.fullDataSet);
} else {
keys = Object.keys(this.fullDataSet);
}
} else {
keys = Object.keys(this.fullDataSet);
throw new Error(
`Unexpected data found in pagination target${this.inputPathForErrorMessages}: expected an Array or an Object.`
);
}

@@ -183,3 +203,7 @@

// we don’t need to make a copy of this because we .slice() above to create a new copy
result = this.data.pagination.before(result, this.data);
let fns = {};
if (this.config) {
fns = this.config.javascriptFunctions;
}
result = this.data.pagination.before.call(fns, result, this.data);
}

@@ -200,12 +224,14 @@

if (!this.data) {
throw new Error("Missing `setData` call for Pagination object.");
throw new Error(
`Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`
);
}
return lodashChunk(this.target, this.size);
}
const chunks = lodashChunk(this.target, this.size);
// TODO this name is not good
// “To cancel” means to not write the original root template
cancel() {
return this.hasPagination();
if (this.data.pagination && this.data.pagination.generatePageOnEmptyData) {
return chunks.length ? chunks : [[]];
} else {
return chunks;
}
}

@@ -225,23 +251,4 @@

getOverrideData(pageItems) {
let override = {
pagination: {
data: this.data.pagination.data,
size: this.data.pagination.size,
alias: this.alias,
items: pageItems,
},
};
if (this.alias) {
lodashSet(override, this.alias, this.getNormalizedItems(pageItems));
}
return override;
}
getOverrideDataPages(items, pageNumber) {
let obj = {
pages: this.size === 1 ? items.map((entry) => entry[0]) : items,
return {
// See Issue #345 for more examples

@@ -265,7 +272,5 @@ page: {

};
return obj;
}
getOverrideDataLinks(pageNumber, templates, links) {
getOverrideDataLinks(pageNumber, templateCount, links) {
let obj = {};

@@ -278,3 +283,3 @@

obj.nextPageLink =
pageNumber < templates.length - 1 ? links[pageNumber + 1] : null;
pageNumber < templateCount - 1 ? links[pageNumber + 1] : null;
obj.next = obj.nextPageLink;

@@ -291,3 +296,3 @@

getOverrideDataHrefs(pageNumber, templates, hrefs) {
getOverrideDataHrefs(pageNumber, templateCount, hrefs) {
let obj = {};

@@ -298,3 +303,3 @@

obj.nextPageHref =
pageNumber < templates.length - 1 ? hrefs[pageNumber + 1] : null;
pageNumber < templateCount - 1 ? hrefs[pageNumber + 1] : null;

@@ -319,3 +324,5 @@ obj.firstPageHref = hrefs.length > 0 ? hrefs[0] : null;

if (!this.data) {
throw new Error("Missing `setData` call for Pagination object.");
throw new Error(
`Missing \`setData\` call for Pagination object${this.inputPathForErrorMessages}`
);
}

@@ -327,56 +334,108 @@

let pages = [];
let entries = [];
let items = this.chunkedItems;
let tmpl = this.template;
let templates = [];
let pages = this.size === 1 ? items.map((entry) => entry[0]) : items;
let links = [];
let hrefs = [];
let overrides = [];
for (let pageNumber = 0, k = items.length; pageNumber < k; pageNumber++) {
let cloned = tmpl.clone();
let hasPermalinkField = Boolean(this.data[this.config.keys.permalink]);
let hasComputedPermalinkField = Boolean(
this.data.eleventyComputed &&
this.data.eleventyComputed[this.config.keys.permalink]
);
// TODO maybe also move this permalink additions up into the pagination class
let hasPermalinkField = Boolean(this.data[this.config.keys.permalink]);
let hasComputedPermalinkField = Boolean(
this.data.eleventyComputed &&
this.data.eleventyComputed[this.config.keys.permalink]
);
if (pageNumber > 0 && !(hasPermalinkField || hasComputedPermalinkField)) {
// Do *not* pass collections through DeepCopy, we’ll re-add them back in later.
let collections = this.data.collections;
if (collections) {
delete this.data.collections;
}
let parentData = DeepCopy(
{
pagination: {
data: this.data.pagination.data,
size: this.data.pagination.size,
alias: this.alias,
pages,
},
},
this.data
);
// Restore skipped collections
if (collections) {
this.data.collections = collections;
// Keep the original reference to the collections, no deep copy!!
parentData.collections = collections;
}
// TODO future improvement dea: use a light Template wrapper for paged template clones (PagedTemplate?)
// so that we don’t have the memory cost of the full template (and can reuse the parent
// template for some things)
for (let pageNumber = 0; pageNumber < items.length; pageNumber++) {
let cloned = this.template.clone();
if (pageNumber > 0 && !hasPermalinkField && !hasComputedPermalinkField) {
cloned.setExtraOutputSubdirectory(pageNumber);
}
templates.push(cloned);
let override = this.getOverrideData(items[pageNumber]);
let paginationData = {
pagination: {
items: items[pageNumber],
},
page: {},
};
Object.assign(
override.pagination,
paginationData.pagination,
this.getOverrideDataPages(items, pageNumber)
);
overrides.push(override);
cloned.setPaginationData(override);
if (this.alias) {
lodashSet(
paginationData,
this.alias,
this.getNormalizedItems(items[pageNumber])
);
}
// Do *not* deep merge pagination data! See https://github.com/11ty/eleventy/issues/147#issuecomment-440802454
let clonedData = ProxyWrap(paginationData, parentData);
let { rawPath, path, href } = await cloned.getOutputLocations(clonedData);
// TO DO subdirectory to links if the site doesn’t live at /
// TODO missing data argument means Template.getData is regenerated, maybe doesn’t matter because of data cache?
let { rawPath, href } = await cloned.getOutputLocations();
links.push("/" + rawPath);
hrefs.push(href);
// page.url and page.outputPath are used to avoid another getOutputLocations call later, see Template->addComputedData
clonedData.page.url = href;
clonedData.page.outputPath = path;
entries.push({
template: cloned,
data: clonedData,
});
}
// we loop twice to pass in the appropriate prev/next links (already full generated now)
for (let pageNumber = 0; pageNumber < templates.length; pageNumber++) {
let linksObj = this.getOverrideDataLinks(pageNumber, templates, links);
Object.assign(overrides[pageNumber].pagination, linksObj);
let numberOfEntries = entries.length;
for (let pageNumber = 0; pageNumber < numberOfEntries; pageNumber++) {
let linksObj = this.getOverrideDataLinks(
pageNumber,
numberOfEntries,
links
);
let hrefsObj = this.getOverrideDataHrefs(pageNumber, templates, hrefs);
Object.assign(overrides[pageNumber].pagination, hrefsObj);
Object.assign(entries[pageNumber].data.pagination, linksObj);
let cloned = templates[pageNumber];
cloned.setPaginationData(overrides[pageNumber]);
pages.push(cloned);
let hrefsObj = this.getOverrideDataHrefs(
pageNumber,
numberOfEntries,
hrefs
);
Object.assign(entries[pageNumber].data.pagination, hrefsObj);
}
return pages;
return entries;
}

@@ -383,0 +442,0 @@ }

const fs = require("fs");
const fsp = fs.promises;
const isPlainObject = require("../Util/IsPlainObject");
const { TemplatePath } = require("@11ty/eleventy-utils");
const { TemplatePath, isPlainObject } = require("@11ty/eleventy-utils");
// TODO add a first-class Markdown component to expose this using Markdown-only syntax (will need to be synchronous for markdown-it)
const Merge = require("../Util/Merge");
const { ProxyWrap } = require("../Util/ProxyWrap");
const TemplateDataInitialGlobalData = require("../TemplateDataInitialGlobalData");
const EleventyShortcodeError = require("../EleventyShortcodeError");
const TemplateRender = require("../TemplateRender");
const TemplateConfig = require("../TemplateConfig");
const EleventyErrorUtil = require("../EleventyErrorUtil");
const Liquid = require("../Engines/Liquid");
function normalizeDirectories(dir = {}) {
return Object.assign(
{
input: ".",
},
dir
);
}
async function render(
content,
templateLang = "html",
normalizedDirs = {},
{ templateConfig, extensionMap }
) {
async function compile(content, templateLang, { templateConfig, extensionMap } = {}) {
if (!templateConfig) {
templateConfig = new TemplateConfig();
templateConfig = new TemplateConfig(null, false);
}
let tr = new TemplateRender(
templateLang,
normalizedDirs.input,
templateConfig
);
// Breaking change in 2.0+, previous default was `html` and now we default to the page template syntax
if (!templateLang) {
templateLang = this.page.templateSyntax;
}
let cfg = templateConfig;
// templateConfig might already be a userconfig
if (cfg instanceof TemplateConfig) {
cfg = cfg.getConfig();
}
let tr = new TemplateRender(templateLang, cfg.dir.input, templateConfig);
tr.extensionMap = extensionMap;

@@ -50,20 +47,10 @@ tr.setEngineOverride(templateLang);

// No templateLang default, it should infer from the inputPath.
async function renderFile(
inputPath,
templateLang,
normalizedDirs = {},
{ templateConfig, extensionMap }
) {
async function compileFile(inputPath, { templateConfig, extensionMap, config } = {}, templateLang) {
if (!inputPath) {
throw new Error(
"Missing file path argument passed to the `renderFile` shortcode."
);
throw new Error("Missing file path argument passed to the `renderFile` shortcode.");
}
if (
!fs.existsSync(TemplatePath.normalizeOperatingSystemFilePath(inputPath))
) {
if (!fs.existsSync(TemplatePath.normalizeOperatingSystemFilePath(inputPath))) {
throw new Error(
"Could not find render plugin file for the `renderFile` shortcode, looking for: " +
inputPath
"Could not find render plugin file for the `renderFile` shortcode, looking for: " + inputPath
);

@@ -73,6 +60,10 @@ }

if (!templateConfig) {
templateConfig = new TemplateConfig();
templateConfig = new TemplateConfig(null, false);
}
if (config && typeof config === "function") {
await config(templateConfig.userConfig);
}
let tr = new TemplateRender(inputPath, normalizedDirs.input, templateConfig);
let cfg = templateConfig.getConfig();
let tr = new TemplateRender(inputPath, cfg.dir.input, templateConfig);
tr.extensionMap = extensionMap;

@@ -92,3 +83,40 @@ if (templateLang) {

async function renderShortcodeFn(fn, data) {
if (fn === undefined) {
return;
} else if (typeof fn !== "function") {
throw new Error(`The \`compile\` function did not return a function. Received ${fn}`);
}
// if the user passes a string or other literal, remap to an object.
if (!isPlainObject(data)) {
data = {
_: data,
};
}
if ("data" in this && isPlainObject(this.data)) {
// when options.accessGlobalData is true, this allows the global data
// to be accessed inside of the shortcode as a fallback
data = ProxyWrap(data, this.data);
} else {
// save `page` and `eleventy` for reuse
data.page = this.page;
data.eleventy = this.eleventy;
}
return fn(data);
}
function EleventyPlugin(eleventyConfig, options = {}) {
let opts = Object.assign(
{
tagName: "renderTemplate",
tagNameFile: "renderFile",
templateConfig: null,
accessGlobalData: false,
},
options
);
function liquidTemplateTag(liquidEngine, tagName) {

@@ -114,22 +142,33 @@ // via https://github.com/harttle/liquidjs/blob/b5a22fa0910c708fe7881ef170ed44d3594e18f3/src/builtin/tags/raw.ts

},
render: async function (ctx) {
render: function* (ctx) {
let normalizedContext = {};
if (ctx) {
if (opts.accessGlobalData) {
// parent template data cascade
normalizedContext.data = ctx.getAll();
}
normalizedContext.page = ctx.get(["page"]);
normalizedContext.eleventy = ctx.get(["eleventy"]);
}
let argArray = await Liquid.parseArguments(
null,
this.args,
ctx,
this.liquid
);
let rawArgs = Liquid.parseArguments(null, this.args);
let argArray = [];
let contextScope = ctx.getAll();
for (let arg of rawArgs) {
let b = yield liquidEngine.evalValue(arg, contextScope);
argArray.push(b);
}
// plaintext paired shortcode content
let body = this.tokens.map((token) => token.getText()).join("");
return renderStringShortcodeFn.call(
let ret = _renderStringShortcodeFn.call(
normalizedContext,
body,
// templateLang, data
...argArray
);
yield ret;
return ret;
},

@@ -163,6 +202,3 @@ };

// or when we've found the matching "endraw" block
while (
(matches = parser.tokens._extractRegex(rawBlockRegex)) &&
rawLevel > 0
) {
while ((matches = parser.tokens._extractRegex(rawBlockRegex)) && rawLevel > 0) {
const all = matches[0];

@@ -204,3 +240,10 @@ const pre = matches[1];

normalizedContext.ctx = context.ctx;
// TODO .data
// if(opts.accessGlobalData) {
// normalizedContext.data = context.ctx;
// }
normalizedContext.page = context.ctx.page;
normalizedContext.eleventy = context.ctx.eleventy;
}

@@ -212,3 +255,3 @@

new EleventyShortcodeError(
`Error with Nunjucks paired shortcode \`${shortcodeName}\`${EleventyErrorUtil.convertErrorToString(
`Error with Nunjucks paired shortcode \`${tagName}\`${EleventyErrorUtil.convertErrorToString(
e

@@ -221,5 +264,6 @@ )}`

Promise.resolve(
renderStringShortcodeFn.call(
_renderStringShortcodeFn.call(
normalizedContext,
bodyContent,
// templateLang, data
...argArray

@@ -234,3 +278,3 @@ )

new EleventyShortcodeError(
`Error with Nunjucks paired shortcode \`${shortcodeName}\`${EleventyErrorUtil.convertErrorToString(
`Error with Nunjucks paired shortcode \`${tagName}\`${EleventyErrorUtil.convertErrorToString(
e

@@ -247,10 +291,2 @@ )}`

let opts = Object.assign(
{
tagName: "renderTemplate",
tagNameFile: "renderFile",
},
options
);
// This will only work on 1.0.0-beta.5+ but is only necessary if you want to reuse your config inside of template shortcodes.

@@ -268,88 +304,111 @@ // Just rendering raw templates (without filters, shortcodes, etc. from your config) will work fine on old versions.

async function renderStringShortcodeFn(content, templateLang, data = {}) {
let fn = await render.call(
this,
content,
templateLang,
normalizeDirectories(eleventyConfig.dir),
{
templateConfig,
extensionMap,
}
);
if (fn === undefined) {
return;
} else if (typeof fn !== "function") {
throw new Error(
`The \`compile\` function did not return a function. Received ${fn}`
);
async function _renderStringShortcodeFn(content, templateLang, data = {}) {
// Default is fn(content, templateLang, data) but we want to support fn(content, data) too
if (typeof templateLang !== "string") {
data = templateLang;
templateLang = false;
}
// if the user passes a string or other literal, remap to an object.
if (!isPlainObject(data)) {
data = {
_: data,
};
}
let fn = await compile.call(this, content, templateLang, {
templateConfig: opts.templateConfig || templateConfig,
extensionMap,
});
// save `page` for reuse
data.page = this.page;
return fn(data);
return renderShortcodeFn.call(this, fn, data);
}
async function renderFileShortcodeFn(inputPath, data = {}, templateLang) {
let fn = await renderFile.call(
async function _renderFileShortcodeFn(inputPath, data = {}, templateLang) {
let fn = await compileFile.call(
this,
inputPath,
templateLang,
normalizeDirectories(eleventyConfig.dir),
{
templateConfig,
templateConfig: opts.templateConfig || templateConfig,
extensionMap,
}
},
templateLang
);
if (fn === undefined) {
return;
} else if (typeof fn !== "function") {
throw new Error(
`The \`compile\` function did not return a function. Received ${fn}`
);
return renderShortcodeFn.call(this, fn, data);
}
// Render strings
if (opts.tagName) {
// use falsy to opt-out
eleventyConfig.addJavaScriptFunction(opts.tagName, _renderStringShortcodeFn);
eleventyConfig.addLiquidTag(opts.tagName, function (liquidEngine) {
return liquidTemplateTag(liquidEngine, opts.tagName);
});
eleventyConfig.addNunjucksTag(opts.tagName, function (nunjucksLib) {
return nunjucksTemplateTag(nunjucksLib, opts.tagName);
});
}
// Render File
// use `false` to opt-out
if (opts.tagNameFile) {
eleventyConfig.addAsyncShortcode(opts.tagNameFile, _renderFileShortcodeFn);
}
}
module.exports = EleventyPlugin;
module.exports.File = compileFile;
module.exports.String = compile;
// Will re-use the same configuration instance both at a top level and across any nested renders
class RenderManager {
constructor() {
this.templateConfig = new TemplateConfig(null, false);
// This is the only plugin running on the Edge
this.templateConfig.userConfig.addPlugin(EleventyPlugin, {
templateConfig: this.templateConfig,
accessGlobalData: true,
});
}
// `callback` is async-friendly but requires await upstream
config(callback) {
// run an extra `function(eleventyConfig)` configuration callbacks
if (callback && typeof callback === "function") {
return callback(this.templateConfig.userConfig);
}
}
// if the user passes a string or other literal, remap to an object.
if (!isPlainObject(data)) {
data = {
_: data,
};
get initialGlobalData() {
if (!this._data) {
this._data = new TemplateDataInitialGlobalData(this.templateConfig);
}
return this._data;
}
// save `page` for re-use
data.page = this.page;
return fn(data);
// because we don’t have access to the full data cascade—but
// we still want configuration data added via `addGlobalData`
async getData(...data) {
let globalData = await this.initialGlobalData.getData();
let merged = Merge({}, globalData, ...data);
return merged;
}
// Render strings
eleventyConfig.addJavaScriptFunction(opts.tagName, renderStringShortcodeFn);
compile(content, templateLang, options = {}) {
// Missing here: extensionMap
options.templateConfig = this.templateConfig;
eleventyConfig.addLiquidTag(opts.tagName, function (liquidEngine) {
return liquidTemplateTag(liquidEngine, opts.tagName);
});
// We don’t need `compile.call(this)` here because the Edge always uses "liquid" as the template lang (instead of relying on this.page.templateSyntax)
// returns promise
return compile(content, templateLang, options);
}
eleventyConfig.addNunjucksTag(opts.tagName, function (nunjucksLib) {
return nunjucksTemplateTag(nunjucksLib, opts.tagName);
});
async render(fn, edgeData, buildTimeData) {
let mergedData = await this.getData(edgeData);
// Set .data for options.accessGlobalData feature
let context = {
data: mergedData,
};
// Render File
eleventyConfig.addJavaScriptFunction(opts.tagNameFile, renderFileShortcodeFn);
eleventyConfig.addLiquidShortcode(opts.tagNameFile, renderFileShortcodeFn);
eleventyConfig.addNunjucksAsyncShortcode(
opts.tagNameFile,
renderFileShortcodeFn
);
return renderShortcodeFn.call(context, fn, buildTimeData);
}
}
module.exports = EleventyPlugin;
module.exports.RenderManager = RenderManager;

@@ -5,114 +5,29 @@ const fs = require("fs");

const isGlob = require("is-glob");
const TOML = require("@iarna/toml");
const copy = require("recursive-copy");
const dependencyTree = require("@11ty/dependency-tree");
const querystring = require("querystring");
const { TemplatePath } = require("@11ty/eleventy-utils");
const NetlifyRedirects = require("./Serverless/NetlifyRedirects");
const { EleventyRequire } = require("../Util/Require");
const DirContains = require("../Util/DirContains");
const JavaScriptDependencies = require("../Util/JavaScriptDependencies");
const deleteRequireCache = require("../Util/DeleteRequireCache");
const debug = require("debug")("Eleventy:Serverless");
function netlifyTomlRedirectHandler(name, outputMap, target) {
if (!target) {
throw new Error(
`Missing redirect target in Eleventy Serverless Bundler Plugin. Received ${target}`
);
}
let newRedirects = [];
for (let url in outputMap) {
newRedirects.push({
from: url,
to: `${target}${name}`,
status: 200,
force: true,
_generated_by_eleventy_serverless: name,
});
}
let configFilename = "./netlify.toml";
let cfg = {};
// parse existing netlify.toml
if (fs.existsSync(configFilename)) {
cfg = TOML.parse(fs.readFileSync(configFilename));
}
let cfgWithRedirects = addRedirectsWithoutDuplicates(name, cfg, newRedirects);
fs.writeFileSync(configFilename, TOML.stringify(cfgWithRedirects));
debug(
`Eleventy Serverless (${name}), writing (×${newRedirects.length}): ${configFilename}`
);
}
// Provider specific
const redirectHandlers = {
"netlify-toml": function (name, outputMap) {
return netlifyTomlRedirectHandler(name, outputMap, "/.netlify/functions/");
let r = new NetlifyRedirects(name);
return r.writeFile(outputMap, "/.netlify/functions/");
},
"netlify-toml-functions": function (name, outputMap) {
return netlifyTomlRedirectHandler(name, outputMap, "/.netlify/functions/");
let r = new NetlifyRedirects(name);
return r.writeFile(outputMap, "/.netlify/functions/");
},
"netlify-toml-builders": function (name, outputMap) {
return netlifyTomlRedirectHandler(name, outputMap, "/.netlify/builders/");
let r = new NetlifyRedirects(name);
return r.writeFile(outputMap, "/.netlify/builders/");
},
};
function getNodeModulesList(files) {
let pkgs = new Set();
let jsFiles = files.filter((entry) => entry.endsWith(".js"));
for (let filepath of jsFiles) {
let modules = dependencyTree(filepath, {
nodeModuleNamesOnly: true,
allowNotFound: true, // TODO is this okay?
});
for (let name of modules) {
pkgs.add(name);
}
}
return Array.from(pkgs).sort();
}
function addRedirectsWithoutDuplicates(name, config, newRedirects) {
// keep non-generated redirects or those generated by a different function
let redirects = (config.redirects || []).filter((entry) => {
return (
!entry._generated_by_eleventy_serverless ||
entry._generated_by_eleventy_serverless !== name
);
});
// Sort for stable order
newRedirects.sort((a, b) => {
if (a.from < b.from) {
return -1;
} else if (a.from > b.from) {
return 1;
}
return 0;
});
for (let r of newRedirects) {
let found = false;
for (let entry of redirects) {
if (r.from === entry.from && r.to === entry.to) {
found = true;
}
}
if (!found) {
redirects.unshift(r);
}
}
if (redirects.length) {
config.redirects = redirects;
} else {
delete config.redirects;
}
return config;
}
class BundlerHelper {

@@ -142,12 +57,2 @@ constructor(name, options, eleventyConfig) {

copyFile(fullPath, outputFilename) {
debug(
`Eleventy Serverless: Copying ${fullPath} to ${this.getOutputPath(
outputFilename
)}`
);
fs.copyFileSync(fullPath, this.getOutputPath(outputFilename));
this.copyCount++;
}
recursiveCopy(src, dest, options = {}) {

@@ -160,2 +65,4 @@ // skip this one if not a glob and doesn’t exist

let finalDest = this.getOutputPath(dest || src);
debug(`Eleventy Serverless: Copying ${src} to ${finalDest}`);
return copy(

@@ -167,3 +74,3 @@ src,

overwrite: true,
dot: true,
dot: false,
junk: false,

@@ -181,4 +88,8 @@ results: false,

writeBundlerDependenciesFile(filename, deps = []) {
let fullPath = this.getOutputPath(filename);
if (deps.length === 0 && fs.existsSync(fullPath)) {
return;
}
let modules = deps.map((name) => `require("${name}");`);
let fullPath = this.getOutputPath(filename);
fs.writeFileSync(fullPath, modules.join("\n"));

@@ -192,3 +103,10 @@ this.copyCount++;

writeDependencyEntryFile() {
// we write this even when disabled because the serverless function expects it
// ensure these exist for requiring
if (this.options.copyEnabled) {
this.writeBundlerDependenciesFile("eleventy-app-config-modules.js");
this.writeBundlerDependenciesFile("eleventy-app-globaldata-modules.js");
this.writeBundlerDependenciesFile("eleventy-app-dirdata-modules.js");
}
// we write these even when copy is disabled because the serverless function expects it
this.writeBundlerDependenciesFile(

@@ -200,2 +118,3 @@ "eleventy-bundler-modules.js",

"./eleventy-app-globaldata-modules.js",
"./eleventy-app-dirdata-modules.js",
]

@@ -206,3 +125,12 @@ : []

writeDependencyConfigFile(configPath) {
async copyFileList(fileList) {
let promises = [];
for (let file of fileList) {
promises.push(this.recursiveCopy(file));
}
return Promise.all(promises);
}
// Does *not* copy the original files (only the deps)
async processJavaScriptFiles(files, dependencyFilename) {
if (!this.options.copyEnabled) {

@@ -212,35 +140,45 @@ return;

let modules = getNodeModulesList([configPath]);
let nodeModules = JavaScriptDependencies.getDependencies(files, true);
this.writeBundlerDependenciesFile(
"eleventy-app-config-modules.js",
modules.filter(
(name) => this.options.excludeDependencies.indexOf(name) === -1
)
dependencyFilename,
nodeModules.filter((name) => this.options.excludeDependencies.indexOf(name) === -1)
);
let localModules = JavaScriptDependencies.getDependencies(files, false);
// promise
return this.copyFileList(localModules);
}
writeDependencyGlobalDataFile(globalDataFileList) {
if (!this.options.copyEnabled) {
return;
// https://github.com/11ty/eleventy/issues/2422
// This behavior is dictated by AWS Lambda https://aws.amazon.com/blogs/compute/support-for-multi-value-parameters-in-amazon-api-gateway/
// Duplicate keys are provided as a single comma separated string
getSingleValueQueryParams(searchParams) {
let obj = {};
for (let [key, value] of searchParams) {
if (!obj[key]) {
obj[key] = value;
} else {
obj[key] += `, ${value}`;
}
}
return obj;
}
let modules = getNodeModulesList(globalDataFileList);
this.writeBundlerDependenciesFile(
"eleventy-app-globaldata-modules.js",
modules.filter(
(name) => this.options.excludeDependencies.indexOf(name) === -1
)
);
// https://github.com/11ty/eleventy/issues/2422
// This behavior is dictated by AWS Lambda https://aws.amazon.com/blogs/compute/support-for-multi-value-parameters-in-amazon-api-gateway/
// Duplicate keys are combined into one array of values
getMultiValueQueryParams(searchParams) {
return querystring.parse(searchParams.toString());
}
browserSyncMiddleware() {
serverMiddleware() {
let serverlessFilepath = TemplatePath.addLeadingDotSlash(
path.join(TemplatePath.getWorkingDir(), this.dir, "index")
);
deleteRequireCache(TemplatePath.absolutePath(serverlessFilepath));
return async function EleventyServerlessMiddleware(req, res, next) {
let serverlessFunction = require(serverlessFilepath);
deleteRequireCache(serverlessFilepath);
let serverlessFunction = EleventyRequire(serverlessFilepath);
let url = new URL(req.url, "http://localhost/"); // any domain will do here, we just want the searchParams
let queryParams = Object.fromEntries(url.searchParams);

@@ -252,5 +190,7 @@ let start = new Date();

rawUrl: url.toString(),
// @netlify/functions builder overwrites these to {} intentionally
// See https://github.com/netlify/functions/issues/38
queryStringParameters: queryParams,
queryStringParameters: this.getSingleValueQueryParams(url.searchParams),
multiValueQueryStringParameters: this.getMultiValueQueryParams(url.searchParams),
});

@@ -265,3 +205,2 @@

res.write(result.body);
res.end();

@@ -271,2 +210,11 @@ this.eleventyConfig.logger.forceLog(

);
// eleventy-dev-server 1.0.0-canary.10 and newer
if ("_shouldForceEnd" in res) {
res._shouldForceEnd = true;
next();
} else {
// eleventy-dev-server 1.0.0-canary.9 and below
res.end();
}
}.bind(this);

@@ -291,6 +239,3 @@ }

contents = contents.replace(/\%\%NAME\%\%/g, this.name);
contents = contents.replace(
/\%\%FUNCTIONS_DIR\%\%/g,
this.options.functionsDir
);
contents = contents.replace(/\%\%FUNCTIONS_DIR\%\%/g, this.options.functionsDir);
return fsp.writeFile(filepath, contents);

@@ -326,5 +271,3 @@ }

if (!options.name) {
throw new Error(
"Serverless addPlugin second argument options object must have a name."
);
throw new Error("Serverless addPlugin second argument options object must have a name.");
}

@@ -335,4 +278,4 @@

eleventyConfig.setBrowserSyncConfig({
middleware: [helper.browserSyncMiddleware()],
eleventyConfig.setServerOptions({
middleware: [helper.serverMiddleware()],
});

@@ -362,6 +305,3 @@

} else {
debug(
"Ignored extra copy %o (needs to be a string or a {from: '', to: ''})",
cp
);
debug("Ignored extra copy %o (needs to be a string or a {from: '', to: ''})", cp);
}

@@ -382,13 +322,19 @@ }

if (options.copyEnabled) {
helper.copyFile(env.config, "eleventy.config.js");
if (!options.copyEnabled) {
return;
}
helper.writeDependencyConfigFile(env.config);
}
await helper.recursiveCopy(env.config, "eleventy.config.js");
await helper.processJavaScriptFiles([env.config], "eleventy-app-config-modules.js");
});
eleventyConfig.on("eleventy.globalDataFiles", (fileList) => {
helper.writeDependencyGlobalDataFile(fileList);
eleventyConfig.on("eleventy.globalDataFiles", async (fileList) => {
if (!options.copyEnabled) {
return;
}
// Note that originals are copied in `eleventy.directories` event below
await helper.processJavaScriptFiles(fileList, "eleventy-app-globaldata-modules.js");
});
// directory data files only
eleventyConfig.on("eleventy.dataFiles", async (fileList) => {

@@ -399,7 +345,4 @@ if (!options.copyEnabled) {

let promises = [];
for (let file of fileList) {
promises.push(helper.recursiveCopy(file));
}
await Promise.all(promises);
await helper.copyFileList(fileList);
await helper.processJavaScriptFiles(fileList, "eleventy-app-dirdata-modules.js");
});

@@ -410,5 +353,12 @@

if (options.copyEnabled) {
promises.push(helper.recursiveCopy(dirs.data));
promises.push(helper.recursiveCopy(dirs.includes));
if (dirs.layouts) {
promises.push(helper.recursiveCopy(dirs.input));
if (!DirContains(dirs.input, dirs.data)) {
promises.push(helper.recursiveCopy(dirs.data));
}
if (!DirContains(dirs.input, dirs.includes)) {
promises.push(helper.recursiveCopy(dirs.includes));
}
if (dirs.layouts && !DirContains(dirs.input, dirs.layouts)) {
// TODO avoid copy if dirs.layouts is in dirs.input
promises.push(helper.recursiveCopy(dirs.layouts));

@@ -472,5 +422,3 @@ }

fs.writeFileSync(filename, JSON.stringify(outputMap, null, 2));
debug(
`Eleventy Serverless (${options.name}), writing (×${mapEntryCount}): ${filename}`
);
debug(`Eleventy Serverless (${options.name}), writing (×${mapEntryCount}): ${filename}`);
this.copyCount++;

@@ -480,6 +428,3 @@

if (options.copyEnabled && options.redirects) {
if (
typeof options.redirects === "string" &&
redirectHandlers[options.redirects]
) {
if (typeof options.redirects === "string" && redirectHandlers[options.redirects]) {
redirectHandlers[options.redirects](options.name, outputMap);

@@ -490,9 +435,2 @@ } else if (typeof options.redirects === "function") {

}
if (options.copyEnabled && mapEntryCount > 0) {
// Copy templates to bundle folder
for (let url in outputMap) {
helper.recursiveCopy(outputMap[url]);
}
}
});

@@ -499,0 +437,0 @@ }

@@ -7,2 +7,3 @@ const path = require("path");

const Eleventy = require("./Eleventy");
const normalizeServerlessUrl = require("./Util/NormalizeServerlessUrl");
const deleteRequireCache = require("./Util/DeleteRequireCache");

@@ -25,5 +26,3 @@ const debug = require("debug")("Eleventy:Serverless");

if (!this.path) {
throw new Error(
"`path` must exist in the options argument in Eleventy Serverless."
);
throw new Error("`path` must exist in the options argument in Eleventy Serverless.");
}

@@ -44,12 +43,24 @@

functionsDir: "functions/",
matchUrlToPattern(path, urlToCompare) {
urlToCompare = normalizeServerlessUrl(urlToCompare);
let fn = match(urlToCompare, { decode: decodeURIComponent });
return fn(path);
},
// Query String Parameters
query: {},
// Configuration callback
config: function (eleventyConfig) {},
// Is serverless build scoped to a single template?
// Use `false` to make serverless more collections-friendly (but slower!)
// With `false` you don’t need precompiledCollections.
// Works great with on-demand builders
singleTemplateScope: true,
// Inject shared collections
precompiledCollections: {},
// Configuration callback
config: function (eleventyConfig) {},
},

@@ -63,8 +74,3 @@ options

initializeEnvironmentVariables() {
// set and delete env variables to make it work the same on --serve
this.serverlessEnvironmentVariableAlreadySet =
!!process.env.ELEVENTY_SERVERLESS;
if (!this.serverlessEnvironmentVariableAlreadySet) {
process.env.ELEVENTY_SERVERLESS = true;
}
this.serverlessEnvironmentVariableAlreadySet = !!process.env.ELEVENTY_SERVERLESS;
}

@@ -93,5 +99,3 @@

throw new Error(
`Couldn’t find the "${dir}" directory. Looked in: ${paths}`
);
throw new Error(`Couldn’t find the "${dir}" directory. Looked in: ${paths}`);
}

@@ -101,10 +105,8 @@

let fullPath = TemplatePath.absolutePath(this.dir, this.mapFilename);
debug(
`Including content map (maps output URLs to input files) from ${fullPath}`
);
debug(`Including content map (maps output URLs to input files) from ${fullPath}`);
// TODO dedicated reset method, don’t delete this every time
deleteRequireCache(fullPath);
let mapContent = require(fullPath);
return mapContent;
return require(fullPath);
}

@@ -115,7 +117,7 @@

debug(`Including config info file from ${fullPath}`);
// TODO dedicated reset method, don’t delete this every time
deleteRequireCache(fullPath);
let configInfo = require(fullPath);
return configInfo;
return require(fullPath);
}

@@ -170,6 +172,3 @@

let inputDir =
this.options.input ||
this.options.inputDir ||
this.getConfigInfo().dir.input;
let inputDir = this.options.input || this.options.inputDir || this.getConfigInfo().dir.input;
let configPath = path.join(this.dir, this.configFilename);

@@ -180,5 +179,3 @@ let { pathParams, inputPath } = this.matchUrlPattern(this.path);

let err = new Error(
`No matching URL found for ${this.path} in ${JSON.stringify(
this.getContentMap()
)}`
`No matching URL found for ${this.path} in ${JSON.stringify(this.getContentMap())}`
);

@@ -198,6 +195,10 @@ err.httpStatusCode = 404;

// TODO (@zachleat) change to use this hook: https://github.com/11ty/eleventy/issues/1957
this.initializeEnvironmentVariables();
let elev = new Eleventy(this.options.input || inputPath, null, {
let isScoped = !!this.options.singleTemplateScope;
let projectInput = isScoped ? this.options.input || inputPath : inputDir;
let elev = new Eleventy(projectInput, null, {
// https://github.com/11ty/eleventy/issues/1957
isServerless: true,
configPath,

@@ -207,5 +208,3 @@ inputDir,

if (Object.keys(this.options.precompiledCollections).length > 0) {
eleventyConfig.setPrecompiledCollections(
this.options.precompiledCollections
);
eleventyConfig.setPrecompiledCollections(this.options.precompiledCollections);
}

@@ -227,10 +226,17 @@

if (!isScoped) {
elev.setIncrementalFile(this.options.input || inputPath);
}
let json = await elev.toJSON();
// TODO (@zachleat) https://github.com/11ty/eleventy/issues/1957
// https://github.com/11ty/eleventy/issues/1957
this.deleteEnvironmentVariables();
let filtered = json.filter((entry) => {
return entry.inputPath === inputPath;
});
let filtered = [];
if (Array.isArray(json)) {
filtered = json.filter((entry) => {
return entry.inputPath === inputPath;
});
}

@@ -237,0 +243,0 @@ if (!filtered.length) {

@@ -9,10 +9,10 @@ const fs = require("graceful-fs");

const normalize = require("normalize-path");
const lodashGet = require("lodash/get");
const lodashSet = require("lodash/set");
const lodashGet = require("lodash.get");
const lodashSet = require("lodash.set");
const { DateTime } = require("luxon");
const { TemplatePath } = require("@11ty/eleventy-utils");
const { TemplatePath, isPlainObject } = require("@11ty/eleventy-utils");
const isPlainObject = require("./Util/IsPlainObject");
const ConsoleLogger = require("./Util/ConsoleLogger");
const getDateFromGitLastUpdated = require("./Util/DateGitLastUpdated");
const getDateFromGitFirstAdded = require("./Util/DateGitFirstAdded");

@@ -38,10 +38,3 @@ const TemplateData = require("./TemplateData");

class Template extends TemplateContent {
constructor(
templatePath,
inputDir,
outputDir,
templateData,
extensionMap,
config
) {
constructor(templatePath, inputDir, outputDir, templateData, extensionMap, config) {
debugDev("new Template(%o)", templatePath);

@@ -65,18 +58,10 @@ super(templatePath, inputDir, config);

this.transforms = [];
this.templateData = templateData;
if (this.templateData) {
this.templateData.setInputDir(this.inputDir);
}
this.paginationData = {};
this.setTemplateData(templateData);
this.isVerbose = true;
this.isDryRun = false;
this.writeCount = 0;
this.skippedCount = 0;
this.wrapWithLayouts = true;
this.fileSlug = new TemplateFileSlug(
this.inputPath,
this.inputDir,
this.extensionMap
);
this.fileSlug = new TemplateFileSlug(this.inputPath, this.inputDir, this.extensionMap);
this.fileSlugStr = this.fileSlug.getSlug();

@@ -93,2 +78,9 @@ this.filePathStem = this.fileSlug.getFullPathWithoutExtension();

setTemplateData(templateData) {
this.templateData = templateData;
if (this.templateData) {
this.templateData.setInputDir(this.inputDir);
}
}
get logger() {

@@ -107,2 +99,25 @@ if (!this._logger) {

setRenderableOverride(renderableOverride) {
this.behavior.setRenderableOverride(renderableOverride);
}
reset() {
this.writeCount = 0;
}
resetCaches(types) {
types = this.getResetTypes(types);
super.resetCaches(types);
if (types.data) {
delete this._dataCache;
}
if (types.render) {
delete this._cacheRenderedContent;
delete this._cacheFinalContent;
}
}
setOutputFormat(to) {

@@ -118,2 +133,7 @@ this.outputFormat = to;

setDryRunViaIncremental() {
this.isDryRun = true;
this.isIncremental = true;
}
setDryRun(isDryRun) {

@@ -123,6 +143,2 @@ this.isDryRun = !!isDryRun;

setWrapWithLayouts(wrap) {
this.wrapWithLayouts = wrap;
}
setExtraOutputSubdirectory(dir) {

@@ -137,22 +153,11 @@ this.extraOutputSubdirectory = dir + "/";

getLayout(layoutKey) {
if (!this._layout || layoutKey !== this._layoutKey) {
this._layoutKey = layoutKey;
this._layout = TemplateLayout.getTemplate(
layoutKey,
this.getInputDir(),
this.config,
this.extensionMap
);
}
return this._layout;
// already cached downstream in TemplateLayout -> TemplateCache
return TemplateLayout.getTemplate(
layoutKey,
this.getInputDir(),
this.config,
this.extensionMap
);
}
async _testGetLayoutChain() {
if (!this._layout) {
await this.getData();
}
return this._layout._testGetLayoutChain();
}
get baseFile() {

@@ -180,3 +185,3 @@ return this.extensionMap.removeTemplateExtension(this.parsed.base);

initServerlessUrlsForEmptyPaginationTemplates(permalinkValue) {
async initServerlessUrlsForEmptyPaginationTemplates(permalinkValue) {
if (isPlainObject(permalinkValue)) {

@@ -192,11 +197,9 @@ let buildlessPermalink = Object.assign({}, permalinkValue);

_getRawPermalinkInstance(permalinkValue) {
let perm = new TemplatePermalink(
permalinkValue,
this.extraOutputSubdirectory
);
async _getRawPermalinkInstance(permalinkValue) {
let perm = new TemplatePermalink(permalinkValue, this.extraOutputSubdirectory);
perm.setUrlTransforms(this.config.urlTransforms);
if (this.templateData) {
perm.setServerlessPathData(this.templateData.getServerlessPathData());
perm.setServerlessPathData(await this.templateData.getServerlessPathData());
}
this.behavior.setFromPermalink(perm);

@@ -210,3 +213,3 @@ this.serverlessUrls = perm.getServerlessUrls();

if (!data) {
data = await this.getData();
throw new Error("data argument missing in Template->_getLink");
}

@@ -222,6 +225,3 @@

permalinkValue = permalink;
} else if (
permalink &&
(!this.config.dynamicPermalinks || data.dynamicPermalink === false)
) {
} else if (permalink && (!this.config.dynamicPermalinks || data.dynamicPermalink === false)) {
debugDev("Not using dynamic permalinks, using %o", permalink);

@@ -240,7 +240,3 @@ permalinkValue = permalink;

promises.push(
Promise.all(
[...permalink[key]].map((entry) =>
super.renderPermalink(entry, data)
)
)
Promise.all([...permalink[key]].map((entry) => super.renderPermalink(entry, data)))
);

@@ -270,8 +266,3 @@ } else {

permalinkValue = await super.renderPermalink(permalink, data);
debug(
"Rendering permalink for %o: %s becomes %o",
this.inputPath,
permalink,
permalinkValue
);
debug("Rendering permalink for %o: %s becomes %o", this.inputPath, permalink, permalinkValue);
debugDev("Permalink rendered with data: %o", data);

@@ -284,7 +275,3 @@ }

if (typeof permalinkCompilation === "function") {
let ret = await this._renderFunction(
permalinkCompilation,
permalinkValue,
this.inputPath
);
let ret = await this._renderFunction(permalinkCompilation, permalinkValue, this.inputPath);
if (ret !== undefined) {

@@ -307,3 +294,3 @@ if (typeof ret === "function") {

// No `permalink` specified in data cascade, do the default
return TemplatePermalink.generate(
let p = TemplatePermalink.generate(
this.getTemplateSubfolder(),

@@ -315,2 +302,4 @@ this.baseFile,

);
p.setUrlTransforms(this.config.urlTransforms);
return p;
}

@@ -321,5 +310,3 @@

// TODO this only works with immediate front matter and not data files
this._usePermalinkRoot = (await this.getFrontMatterData())[
this.config.keys.permalinkRoot
];
this._usePermalinkRoot = (await this.getFrontMatterData())[this.config.keys.permalinkRoot];
}

@@ -332,2 +319,3 @@

async getOutputLocations(data) {
this.bench.get("(count) getOutputLocations").incrementCount();
let link = await this._getLink(data);

@@ -349,4 +337,6 @@

// This is likely now a test-only method
// Preferred to use the singular `getOutputLocations` above.
async getRawOutputPath(data) {
this.bench.get("(count) getRawOutputPath").incrementCount();
let link = await this._getLink(data);

@@ -358,2 +348,3 @@ return link.toOutputPath();

async getOutputHref(data) {
this.bench.get("(count) getOutputHref").incrementCount();
let link = await this._getLink(data);

@@ -365,2 +356,3 @@ return link.toHref();

async getOutputPath(data) {
this.bench.get("(count) getOutputPath").incrementCount();
let link = await this._getLink(data);

@@ -373,33 +365,2 @@ if (await this.usePermalinkRoot()) {

setPaginationData(paginationData) {
this.paginationData = paginationData;
}
async mapDataAsRenderedTemplates(data, templateData) {
// function supported in JavaScript type
if (typeof data === "string" || typeof data === "function") {
debug("rendering data.renderData for %o", this.inputPath);
// bypassMarkdown
let str = await super.render(data, templateData, true);
return str;
} else if (Array.isArray(data)) {
return Promise.all(
data.map((item) => this.mapDataAsRenderedTemplates(item, templateData))
);
} else if (isPlainObject(data)) {
let obj = {};
await Promise.all(
Object.keys(data).map(async (value) => {
obj[value] = await this.mapDataAsRenderedTemplates(
data[value],
templateData
);
})
);
return obj;
}
return data;
}
async _testGetAllLayoutFrontMatterData() {

@@ -415,56 +376,45 @@ let frontMatterData = await this.getFrontMatterData();

async getData() {
if (!this.dataCache) {
debugDev("%o getData()", this.inputPath);
let localData = {};
let globalData = {};
if (this._dataCache) {
return this._dataCache;
}
if (this.templateData) {
localData = await this.templateData.getTemplateDirectoryData(
this.inputPath
);
globalData = await this.templateData.getGlobalData(this.inputPath);
debugDev(
"%o getData() getTemplateDirectoryData and getGlobalData",
this.inputPath
);
}
debugDev("%o getData", this.inputPath);
let localData = {};
let globalData = {};
let frontMatterData = await this.getFrontMatterData();
let layoutKey =
frontMatterData[this.config.keys.layout] ||
localData[this.config.keys.layout] ||
globalData[this.config.keys.layout];
if (this.templateData) {
localData = await this.templateData.getTemplateDirectoryData(this.inputPath);
globalData = await this.templateData.getGlobalData(this.inputPath);
debugDev("%o getData getTemplateDirectoryData and getGlobalData", this.inputPath);
}
// Layout front matter data
let mergedLayoutData = {};
if (layoutKey) {
let layout = this.getLayout(layoutKey);
let frontMatterData = await this.getFrontMatterData();
let layoutKey =
frontMatterData[this.config.keys.layout] ||
localData[this.config.keys.layout] ||
globalData[this.config.keys.layout];
mergedLayoutData = await layout.getData();
debugDev(
"%o getData() get merged layout chain front matter",
this.inputPath
);
}
// Layout front matter data
let mergedLayoutData = {};
if (layoutKey) {
let layout = this.getLayout(layoutKey);
let mergedData = TemplateData.mergeDeep(
this.config,
{},
globalData,
mergedLayoutData,
localData,
frontMatterData
);
mergedData = await this.addPageDate(mergedData);
mergedData = this.addPageData(mergedData);
debugDev("%o getData() mergedData", this.inputPath);
this.dataCache = mergedData;
mergedLayoutData = await layout.getData();
debugDev("%o getData merged layout chain front matter", this.inputPath);
}
// Don’t deep merge pagination data! See https://github.com/11ty/eleventy/issues/147#issuecomment-440802454
return Object.assign(
TemplateData.mergeDeep(this.config, {}, this.dataCache),
this.paginationData
let mergedData = TemplateData.mergeDeep(
this.config,
{},
globalData,
mergedLayoutData,
localData,
frontMatterData
);
mergedData = await this.addPageDate(mergedData);
mergedData = this.addPageData(mergedData);
debugDev("%o getData mergedData", this.inputPath);
this._dataCache = mergedData;
return mergedData;
}

@@ -501,2 +451,5 @@

data.page.outputFileExtension = this.engine.defaultTemplateFileExtension;
data.page.templateSyntax = this.templateRender.getEnginesList(
data[this.config.keys.engineOverride]
);

@@ -506,2 +459,3 @@ return data;

// TODO This isn’t used any more, see `renderPageEntry`
async renderLayout(tmpl, tmplData) {

@@ -512,22 +466,24 @@ let layoutKey = tmplData[tmpl.config.keys.layout];

// TODO reuse templateContent from templateMap
let templateContent = await super.render(
await this.getPreRender(),
tmplData
);
let templateContent = await super.render(await this.getPreRender(), tmplData);
return layout.render(tmplData, templateContent);
}
async _testRenderWithoutLayouts(data) {
this.setWrapWithLayouts(false);
let ret = await this.render(data);
this.setWrapWithLayouts(true);
return ret;
async renderDirect(str, data, bypassMarkdown) {
return super.render(str, data, bypassMarkdown);
}
// Used only by tests
async renderContent(str, data, bypassMarkdown) {
return super.render(str, data, bypassMarkdown);
// This is the primary render mechanism, called via TemplateMap->populateContentDataInMap
async renderWithoutLayout(data) {
if (this._cacheRenderedContent) {
return this._cacheRenderedContent;
}
let content = await this.getPreRender();
let renderedContent = await this.renderDirect(content, data);
this._cacheRenderedContent = renderedContent;
return renderedContent;
}
// TODO This isn’t used any more, see `renderPageEntry`
async render(data) {

@@ -539,15 +495,7 @@ debugDev("%o render()", this.inputPath);

if (!this.wrapWithLayouts && data[this.config.keys.layout]) {
debugDev("Template.render is bypassing layouts for %o.", this.inputPath);
}
if (this.wrapWithLayouts && data[this.config.keys.layout]) {
debugDev(
"Template.render found layout: %o",
data[this.config.keys.layout]
);
if (data[this.config.keys.layout]) {
return this.renderLayout(this, data);
} else {
debugDev("Template.render renderContent for %o", this.inputPath);
return super.render(await this.getPreRender(), data);
debugDev("Template.render renderDirect for %o", this.inputPath);
return this.renderWithoutLayout(data);
}

@@ -560,3 +508,6 @@ }

async runLinters(str, inputPath, outputPath) {
async runLinters(str, page) {
let { inputPath, outputPath, url } = page;
let pageData = page.data.page;
for (let linter of this.linters) {

@@ -568,2 +519,4 @@ // these can be asynchronous but no guarantee of order when they run

outputPath,
url,
page: pageData,
},

@@ -584,3 +537,6 @@ str,

async runTransforms(str, inputPath, outputPath) {
async runTransforms(str, page) {
let { inputPath, outputPath, url } = page;
let pageData = page.data.page;
for (let { callback, name } of this.transforms) {

@@ -593,2 +549,4 @@ try {

outputPath,
url,
page: pageData,
},

@@ -625,8 +583,3 @@ str,

keys.push(key);
this._addComputedEntry(
computedData,
obj[key],
keys.join("."),
declaredDependencies
);
this._addComputedEntry(computedData, obj[key], keys.join("."), declaredDependencies);
}

@@ -637,3 +590,3 @@ } else if (typeof obj === "string") {

async (innerData) => {
return await this.renderComputedData(obj, innerData, true);
return await this.renderComputedData(obj, innerData);
},

@@ -650,9 +603,10 @@ declaredDependencies,

async addComputedData(data) {
// will _not_ consume renderData
this.computedData = new ComputedData(this.config);
if (this.config.keys.computed in data) {
this.computedData = new ComputedData(this.config);
if (this.config.keys.computed in data) {
// Note that `permalink` is only a thing that gets consumed—it does not go directly into generated data
// this allows computed entries to use page.url or page.outputPath and they’ll be resolved properly
// TODO Room for optimization here—we don’t need to recalculate `getOutputHref` and `getOutputPath`
// TODO Why are these using addTemplateString instead of add
this.computedData.addTemplateString(

@@ -673,6 +627,3 @@ "page.url",

// actually add the computed data
this._addComputedEntry(
this.computedData,
data[this.config.keys.computed]
);
this._addComputedEntry(this.computedData, data[this.config.keys.computed]);

@@ -694,2 +645,7 @@ // limited run of computed data—save the stuff that relies on collections for later.

// pagination will already have these set via Pagination->getPageTemplates
if (data.page.url && data.page.outputPath) {
return;
}
let { href, path } = await this.getOutputLocations(data);

@@ -699,15 +655,61 @@ data.page.url = href;

}
}
// Deprecated, use eleventyComputed instead.
if ("renderData" in data) {
data.renderData = await this.mapDataAsRenderedTemplates(
data.renderData,
data
);
// Computed data consuming collections!
async resolveRemainingComputedData(data) {
// If it doesn’t exist, computed data is not used for this template
if (this.computedData) {
debug("Second round of computed data for %o", this.inputPath);
await this.computedData.processRemainingData(data);
}
}
async resolveRemainingComputedData(data) {
debug("Second round of computed data for %o", this.inputPath);
await this.computedData.processRemainingData(data);
static augmentWithTemplateContentProperty(obj) {
return Object.defineProperties(obj, {
checkTemplateContent: {
enumerable: false,
writable: true,
value: true,
},
_templateContent: {
enumerable: false,
writable: true,
value: undefined,
},
templateContent: {
enumerable: true,
set(content) {
if (content === undefined) {
this.checkTemplateContent = false;
}
this._templateContent = content;
},
get() {
if (this.checkTemplateContent && this._templateContent === undefined) {
if (this.template.behavior.isRenderable()) {
// should at least warn here
throw new TemplateContentPrematureUseError(
`Tried to use templateContent too early on ${this.inputPath}${
this.pageNumber ? ` (page ${this.pageNumber})` : ""
}`
);
} else {
throw new TemplateContentUnrenderedTemplateError(
`Tried to use templateContent on unrendered template. You need a valid permalink (or permalink object) to use templateContent on ${
this.inputPath
}${this.pageNumber ? ` (page ${this.pageNumber})` : ""}`
);
}
}
return this._templateContent;
},
},
// Alias for templateContent for consistency
content: {
enumerable: true,
get() {
return this.templateContent;
},
},
});
}

@@ -717,99 +719,48 @@

// no pagination with permalink.serverless
let hasPagination = Pagination.hasPagination(data);
if (!hasPagination) {
if (!Pagination.hasPagination(data)) {
await this.addComputedData(data);
return [
{
template: this,
inputPath: this.inputPath,
fileSlug: this.fileSlugStr,
filePathStem: this.filePathStem,
data: data,
date: data.page.date,
outputPath: data.page.outputPath,
url: data.page.url,
checkTemplateContent: true,
set templateContent(content) {
if (content === undefined) {
this.checkTemplateContent = false;
}
this._templateContent = content;
},
get templateContent() {
if (
this.checkTemplateContent &&
this._templateContent === undefined
) {
if (this.template.behavior.isRenderable()) {
// should at least warn here
throw new TemplateContentPrematureUseError(
`Tried to use templateContent too early (${this.inputPath})`
);
} else {
throw new TemplateContentUnrenderedTemplateError(
`Tried to use templateContent on unrendered template. You need a valid permalink (or permalink object) to use templateContent on ${this.inputPath}`
);
}
}
return this._templateContent;
},
},
];
let obj = {
template: this, // not on the docs but folks are relying on it
data,
page: data.page,
inputPath: this.inputPath,
fileSlug: this.fileSlugStr,
filePathStem: this.filePathStem,
date: data.page.date,
outputPath: data.page.outputPath,
url: data.page.url,
};
obj = Template.augmentWithTemplateContentProperty(obj);
return [obj];
} else {
// needs collections for pagination items
// but individual pagination entries won’t be part of a collection
this.paging = new Pagination(data, this.config);
this.paging.setTemplate(this);
this.paging = new Pagination(this, data, this.config);
let pageTemplates = await this.paging.getPageTemplates();
return await Promise.all(
pageTemplates.map(async (page, pageNumber) => {
// TODO get smarter with something like Object.assign(data, override);
let pageData = Object.assign({}, await page.getData());
pageTemplates.map(async (pageEntry, pageNumber) => {
await pageEntry.template.addComputedData(pageEntry.data);
await page.addComputedData(pageData);
let obj = {
template: pageEntry.template, // not on the docs but folks are relying on it
pageNumber: pageNumber,
data: pageEntry.data,
// Issue #115
if (data.collections) {
pageData.collections = data.collections;
}
return {
template: page,
page: pageEntry.data.page,
inputPath: this.inputPath,
fileSlug: this.fileSlugStr,
filePathStem: this.filePathStem,
data: pageData,
date: pageData.page.date,
pageNumber: pageNumber,
outputPath: pageData.page.outputPath,
url: pageData.page.url,
checkTemplateContent: true,
set templateContent(content) {
if (content === undefined) {
this.checkTemplateContent = false;
}
this._templateContent = content;
},
get templateContent() {
if (
this.checkTemplateContent &&
this._templateContent === undefined
) {
if (this.template.behavior.isRenderable()) {
throw new TemplateContentPrematureUseError(
`Tried to use templateContent too early (${this.inputPath} page ${this.pageNumber})`
);
} else {
throw new TemplateContentUnrenderedTemplateError(
`Tried to use templateContent on unrendered template. You need a valid permalink (or permalink object) to use templateContent on ${this.inputPath} page ${this.pageNumber}`
);
}
}
return this._templateContent;
},
date: pageEntry.data.page.date,
outputPath: pageEntry.data.page.outputPath,
url: pageEntry.data.page.url,
};
obj = Template.augmentWithTemplateContentProperty(obj);
return obj;
})

@@ -820,22 +771,3 @@ );

// TODO move this into tests (this is only used by tests)
async getRenderedTemplates(data) {
let pages = await this.getTemplates(data);
await Promise.all(
pages.map(async (page) => {
let content = await page.template.render(page.data);
page.templateContent = content;
})
);
return pages;
}
async _write(outputPath, finalContent) {
let shouldWriteFile = true;
if (this.isDryRun) {
shouldWriteFile = false;
}
async _write({ url, outputPath, data }, finalContent) {
let lang = {

@@ -846,44 +778,52 @@ start: "Writing",

if (!shouldWriteFile) {
lang = {
start: "Skipping",
finished: "", // not used, promise doesn’t resolve
};
if (!this.isDryRun) {
let engineList = this.templateRender.getReadableEnginesListDifferingFromFileExtension();
this.logger.log(
`${lang.start} ${outputPath} from ${this.inputPath}${engineList ? ` (${engineList})` : ""}`
);
} else if (this.isDryRun) {
return;
}
let engineList =
this.templateRender.getReadableEnginesListDifferingFromFileExtension();
this.logger.log(
`${lang.start} ${outputPath} from ${this.inputPath}${
engineList ? ` (${engineList})` : ""
}`
);
let templateBenchmark = this.bench.get("Template Write");
templateBenchmark.before();
if (!shouldWriteFile) {
this.skippedCount++;
} else {
let templateBenchmark = this.bench.get("Template Write");
templateBenchmark.before();
// TODO(@zachleat) add a cache to check if this was already created
let templateOutputDir = path.parse(outputPath).dir;
if (templateOutputDir) {
await mkdir(templateOutputDir, { recursive: true });
}
// TODO add a cache to check if this was already created
let templateOutputDir = path.parse(outputPath).dir;
if (templateOutputDir) {
await mkdir(templateOutputDir, { recursive: true });
}
if (!Buffer.isBuffer(finalContent) && typeof finalContent !== "string") {
throw new Error(
`The return value from the render function for the ${this.engine.name} template was not a String or Buffer. Received ${finalContent}`
);
}
if (!Buffer.isBuffer(finalContent) && typeof finalContent !== "string") {
throw new Error(
`The return value from the render function for the ${this.engine.name} template was not a String or Buffer. Received ${finalContent}`
);
return writeFile(outputPath, finalContent).then(() => {
templateBenchmark.after();
this.writeCount++;
debug(`${outputPath} ${lang.finished}.`);
let ret = {
inputPath: this.inputPath,
outputPath: outputPath,
url,
content: finalContent,
};
if (data && this.config.dataFilterSelectors && this.config.dataFilterSelectors.size > 0) {
ret.data = this.retrieveDataForJsonOutput(data, this.config.dataFilterSelectors);
}
return writeFile(outputPath, finalContent).then(() => {
templateBenchmark.after();
this.writeCount++;
debug(`${outputPath} ${lang.finished}.`);
});
}
return ret;
});
}
async renderPageEntry(mapEntry, page) {
// cache with transforms output
if (this._cacheFinalContent) {
return this._cacheFinalContent;
}
let content;

@@ -899,8 +839,7 @@ let layoutKey = mapEntry.data[this.config.keys.layout];

await this.runLinters(content, page.inputPath, page.outputPath);
content = await this.runTransforms(
content,
page.inputPath,
page.outputPath
);
await this.runLinters(content, page);
content = await this.runTransforms(content, page);
this._cacheFinalContent = content;
return content;

@@ -936,10 +875,4 @@ }

if (
this.config.dataFilterSelectors &&
this.config.dataFilterSelectors.size > 0
) {
obj.data = this.retrieveDataForJsonOutput(
page.data,
this.config.dataFilterSelectors
);
if (this.config.dataFilterSelectors && this.config.dataFilterSelectors.size > 0) {
obj.data = this.retrieveDataForJsonOutput(page.data, this.config.dataFilterSelectors);
}

@@ -977,3 +910,3 @@

if (content !== undefined) {
return this._write(page.outputPath, content);
return this._write(page, content);
}

@@ -984,4 +917,4 @@ })

// TODO this but better
clone() {
// TODO do we need to even run the constructor here or can we simplify it even more
let tmpl = new Template(

@@ -996,15 +929,6 @@ this.inputPath,

// Avoid re-reads, especially for pagination
tmpl.setInputContent(this.inputContent);
tmpl.logger = this.logger;
for (let transform of this.transforms) {
tmpl.addTransform(transform.name, transform.callback);
// preserves caches too, e.g. _frontMatterDataCache
for (let key in this) {
tmpl[key] = this[key];
}
for (let linter of this.linters) {
tmpl.addLinter(linter);
}
tmpl.setIsVerbose(this.isVerbose);
tmpl.setDryRun(this.isDryRun);

@@ -1018,6 +942,2 @@ return tmpl;

getSkippedCount() {
return this.skippedCount;
}
async getInputFileStat() {

@@ -1059,7 +979,3 @@ if (this._stats) {

if ("date" in data && data.date) {
debug(
"getMappedDate: using a date in the data for %o of %o",
this.inputPath,
data.date
);
debug("getMappedDate: using a date in the data for %o of %o", this.inputPath, data.date);
if (data.date instanceof Date) {

@@ -1079,3 +995,3 @@ // YAML does its own date parsing

// return now if this file is not yet available in `git`
return Date.now();
return new Date();
}

@@ -1085,2 +1001,11 @@ if (data.date.toLowerCase() === "last modified") {

}
if (data.date.toLowerCase() === "git created") {
let d = getDateFromGitFirstAdded(this.inputPath);
if (d) {
return d;
}
// return now if this file is not yet available in `git`
return new Date();
}
if (data.date.toLowerCase() === "created") {

@@ -1093,12 +1018,5 @@ return this._getDateInstance("birthtimeMs");

if (!date.isValid) {
throw new Error(
`date front matter value (${data.date}) is invalid for ${this.inputPath}`
);
throw new Error(`date front matter value (${data.date}) is invalid for ${this.inputPath}`);
}
debug(
"getMappedDate: Luxon parsed %o: %o and %o",
data.date,
date,
date.toJSDate()
);
debug("getMappedDate: Luxon parsed %o: %o and %o", data.date, date, date.toJSDate());

@@ -1109,2 +1027,3 @@ return date.toJSDate();

if (filepathRegex !== null) {
// if multiple are found in the path, use the first one for the date
let dateObj = DateTime.fromISO(filepathRegex[1], {

@@ -1126,17 +1045,6 @@ zone: "utc",

/* This is the primary render mechanism, called via TemplateMap->populateContentDataInMap */
async getTemplateMapContent(pageMapEntry) {
pageMapEntry.template.setWrapWithLayouts(false);
let content = await pageMapEntry.template.render(pageMapEntry.data);
pageMapEntry.template.setWrapWithLayouts(true);
return content;
}
async getTemplateMapEntries(dataOverride) {
// Important reminder: Template data is first generated in TemplateMap
async getTemplateMapEntries(data) {
debugDev("%o getMapped()", this.inputPath);
// Important reminder: This is where the template data is first generated via TemplateMap
let data = dataOverride || (await this.getData());
this.behavior.setRenderViaDataCascade(data);

@@ -1154,25 +1062,4 @@

}
async _testCompleteRender() {
let entries = await this.getTemplateMapEntries();
let nestedContent = await Promise.all(
entries.map(async (entry) => {
entry._pages = await entry.template.getTemplates(entry.data);
return Promise.all(
entry._pages.map(async (page) => {
page.templateContent = await entry.template.getTemplateMapContent(
page
);
return this.renderPageEntry(entry, page);
})
);
})
);
let contents = [].concat(...nestedContent);
return contents;
}
}
module.exports = Template;

@@ -1,2 +0,2 @@

const isPlainObject = require("./Util/IsPlainObject");
const { isPlainObject } = require("@11ty/eleventy-utils");

@@ -15,5 +15,9 @@ class TemplateBehavior {

setRenderableOverride(renderableOverride) {
this.renderableOverride = renderableOverride;
}
// permalink *has* a build key or output is json/ndjson
isRenderable() {
return this.render || this.isRenderForced();
return this.renderableOverride ?? (this.render || this.isRenderForced());
}

@@ -20,0 +24,0 @@

@@ -0,1 +1,5 @@

const { TemplatePath } = require("@11ty/eleventy-utils");
const eventBus = require("./EventBus");
// Note: this is only used for TemplateLayout right now but could be used for more

@@ -7,2 +11,3 @@ // Just be careful because right now the TemplateLayout cache keys are not directly mapped to paths

this.cache = {};
this.cacheByInputPath = {};
}

@@ -12,10 +17,35 @@

this.cache = {};
this.cacheByInputPath = {};
}
size() {
return Object.keys(this.cache).length;
return Object.keys(this.cacheByInputPath).length;
}
add(key, template) {
this.cache[key] = template;
add(layoutTemplate) {
let keys = new Set();
if (typeof layoutTemplate === "string") {
throw new Error(
"Invalid argument type passed to TemplateCache->add(). Should be a TemplateLayout."
);
}
if ("getFullKey" in layoutTemplate) {
keys.add(layoutTemplate.getFullKey());
}
if ("getKey" in layoutTemplate) {
// if `key` was an alias, also set to the pathed layout value too
// e.g. `layout: "default"` and `layout: "default.liquid"` will both map to the same template.
keys.add(layoutTemplate.getKey());
}
for (let key of keys) {
this.cache[key] = layoutTemplate;
}
// also the full template input path for use with eleventy --serve/--watch e.g. `_includes/default.liquid` (see `remove` below)
let fullPath = TemplatePath.stripLeadingDotSlash(layoutTemplate.inputPath);
this.cacheByInputPath[fullPath] = layoutTemplate;
}

@@ -35,10 +65,28 @@

remove(key) {
if (this.cache[key]) {
remove(filePath) {
filePath = TemplatePath.stripLeadingDotSlash(filePath);
if (!this.cacheByInputPath[filePath]) {
return;
}
let layoutTemplate = this.cacheByInputPath[filePath];
layoutTemplate.resetCaches();
let keys = layoutTemplate.getCacheKeys();
for (let key of keys) {
delete this.cache[key];
}
delete this.cacheByInputPath[filePath];
}
}
let layoutCache = new TemplateCache();
eventBus.on("eleventy.resourceModified", (path) => {
layoutCache.remove(path);
});
// singleton
module.exports = new TemplateCache();
module.exports = layoutCache;

@@ -12,10 +12,2 @@ const multimatch = require("multimatch");

// TODO move this into tests (this is only used by tests)
async _testAddTemplate(template) {
let data = await template.getData();
for (let map of await template.getTemplates(data)) {
this.add(map);
}
}
getAll() {

@@ -22,0 +14,0 @@ return this.items.slice();

const fs = require("fs");
const chalk = require("kleur");
const lodashUniq = require("lodash/uniq");
const lodashMerge = require("lodash/merge");
const { TemplatePath } = require("@11ty/eleventy-utils");
const EleventyBaseError = require("./EleventyBaseError");
const UserConfig = require("./UserConfig");
const EleventyBaseError = require("./EleventyBaseError.js");
const UserConfig = require("./UserConfig.js");
const GlobalDependencyMap = require("./GlobalDependencyMap.js");
const { EleventyRequire } = require("./Util/Require.js");
const merge = require("./Util/Merge.js");
const unique = require("./Util/Unique");
const eventBus = require("./EventBus.js");
const debug = require("debug")("Eleventy:TemplateConfig");
const debugDev = require("debug")("Dev:Eleventy:TemplateConfig");
const deleteRequireCache = require("./Util/DeleteRequireCache");

@@ -55,3 +60,12 @@ /**

*/
this.projectConfigPath = projectConfigPath || ".eleventy.js";
if (projectConfigPath !== undefined) {
if (!projectConfigPath) {
// falsy skips config files
this.projectConfigPaths = [];
} else {
this.projectConfigPaths = [projectConfigPath];
}
} else {
this.projectConfigPaths = [".eleventy.js", "eleventy.config.js", "eleventy.config.cjs"];
}

@@ -84,5 +98,17 @@ if (customRootConfig) {

getLocalProjectConfigFile() {
return TemplatePath.addLeadingDotSlash(this.projectConfigPath);
let configFiles = this.getLocalProjectConfigFiles();
// Add the configFiles[0] in case of a test, where no file exists on the file system
let configFile = configFiles.find((path) => path && fs.existsSync(path)) || configFiles[0];
if (configFile) {
return configFile;
}
}
getLocalProjectConfigFiles() {
if (this.projectConfigPaths && this.projectConfigPaths.length > 0) {
return TemplatePath.addLeadingDotSlashArray(this.projectConfigPaths.filter((path) => path));
}
return [];
}
get inputDir() {

@@ -103,4 +129,7 @@ return this._inputDir;

this.initializeRootConfig();
this.forceReloadConfig();
this.usesGraph.reset();
this.config = this.mergeConfig();
// Clear the compile cache
eventBus.emit("eleventy.compileCacheReset");
}

@@ -118,2 +147,10 @@

/**
* Force a reload of the configuration object.
*/
forceReloadConfig() {
this.hasConfigMerged = false;
this.getConfig();
}
/**
* Returns the config object.

@@ -138,11 +175,12 @@ *

setProjectConfigPath(path) {
this.projectConfigPath = path;
if (path !== undefined) {
this.projectConfigPaths = [path];
} else {
this.projectConfigPaths = [];
}
if (this.hasConfigMerged) {
// merge it again
debugDev(
"Merging in getConfig again after setting the local project config path."
);
this.hasConfigMerged = false;
this.getConfig();
debugDev("Merging in getConfig again after setting the local project config path.");
this.forceReloadConfig();
}

@@ -157,9 +195,23 @@ }

setPathPrefix(pathPrefix) {
debug("Setting pathPrefix to %o", pathPrefix);
this.overrides.pathPrefix = pathPrefix;
if (pathPrefix && pathPrefix !== "/") {
debug("Setting pathPrefix to %o", pathPrefix);
this.overrides.pathPrefix = pathPrefix;
}
}
/**
* Gets the current path prefix denoting the root folder the output will be deployed to
*
* @returns {String} - The path prefix string
*/
getPathPrefix() {
if (this.overrides.pathPrefix) {
return this.overrides.pathPrefix;
}
if (!this.hasConfigMerged) {
this.getConfig();
}
this.config.pathPrefix = pathPrefix;
return this.config.pathPrefix;
}

@@ -180,2 +232,11 @@

/*
* Add additional overrides to the root config object, used for testing
*
* @param {Object} - a subset of the return Object from the user’s config file.
*/
appendToRootConfig(obj) {
Object.assign(this.rootConfig, obj);
}
/*
* Process the userland plugins from the Config

@@ -185,4 +246,5 @@ *

*/
processPlugins({ dir }) {
processPlugins({ dir, pathPrefix }) {
this.userConfig.dir = dir;
this.userConfig.pathPrefix = pathPrefix;

@@ -203,5 +265,3 @@ if (this.logger) {

let name = this.userConfig._getPluginName(plugin);
let namespaces = [storedActiveNamespace, pluginNamespace].filter(
(entry) => !!entry
);
let namespaces = [storedActiveNamespace, pluginNamespace].filter((entry) => !!entry);

@@ -214,5 +274,3 @@ let namespaceStr = "";

throw new EleventyPluginError(
`Error processing ${
name ? `the \`${name}\`` : "a"
} plugin${namespaceStr}`,
`Error processing ${name ? `the \`${name}\`` : "a"} plugin${namespaceStr}`,
e

@@ -227,19 +285,15 @@ );

/**
* Merges different config files together.
* Fetches and executes the local configuration file
*
* @param {String} projectConfigPath - Path to project config.
* @returns {{}} merged - The merged config file.
* @returns {{}} merged - The merged config file object.
*/
mergeConfig() {
requireLocalConfigFile() {
let localConfig = {};
let path = TemplatePath.absolutePath(this.projectConfigPath);
let path = this.projectConfigPaths.filter((path) => path).find((path) => fs.existsSync(path));
debug(`Merging config with ${path}`);
if (fs.existsSync(path)) {
if (path) {
try {
// remove from require cache so it will grab a fresh copy
deleteRequireCache(path);
localConfig = require(path);
localConfig = EleventyRequire(path);
// debug( "localConfig require return value: %o", localConfig );

@@ -250,6 +304,3 @@ if (typeof localConfig === "function") {

if (
typeof localConfig === "object" &&
typeof localConfig.then === "function"
) {
if (typeof localConfig === "object" && typeof localConfig.then === "function") {
throw new EleventyConfigError(

@@ -285,47 +336,99 @@ `Error in your Eleventy config file '${path}': Returning a promise is not yet supported.`

return localConfig;
}
/**
* Merges different config files together.
*
* @param {String} projectConfigPath - Path to project config.
* @returns {{}} merged - The merged config file.
*/
mergeConfig() {
let localConfig = this.requireLocalConfigFile();
// Template Formats:
// 1. Root Config (usually defaultConfig.js)
// 2. Local Config return object (project .eleventy.js)
// 3.
let templateFormats = this.rootConfig.templateFormats || [];
if (localConfig && localConfig.templateFormats) {
templateFormats = localConfig.templateFormats;
delete localConfig.templateFormats;
}
let mergedConfig = merge({}, this.rootConfig, localConfig);
// Setup a few properties for plugins:
// Setup pathPrefix set via command line for plugin consumption
if (this.overrides.pathPrefix) {
mergedConfig.pathPrefix = this.overrides.pathPrefix;
}
// Returning a falsy value (e.g. "") from user config should reset to the default value.
if (!mergedConfig.pathPrefix) {
mergedConfig.pathPrefix = this.rootConfig.pathPrefix;
}
// Delay processing plugins until after the result of localConfig is returned
// But BEFORE the rest of the config options are merged
// this way we can pass directories and other template information to plugins
this.processPlugins(localConfig || {});
let eleventyConfigApiMergingObject =
this.userConfig.getMergingConfigObject();
// Temporarily restore templateFormats
mergedConfig.templateFormats = templateFormats;
// remove special merge keys from object
this.processPlugins(mergedConfig);
let savedForSpecialMerge = {
templateFormatsAdded: eleventyConfigApiMergingObject.templateFormatsAdded,
};
delete mergedConfig.templateFormats;
let eleventyConfigApiMergingObject = this.userConfig.getMergingConfigObject();
// `templateFormats` is an override via `setTemplateFormats`
// `templateFormatsAdded` is additive via `addTemplateFormats`
if (eleventyConfigApiMergingObject && eleventyConfigApiMergingObject.templateFormats) {
templateFormats = eleventyConfigApiMergingObject.templateFormats;
delete eleventyConfigApiMergingObject.templateFormats;
}
let templateFormatsAdded = eleventyConfigApiMergingObject.templateFormatsAdded || [];
delete eleventyConfigApiMergingObject.templateFormatsAdded;
localConfig = lodashMerge(localConfig, eleventyConfigApiMergingObject);
templateFormats = unique([...templateFormats, ...templateFormatsAdded]);
// blow away any templateFormats set in config return object and prefer those set in config API.
localConfig.templateFormats =
eleventyConfigApiMergingObject.templateFormats ||
localConfig.templateFormats;
merge(mergedConfig, eleventyConfigApiMergingObject);
// debug("this.userConfig.getMergingConfigObject: %o", this.userConfig.getMergingConfigObject());
debug("localConfig: %o", localConfig);
// Apply overrides, currently only pathPrefix uses this I think!
debug("overrides: %o", this.overrides);
merge(mergedConfig, this.overrides);
// Object assign overrides original values (good only for templateFormats) but not good for anything else
let merged = lodashMerge({}, this.rootConfig, localConfig, this.overrides);
// blow away any templateFormats upstream (don’t deep merge)
merged.templateFormats =
localConfig.templateFormats || this.rootConfig.templateFormats;
// Restore templateFormats
mergedConfig.templateFormats = templateFormats;
// Additive should preserve original templateFormats, wherever those come from (config API or config return object)
if (savedForSpecialMerge.templateFormatsAdded) {
merged.templateFormats = merged.templateFormats.concat(
savedForSpecialMerge.templateFormatsAdded
);
debug("Current configuration: %o", mergedConfig);
this.afterConfigMergeActions(mergedConfig);
return mergedConfig;
}
get usesGraph() {
if (!this._usesGraph) {
this._usesGraph = new GlobalDependencyMap();
}
return this._usesGraph;
}
// Unique
merged.templateFormats = lodashUniq(merged.templateFormats);
afterConfigMergeActions(eleventyConfig) {
// Add to the merged config too
eleventyConfig.uses = this.usesGraph;
debug("Current configuration: %o", merged);
// this is used for the layouts event
this.usesGraph.setConfig(eleventyConfig);
}
return merged;
get uses() {
if (!this.usesGraph) {
throw new Error("The Eleventy Global Dependency Graph has not yet been initialized.");
}
return this.usesGraph;
}

@@ -332,0 +435,0 @@ }

@@ -7,3 +7,3 @@ const os = require("os");

const matter = require("gray-matter");
const lodashSet = require("lodash/set");
const lodashSet = require("lodash.set");
const { TemplatePath } = require("@11ty/eleventy-utils");

@@ -29,5 +29,3 @@

if (!config) {
throw new TemplateContentConfigError(
"Missing `config` argument to TemplateContent"
);
throw new TemplateContentConfigError("Missing `config` argument to TemplateContent");
}

@@ -45,2 +43,33 @@ this.config = config;

getResetTypes(types) {
if (types) {
return Object.assign(
{
data: false,
read: false,
render: false,
},
types
);
}
return {
data: true,
read: true,
render: true,
};
}
// Called during an incremental build when the template instance is cached but needs to be reset because it has changed
resetCaches(types) {
types = this.getResetTypes(types);
if (types.read) {
delete this.readingPromise;
delete this.inputContent;
delete this.frontMatter;
delete this._frontMatterDataCache;
}
}
/* Used by tests */

@@ -77,5 +106,3 @@ get extensionMap() {

}
throw new TemplateContentConfigError(
"Tried to get an eleventyConfig but none was found."
);
throw new TemplateContentConfigError("Tried to get an eleventyConfig but none was found.");
}

@@ -89,7 +116,3 @@

if (!this._templateRender) {
this._templateRender = new TemplateRender(
this.inputPath,
this.inputDir,
this.config
);
this._templateRender = new TemplateRender(this.inputPath, this.inputDir, this.config);
this._templateRender.extensionMap = this.extensionMap;

@@ -101,2 +124,20 @@ }

// For monkey patchers
get frontMatter() {
if (this.frontMatterOverride) {
return this.frontMatterOverride;
} else if (this._frontMatter) {
return this._frontMatter;
} else {
throw new Error(
"Unfortunately you’re using code that monkey patched some Eleventy internals and it isn’t async-friendly."
);
}
}
// For monkey patchers
set frontMatter(contentOverride) {
this.frontMatterOverride = contentOverride;
}
getInputPath() {

@@ -111,44 +152,65 @@ return this.inputPath;

async read() {
if (this.inputContent) {
await this.inputContent;
} else {
this.inputContent = await this.getInputContent();
}
if (!this.readingPromise) {
if (!this.inputContent) {
// cache the promise
this.inputContent = this.getInputContent();
}
if (this.inputContent) {
let options = this.config.frontMatterParsingOptions || {};
let fm;
try {
fm = matter(this.inputContent, options);
} catch (e) {
throw new TemplateContentFrontMatterError(
`Having trouble reading front matter from template ${this.inputPath}`,
e
);
}
if (options.excerpt && fm.excerpt) {
let excerptString = fm.excerpt + (options.excerpt_separator || "---");
if (fm.content.startsWith(excerptString + os.EOL)) {
// with a newline after excerpt separator
fm.content =
fm.excerpt.trim() +
"\n" +
fm.content.substr((excerptString + os.EOL).length);
} else if (fm.content.startsWith(excerptString)) {
// no newline after excerpt separator
fm.content = fm.excerpt + fm.content.substr(excerptString.length);
this.readingPromise = new Promise(async (resolve, reject) => {
try {
let content = await this.inputContent;
if (content) {
let options = this.config.frontMatterParsingOptions || {};
let fm;
try {
fm = matter(content, options);
} catch (e) {
throw new TemplateContentFrontMatterError(
`Having trouble reading front matter from template ${this.inputPath}`,
e
);
}
if (options.excerpt && fm.excerpt) {
let excerptString = fm.excerpt + (options.excerpt_separator || "---");
if (fm.content.startsWith(excerptString + os.EOL)) {
// with an os-specific newline after excerpt separator
fm.content =
fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + os.EOL).length);
} else if (fm.content.startsWith(excerptString + "\n")) {
// with a newline (\n) after excerpt separator
// This is necessary for some git configurations on windows
fm.content =
fm.excerpt.trim() + "\n" + fm.content.slice((excerptString + 1).length);
} else if (fm.content.startsWith(excerptString)) {
// no newline after excerpt separator
fm.content = fm.excerpt + fm.content.slice(excerptString.length);
}
// alias, defaults to page.excerpt
let alias = options.excerpt_alias || "page.excerpt";
lodashSet(fm.data, alias, fm.excerpt);
}
// For monkey patchers that used `frontMatter` 🤧
// https://github.com/11ty/eleventy/issues/613#issuecomment-999637109
// https://github.com/11ty/eleventy/issues/2710#issuecomment-1373854834
this._frontMatter = fm;
resolve(fm);
} else {
resolve({
data: {},
content: "",
excerpt: "",
});
}
} catch (e) {
reject(e);
}
});
}
// alias, defaults to page.excerpt
let alias = options.excerpt_alias || "page.excerpt";
lodashSet(fm.data, alias, fm.excerpt);
}
this.frontMatter = fm;
} else {
this.frontMatter = {
data: {},
content: "",
excerpt: "",
};
}
return this.readingPromise;
}

@@ -164,3 +226,3 @@

static deleteCached(path) {
static deleteFromInputCache(path) {
this._inputCache.delete(TemplatePath.absolutePath(path));

@@ -178,8 +240,12 @@ }

}
let templateBenchmark = this.bench.get("Template Read");
templateBenchmark.before();
let content;
if (this.config.useTemplateCache) {
content = TemplateContent.getCached(this.inputPath);
}
if (!content) {

@@ -192,2 +258,3 @@ content = await readFile(this.inputPath, "utf8");

}
templateBenchmark.after();

@@ -198,31 +265,32 @@

// This might only be used in tests
async getFrontMatter() {
if (!this.frontMatter) {
await this.read();
}
return this.frontMatter;
let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();
return fm;
}
async getPreRender() {
if (!this.frontMatter) {
await this.read();
}
let fm = this.frontMatterOverride ? this.frontMatterOverride : await this.read();
return this.frontMatter.content;
return fm.content;
}
async getFrontMatterData() {
if (this._frontMatterDataCache) {
return this._frontMatterDataCache;
if (!this._frontMatterDataCache) {
this._frontMatterDataCache = new Promise(async (resolve, reject) => {
try {
let fm = await this.read();
let extraData = await this.engine.getExtraDataFromFile(this.inputPath);
let data = TemplateData.mergeDeep({}, fm.data, extraData);
let cleanedData = TemplateData.cleanupData(data);
resolve(cleanedData);
} catch (e) {
reject(e);
}
});
}
if (!this.frontMatter) {
await this.read();
}
let extraData = await this.engine.getExtraDataFromFile(this.inputPath);
let data = TemplateData.mergeDeep({}, this.frontMatter.data, extraData);
let cleanedData = TemplateData.cleanupData(data);
this._frontMatterDataCache = cleanedData;
return cleanedData;
return this._frontMatterDataCache;
}

@@ -235,10 +303,5 @@

async setupTemplateRender(bypassMarkdown) {
let engineOverride = await this.getEngineOverride();
async setupTemplateRender(engineOverride, bypassMarkdown) {
if (engineOverride !== undefined) {
debugDev(
"%o overriding template engine to use %o",
this.inputPath,
engineOverride
);
debugDev("%o overriding template engine to use %o", this.inputPath, engineOverride);

@@ -251,18 +314,17 @@ this.templateRender.setEngineOverride(engineOverride, bypassMarkdown);

_getCompileCache(str, bypassMarkdown) {
let engineName = this.engine.getName() + "::" + !!bypassMarkdown;
let engineMap = TemplateContent._compileEngineCache.get(engineName);
if (!engineMap) {
engineMap = new Map();
TemplateContent._compileEngineCache.set(engineName, engineMap);
_getCompileCache(str) {
// Caches used to be bifurcated based on engine name, now they’re based on inputPath
let inputPathMap = TemplateContent._compileCache.get(this.inputPath);
if (!inputPathMap) {
inputPathMap = new Map();
TemplateContent._compileCache.set(this.inputPath, inputPathMap);
}
let cacheable = this.engine.cacheable;
let key = this.engine.getCompileCacheKey(str, this.inputPath);
return [cacheable, key, engineMap];
let { useCache, key } = this.engine.getCompileCacheKey(str, this.inputPath);
return [cacheable, key, inputPathMap, useCache];
}
async compile(str, bypassMarkdown) {
await this.setupTemplateRender(bypassMarkdown);
async compile(str, bypassMarkdown, engineOverride) {
await this.setupTemplateRender(engineOverride, bypassMarkdown);

@@ -275,7 +337,3 @@ if (bypassMarkdown && !this.engine.needsCompilation(str)) {

debugDev(
"%o compile() using engine: %o",
this.inputPath,
this.templateRender.engineName
);
debugDev("%o compile() using engine: %o", this.inputPath, this.templateRender.engineName);

@@ -285,12 +343,13 @@ try {

if (this.config.useTemplateCache) {
let [cacheable, key, cache] = this._getCompileCache(
str,
bypassMarkdown
);
let [cacheable, key, cache, useCache] = this._getCompileCache(str);
if (cacheable && key) {
if (cache.has(key)) {
this.bench.get("Template Compile Cache Hit").incrementCount();
if (useCache && cache.has(key)) {
this.bench.get("(count) Template Compile Cache Hit").incrementCount();
return cache.get(key);
}
this.bench.get("(count) Template Compile Cache Miss").incrementCount();
// Compile cache is cleared when the resource is modified (below)
// Compilation is async, so we eagerly cache a Promise that eventually

@@ -320,3 +379,3 @@ // resolves to the compiled function

} catch (e) {
let [cacheable, key, cache] = this._getCompileCache(str, bypassMarkdown);
let [cacheable, key, cache] = this._getCompileCache(str);
if (cacheable && key) {

@@ -334,5 +393,16 @@ cache.delete(key);

getParseForSymbolsFunction(str) {
if ("parseForSymbols" in this.engine) {
let engine = this.engine;
// Don’t use markdown as the engine to parse for symbols
let preprocessorEngine = this.templateRender.getPreprocessorEngine(); // TODO pass in engineOverride here
if (preprocessorEngine && engine.getName() !== preprocessorEngine) {
let replacementEngine = this.templateRender.getEngineByName(preprocessorEngine);
if (replacementEngine) {
engine = replacementEngine;
}
}
if ("parseForSymbols" in engine) {
return () => {
return this.engine.parseForSymbols(str);
return engine.parseForSymbols(str);
};

@@ -364,3 +434,9 @@ }

async renderPermalink(permalink, data) {
this.bench.get("(count) Render Permalink").incrementCount();
this.bench
.get(`(count) > Render Permalink > ${this.inputPath}${this._getPaginationLogSuffix(data)}`)
.incrementCount();
let permalinkCompilation = this.engine.permalinkNeedsCompilation(permalink);
// No string compilation:

@@ -376,3 +452,3 @@ // ({ compileOptions: { permalink: "raw" }})

/* Usage:
/* Custom `compile` function for permalinks, usage:
permalink: function(permalinkString, inputPath) {

@@ -385,9 +461,6 @@ return async function(data) {

if (permalinkCompilation && typeof permalinkCompilation === "function") {
permalink = await this._renderFunction(
permalinkCompilation,
permalink,
this.inputPath
);
permalink = await this._renderFunction(permalinkCompilation, permalink, this.inputPath);
}
// Raw permalink function (in the app code data cascade)
if (typeof permalink === "function") {

@@ -404,2 +477,18 @@ return this._renderFunction(permalink, data);

_getPaginationLogSuffix(data) {
let suffix = [];
if ("pagination" in data) {
suffix.push(" (");
if (data.pagination.pages) {
suffix.push(
`${data.pagination.pages.length} page${data.pagination.pages.length !== 1 ? "s" : ""}`
);
} else {
suffix.push("Pagination");
}
suffix.push(")");
}
return suffix.join("");
}
async _render(str, data, bypassMarkdown) {

@@ -411,9 +500,8 @@ try {

let fn = await this.compile(str, bypassMarkdown);
let fn = await this.compile(str, bypassMarkdown, data[this.config.keys.engineOverride]);
if (fn === undefined) {
return;
} else if (typeof fn !== "function") {
throw new Error(
`The \`compile\` function did not return a function. Received ${fn}`
);
throw new Error(`The \`compile\` function did not return a function. Received ${fn}`);
}

@@ -423,23 +511,10 @@

let templateBenchmark = this.bench.get("Render");
let paginationSuffix = [];
if ("pagination" in data) {
paginationSuffix.push(" (Pagination");
if (data.pagination.pages) {
paginationSuffix.push(
`: ${data.pagination.pages.length} page${
data.pagination.pages.length !== 1 ? "s" : ""
}`
);
}
paginationSuffix.push(")");
}
// Skip benchmark for each individual pagination entry (very busy output)
let logRenderToOutputBenchmark = "pagination" in data;
let inputPathBenchmark = this.bench.get(
`> Render > ${this.inputPath}${paginationSuffix.join("")}`
`> Render > ${this.inputPath}${this._getPaginationLogSuffix(data)}`
);
let outputPathBenchmark;
if (data.page && data.page.outputPath) {
outputPathBenchmark = this.bench.get(
`> Render > ${data.page.outputPath}`
);
if (data.page && data.page.outputPath && logRenderToOutputBenchmark) {
outputPathBenchmark = this.bench.get(`> Render to > ${data.page.outputPath}`);
}

@@ -464,6 +539,3 @@

templateBenchmark.after();
debugDev(
"%o getCompiledTemplate called, rendered content created",
this.inputPath
);
debugDev("%o getCompiledTemplate called, rendered content created", this.inputPath);
return rendered;

@@ -475,6 +547,3 @@ } catch (e) {

let engine = this.templateRender.getReadableEnginesList();
debug(
`Having trouble rendering ${engine} template ${this.inputPath}: %O`,
str
);
debug(`Having trouble rendering ${engine} template ${this.inputPath}: %O`, str);
throw new TemplateContentRenderError(

@@ -489,4 +558,3 @@ `Having trouble rendering ${engine} template ${this.inputPath}`,

getExtensionEntries() {
let extensions = this.templateRender.engine.extensionEntries;
return extensions;
return this.engine.extensionEntries;
}

@@ -500,5 +568,14 @@

let extensionEntries = this.getExtensionEntries().filter(
(entry) => !!entry.isIncrementalMatch
let hasDependencies = this.engine.hasDependencies(incrementalFile);
let isRelevant = this.engine.isFileRelevantTo(this.inputPath, incrementalFile);
debug(
"Test dependencies to see if %o is relevant to %o: %o",
this.inputPath,
incrementalFile,
isRelevant
);
let extensionEntries = this.getExtensionEntries().filter((entry) => !!entry.isIncrementalMatch);
if (extensionEntries.length) {

@@ -510,2 +587,5 @@ for (let entry of extensionEntries) {

inputPath: this.inputPath,
isFullTemplate: metadata.isFullTemplate,
isFileRelevantToInputPath: isRelevant,
doesFileHaveDependencies: hasDependencies,
},

@@ -521,12 +601,13 @@ incrementalFile

} else {
// Not great way of building all templates if this is a layout, include, JS dependency.
// TODO improve this for default template syntaxes
// This is the fallback way of determining if something is incremental (no isIncrementalMatch available)
// Not great way of building all templates if this is a layout, include, JS dependency.
// TODO improve this for default langs
if (!metadata.isFullTemplate) {
// This will be true if the inputPath and incrementalFile are the same
if (isRelevant) {
return true;
}
// only build if this input path is the same as the file that was changed
if (this.inputPath === incrementalFile) {
// only return true here if dependencies are not known
if (!hasDependencies && !metadata.isFullTemplate) {
return true;

@@ -541,7 +622,20 @@ }

TemplateContent._inputCache = new Map();
TemplateContent._compileEngineCache = new Map();
TemplateContent._compileCache = new Map();
eventBus.on("eleventy.resourceModified", (path) => {
TemplateContent.deleteCached(path);
// delete from input cache
TemplateContent.deleteFromInputCache(path);
// delete from compile cache
let normalized = TemplatePath.addLeadingDotSlash(path);
let compileCache = TemplateContent._compileCache.get(normalized);
if (compileCache) {
compileCache.clear();
}
});
// Used when the configuration file reset https://github.com/11ty/eleventy/issues/2147
eventBus.on("eleventy.compileCacheReset", (path) => {
TemplateContent._compileCache = new Map();
});
module.exports = TemplateContent;

@@ -1,16 +0,14 @@

const pkg = require("../package.json");
const fs = require("fs");
const fastglob = require("fast-glob");
const path = require("path");
const lodashset = require("lodash/set");
const lodashget = require("lodash/get");
const lodashUniq = require("lodash/uniq");
const semver = require("semver");
const { TemplatePath } = require("@11ty/eleventy-utils");
const lodashset = require("lodash.set");
const lodashget = require("lodash.get");
const { TemplatePath, isPlainObject } = require("@11ty/eleventy-utils");
const merge = require("./Util/Merge");
const TemplateRender = require("./TemplateRender");
const unique = require("./Util/Unique");
const TemplateGlob = require("./TemplateGlob");
const EleventyExtensionMap = require("./EleventyExtensionMap");
const EleventyBaseError = require("./EleventyBaseError");
const TemplateDataInitialGlobalData = require("./TemplateDataInitialGlobalData");
const { EleventyRequire } = require("./Util/Require");

@@ -20,3 +18,2 @@ const debugWarn = require("debug")("Eleventy:Warnings");

const debugDev = require("debug")("Dev:Eleventy:TemplateData");
const deleteRequireCache = require("./Util/DeleteRequireCache");

@@ -58,4 +55,2 @@ class FSExistsCache {

this.dataTemplateEngine = this.config.dataTemplateEngine;
this.inputDirNeedsCheck = false;

@@ -71,4 +66,10 @@ this.setInputDir(inputDir);

this._fsExistsCache = new FSExistsCache();
this.initialGlobalData = new TemplateDataInitialGlobalData(this.eleventyConfig);
}
setFileSystemSearch(fileSystemSearch) {
this.fileSystemSearch = fileSystemSearch;
}
get extensionMap() {

@@ -96,3 +97,2 @@ if (!this._extensionMap) {

this.config = config;
this.dataTemplateEngine = this.config.dataTemplateEngine;
}

@@ -108,6 +108,2 @@

setDataTemplateEngine(engineName) {
this.dataTemplateEngine = engineName;
}
getRawImports() {

@@ -119,6 +115,3 @@ let pkgPath = TemplatePath.absolutePath("package.json");

} catch (e) {
debug(
"Could not find and/or require package.json for data preprocessing at %o",
pkgPath
);
debug("Could not find and/or require package.json for data preprocessing at %o", pkgPath);
}

@@ -135,11 +128,6 @@

this.globalData = null;
this.configApiGlobalData = null;
this.templateDirectoryData = {};
}
async cacheData() {
this.clearData();
return this.getData();
}
_getGlobalDataGlobByExtension(dir, extension) {

@@ -177,25 +165,77 @@ return TemplateGlob.normalizePath(

// This is a backwards compatibility helper with the old `jsDataFileSuffix` configuration API
getDataFileSuffixes() {
// New API
if (Array.isArray(this.config.dataFileSuffixes)) {
return this.config.dataFileSuffixes;
}
// Backwards compatibility
if (this.config.jsDataFileSuffix) {
let suffixes = [];
suffixes.push(this.config.jsDataFileSuffix); // e.g. filename.11tydata.json
suffixes.push(""); // suffix-less for free with old API, e.g. filename.json
return suffixes;
}
return []; // if both of these entries are set to false, use no files
}
// This is used exclusively for --watch and --serve chokidar targets
async getTemplateDataFileGlob() {
let dir = await this.getInputDir();
let paths = [
`${dir}/**/*.json`, // covers .11tydata.json too
`${dir}/**/*${this.config.jsDataFileSuffix}.cjs`,
`${dir}/**/*${this.config.jsDataFileSuffix}.js`,
];
let suffixes = this.getDataFileSuffixes();
let globSuffixesWithLeadingDot = new Set();
globSuffixesWithLeadingDot.add("json"); // covers .11tydata.json too
let globSuffixesWithoutLeadingDot = new Set();
// Typically using [ '.11tydata', '' ] suffixes to find data files
for (let suffix of suffixes) {
// TODO the `suffix` truthiness check is purely for backwards compat?
if (suffix && typeof suffix === "string") {
if (suffix.startsWith(".")) {
// .suffix.js
globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.cjs`);
globSuffixesWithLeadingDot.add(`${suffix.slice(1)}.js`);
} else {
// "suffix.js" without leading dot
globSuffixesWithoutLeadingDot.add(`${suffix || ""}.cjs`);
globSuffixesWithoutLeadingDot.add(`${suffix || ""}.js`);
}
}
}
// Configuration Data Extensions e.g. yaml
if (this.hasUserDataExtensions()) {
let userPaths = this.getUserDataExtensions().map(
(extension) => `${dir}/**/*.${extension}` // covers .11tydata.{extension} too
);
paths = userPaths.concat(paths);
for (let extension of this.getUserDataExtensions()) {
globSuffixesWithLeadingDot.add(extension); // covers .11tydata.{extension} too
}
}
let dir = await this.getInputDir();
let paths = [];
if (globSuffixesWithLeadingDot.size > 0) {
paths.push(`${dir}/**/*.{${Array.from(globSuffixesWithLeadingDot).join(",")}}`);
}
if (globSuffixesWithoutLeadingDot.size > 0) {
paths.push(`${dir}/**/*{${Array.from(globSuffixesWithoutLeadingDot).join(",")}}`);
}
return TemplatePath.addLeadingDotSlashArray(paths);
}
// For spidering dependencies
// TODO Can we reuse getTemplateDataFileGlob instead? Maybe just filter off the .json files before scanning for dependencies
async getTemplateJavaScriptDataFileGlob() {
let dir = await this.getInputDir();
return TemplatePath.addLeadingDotSlashArray([
`${dir}/**/*${this.config.jsDataFileSuffix}.js`,
]);
let paths = [];
let suffixes = this.getDataFileSuffixes();
for (let suffix of suffixes) {
if (suffix) {
// TODO this check is purely for backwards compat and I kinda feel like it shouldn’t be here
// paths.push(`${dir}/**/*${suffix || ""}.cjs`); // Same as above
paths.push(`${dir}/**/*${suffix || ""}.js`);
}
}
return TemplatePath.addLeadingDotSlashArray(paths);
}

@@ -206,4 +246,4 @@

let extGlob = this.getGlobalDataExtensionPriorities().join("|");
return [this._getGlobalDataGlobByExtension(dir, "(" + extGlob + ")")];
let extGlob = this.getGlobalDataExtensionPriorities().join(",");
return [this._getGlobalDataGlobByExtension(dir, "{" + extGlob + "}")];
}

@@ -232,8 +272,6 @@

let fsBench = this.benchmarks.aggregate.get("Searching the file system");
let fsBench = this.benchmarks.aggregate.get("Searching the file system (data)");
fsBench.before();
let paths = fastglob.sync(await this.getGlobalDataGlob(), {
caseSensitiveMatch: false,
dot: true,
});
let globs = await this.getGlobalDataGlob();
let paths = await this.fileSystemSearch.search("global-data", globs);
fsBench.after();

@@ -261,6 +299,3 @@

getObjectPathForDataFile(dataFilePath) {
let reducedPath = TemplatePath.stripLeadingSubPath(
dataFilePath,
this.dataDir
);
let reducedPath = TemplatePath.stripLeadingSubPath(dataFilePath, this.dataDir);
let parsed = path.parse(reducedPath);

@@ -270,3 +305,3 @@ let folders = parsed.dir ? parsed.dir.split("/") : [];

return folders.join(".");
return folders;
}

@@ -277,5 +312,3 @@

let globalData = {};
let files = TemplatePath.addLeadingDotSlashArray(
await this.getGlobalDataFiles()
);
let files = TemplatePath.addLeadingDotSlashArray(await this.getGlobalDataFiles());

@@ -287,10 +320,16 @@ this.config.events.emit("eleventy.globalDataFiles", files);

for (let j = 0, k = files.length; j < k; j++) {
let objectPathTarget = await this.getObjectPathForDataFile(files[j]);
let data = await this.getDataValue(files[j], rawImports);
let objectPathTarget = this.getObjectPathForDataFile(files[j]);
// Since we're joining directory paths and an array is not useable as an objectkey since two identical arrays are not double equal,
// we can just join the array by a forbidden character ("/"" is chosen here, since it works on Linux, Mac and Windows).
// If at some point this isn't enough anymore, it would be possible to just use JSON.stringify(objectPathTarget) since that
// is guaranteed to work but is signifivcantly slower.
let objectPathTargetString = objectPathTarget.join(path.sep);
// if two global files have the same path (but different extensions)
// and conflict, let’s merge them.
if (dataFileConflicts[objectPathTarget]) {
if (dataFileConflicts[objectPathTargetString]) {
debugWarn(
`merging global data from ${files[j]} with an already existing global data file (${dataFileConflicts[objectPathTarget]}). Overriding existing keys.`
`merging global data from ${files[j]} with an already existing global data file (${dataFileConflicts[objectPathTargetString]}). Overriding existing keys.`
);

@@ -302,6 +341,4 @@

dataFileConflicts[objectPathTarget] = files[j];
debug(
`Found global data file ${files[j]} and adding as: ${objectPathTarget}`
);
dataFileConflicts[objectPathTargetString] = files[j];
debug(`Found global data file ${files[j]} and adding as: ${objectPathTarget}`);
lodashset(globalData, objectPathTarget, data);

@@ -314,46 +351,33 @@ }

async getInitialGlobalData() {
let globalData = {};
if (!this.configApiGlobalData) {
this.configApiGlobalData = new Promise(async (resolve) => {
let globalData = await this.initialGlobalData.getData();
// via eleventyConfig.addGlobalData
if (this.config.globalData) {
let keys = Object.keys(this.config.globalData);
for (let key of keys) {
let returnValue = this.config.globalData[key];
if (typeof returnValue === "function") {
returnValue = await returnValue();
if (this.environmentVariables) {
if (!("env" in globalData.eleventy)) {
globalData.eleventy.env = {};
}
Object.assign(globalData.eleventy.env, this.environmentVariables);
}
lodashset(globalData, key, returnValue);
}
resolve(globalData);
});
}
if (!("eleventy" in globalData)) {
globalData.eleventy = {};
}
// #2293 for meta[name=generator]
globalData.eleventy.version = semver.coerce(pkg.version).toString();
globalData.eleventy.generator = `Eleventy v${globalData.eleventy.version}`;
if (this.environmentVariables) {
if (!("env" in globalData.eleventy)) {
globalData.eleventy.env = {};
}
Object.assign(globalData.eleventy.env, this.environmentVariables);
}
return globalData;
return this.configApiGlobalData;
}
async getData() {
async getGlobalData() {
let rawImports = this.getRawImports();
if (!this.globalData) {
this.configApiGlobalData = await this.getInitialGlobalData();
this.globalData = new Promise(async (resolve) => {
let configApiGlobalData = await this.getInitialGlobalData();
let globalJson = await this.getAllGlobalData();
let mergedGlobalData = merge(globalJson, this.configApiGlobalData);
let globalJson = await this.getAllGlobalData();
let mergedGlobalData = merge(globalJson, configApiGlobalData);
// OK: Shallow merge when combining rawImports (pkg) with global data files
this.globalData = Object.assign({}, mergedGlobalData, rawImports);
// OK: Shallow merge when combining rawImports (pkg) with global data files
resolve(Object.assign({}, mergedGlobalData, rawImports));
});
}

@@ -382,8 +406,27 @@

let dataSource = {};
for (let path of localDataPaths) {
// clean up data for template/directory data files only.
let dataForPath = await this.getDataValue(path, null, true);
let cleanedDataForPath = TemplateData.cleanupData(dataForPath);
TemplateData.mergeDeep(this.config, localData, cleanedDataForPath);
// debug("`combineLocalData` (iterating) for %o: %O", path, localData);
if (!isPlainObject(dataForPath)) {
debug(
"Warning: Template and Directory data files expect an object to be returned, instead `%o` returned `%o`",
path,
dataForPath
);
} else {
// clean up data for template/directory data files only.
let cleanedDataForPath = TemplateData.cleanupData(dataForPath);
for (let key in cleanedDataForPath) {
if (dataSource.hasOwnProperty(key)) {
debugWarn(
"Local data files have conflicting data. Overwriting '%s' with data from '%s'. Previous data location was from '%s'",
key,
path,
dataSource[key]
);
}
dataSource[key] = path;
}
TemplateData.mergeDeep(this.config, localData, cleanedDataForPath);
}
}

@@ -398,6 +441,3 @@ return localData;

this.templateDirectoryData[templatePath] = Object.assign(
{},
importedData
);
this.templateDirectoryData[templatePath] = Object.assign({}, importedData);
}

@@ -407,6 +447,2 @@ return this.templateDirectoryData[templatePath];

async getGlobalData() {
return this.getData();
}
getUserDataExtensions() {

@@ -427,5 +463,3 @@ if (!this.config.dataExtensions) {

isUserDataExtension(extension) {
return (
this.config.dataExtensions && this.config.dataExtensions.has(extension)
);
return this.config.dataExtensions && this.config.dataExtensions.has(extension);
}

@@ -437,45 +471,45 @@

async _loadFileContents(path) {
async _loadFileContents(path, options = {}) {
let rawInput;
let encoding = "utf8";
if ("encoding" in options) {
encoding = options.encoding;
}
try {
rawInput = await fs.promises.readFile(path, "utf8");
rawInput = await fs.promises.readFile(path, encoding);
} catch (e) {
// if file does not exist, return nothing
}
// Can return a buffer, string, etc
if (typeof rawInput === "string") {
return rawInput.trim();
}
return rawInput;
}
async _parseDataFile(path, rawImports, ignoreProcessing, parser) {
let rawInput = await this._loadFileContents(path);
let engineName = this.dataTemplateEngine;
async _parseDataFile(path, rawImports, ignoreProcessing, parser, options = {}) {
let readFile = !("read" in options) || options.read === true;
let rawInput;
if (!rawInput) {
if (readFile) {
rawInput = await this._loadFileContents(path, options);
}
if (readFile && !rawInput) {
return {};
}
if (ignoreProcessing || engineName === false) {
try {
return parser(rawInput);
} catch (e) {
throw new TemplateDataParseError(
`Having trouble parsing data file ${path}`,
e
);
try {
if (readFile) {
return parser(rawInput, path);
} else {
// path as a first argument is when `read: false`
// path as a second argument is for consistency with `read: true` API
return parser(path, path);
}
} else {
let tr = new TemplateRender(engineName, this.inputDir, this.config);
tr.extensionMap = this.extensionMap;
let fn = await tr.getCompiledTemplate(rawInput);
try {
// pass in rawImports, don’t pass in global data, that’s what we’re parsing
let raw = await fn(rawImports);
return parser(raw);
} catch (e) {
throw new TemplateDataParseError(
`Having trouble parsing data file ${path}`,
e
);
}
} catch (e) {
throw new TemplateDataParseError(`Having trouble parsing data file ${path}`, e);
}

@@ -489,7 +523,3 @@ }

if (
extension === "js" ||
extension === "cjs" ||
(extension === "json" && (ignoreProcessing || !this.dataTemplateEngine))
) {
if (extension === "js" || extension === "cjs") {
// JS data file or require’d JSON (no preprocessing needed)

@@ -509,5 +539,4 @@ let localPath = TemplatePath.absolutePath(path);

dataBench.before();
deleteRequireCache(localPath);
let returnValue = require(localPath);
let returnValue = EleventyRequire(localPath);
// TODO special exception for Global data `permalink.js`

@@ -517,3 +546,4 @@ // module.exports = (data) => `${data.page.filePathStem}/`; // Does not work

if (typeof returnValue === "function") {
returnValue = await returnValue(this.configApiGlobalData || {});
let configApiGlobalData = await this.getInitialGlobalData();
returnValue = await returnValue(configApiGlobalData || {});
}

@@ -526,12 +556,9 @@

// Other extensions
var parser = this.getUserDataParser(extension);
return this._parseDataFile(path, rawImports, ignoreProcessing, parser);
let { parser, options } = this.getUserDataParser(extension);
return this._parseDataFile(path, rawImports, ignoreProcessing, parser, options);
} else if (extension === "json") {
// File to string, parse with JSON (preprocess)
return this._parseDataFile(
path,
rawImports,
ignoreProcessing,
JSON.parse
);
const parser = (content) => JSON.parse(content);
return this._parseDataFile(path, rawImports, ignoreProcessing, parser);
} else {

@@ -550,16 +577,22 @@ throw new TemplateDataParseError(

_addBaseToPaths(paths, base, extensions) {
let dataSuffix = this.config.jsDataFileSuffix;
_addBaseToPaths(paths, base, extensions, nonEmptySuffixesOnly = false) {
let suffixes = this.getDataFileSuffixes();
// data suffix
paths.push(base + dataSuffix + ".js");
paths.push(base + dataSuffix + ".cjs");
paths.push(base + dataSuffix + ".json");
for (let suffix of suffixes) {
suffix = suffix || "";
// inject user extensions
this._pushExtensionsToPaths(paths, base + dataSuffix, extensions);
if (nonEmptySuffixesOnly && suffix === "") {
continue;
}
// top level
paths.push(base + ".json");
this._pushExtensionsToPaths(paths, base, extensions);
// data suffix
if (suffix) {
paths.push(base + suffix + ".js");
paths.push(base + suffix + ".cjs");
}
paths.push(base + suffix + ".json"); // default: .11tydata.json
// inject user extensions
this._pushExtensionsToPaths(paths, base + suffix, extensions);
}
}

@@ -570,5 +603,3 @@

let parsed = path.parse(templatePath);
let inputDir = TemplatePath.addLeadingDotSlash(
TemplatePath.normalize(this.inputDir)
);
let inputDir = TemplatePath.addLeadingDotSlash(TemplatePath.normalize(this.inputDir));

@@ -581,12 +612,12 @@ debugDev("getLocalDataPaths(%o)", templatePath);

if (parsed.dir) {
let fileNameNoExt = this.extensionMap.removeTemplateExtension(
parsed.base
);
let fileNameNoExt = this.extensionMap.removeTemplateExtension(parsed.base);
// default dataSuffix: .11tydata, is appended in _addBaseToPaths
debug("Using %o suffixes to find data files.", this.getDataFileSuffixes());
// Template data file paths
let filePathNoExt = parsed.dir + "/" + fileNameNoExt;
let dataSuffix = this.config.jsDataFileSuffix;
debug("Using %o to find data files.", dataSuffix);
this._addBaseToPaths(paths, filePathNoExt, userExtensions);
// Directory data file paths
let allDirs = TemplatePath.getAllDirs(parsed.dir);

@@ -602,4 +633,9 @@

}
if (!inputDir || (dir.indexOf(inputDir) === 0 && dir !== inputDir)) {
this._addBaseToPaths(paths, dirPathNoExt, userExtensions);
if (!inputDir || (dir.startsWith(inputDir) && dir !== inputDir)) {
if (this.config.dataFileDirBaseNameOverride) {
let indexDataFile = dir + "/" + this.config.dataFileDirBaseNameOverride;
this._addBaseToPaths(paths, indexDataFile, userExtensions, true);
} else {
this._addBaseToPaths(paths, dirPathNoExt, userExtensions);
}
}

@@ -614,3 +650,11 @@ }

);
if (lastInputDir !== "./") {
// in root input dir, search for index.11tydata.json et al
if (this.config.dataFileDirBaseNameOverride) {
let indexDataFile =
TemplatePath.getDirFromFilePath(lastInputDir) +
"/" +
this.config.dataFileDirBaseNameOverride;
this._addBaseToPaths(paths, indexDataFile, userExtensions, true);
} else if (lastInputDir !== "./") {
this._addBaseToPaths(paths, lastInputDir, userExtensions);

@@ -622,3 +666,3 @@ }

debug("getLocalDataPaths(%o): %o", templatePath, paths);
return lodashUniq(paths).reverse();
return unique(paths).reverse();
}

@@ -639,3 +683,3 @@

static cleanupData(data) {
if ("tags" in data) {
if (isPlainObject(data) && "tags" in data) {
if (typeof data.tags === "string") {

@@ -654,11 +698,5 @@ data.tags = data.tags ? [data.tags] : [];

getServerlessPathData() {
if (
this.configApiGlobalData &&
this.configApiGlobalData.eleventy &&
this.configApiGlobalData.eleventy.serverless &&
this.configApiGlobalData.eleventy.serverless.path
) {
return this.configApiGlobalData.eleventy.serverless.path;
}
async getServerlessPathData() {
let configApiGlobalData = await this.getInitialGlobalData();
return configApiGlobalData?.eleventy?.serverless?.path;
}

@@ -665,0 +703,0 @@ }

@@ -14,2 +14,12 @@ const EleventyBaseError = require("./EleventyBaseError");

static isCustomEngineSimpleAlias(entry) {
let keys = Object.keys(entry);
if (keys.length > 2) {
return false;
}
return !keys.some((key) => {
return key !== "key" && key !== "extension";
});
}
get keyToClassNameMap() {

@@ -30,5 +40,19 @@ if (!this._keyToClassNameMap) {

// Custom entries *can* overwrite default entries above
if ("extensionMap" in this.config) {
for (let entry of this.config.extensionMap) {
this._keyToClassNameMap[entry.key] = "Custom";
// either the key does not already exist or it is not a simple alias and is an override: https://www.11ty.dev/docs/languages/custom/#overriding-an-existing-template-language
if (
!this._keyToClassNameMap[entry.key] ||
!TemplateEngineManager.isCustomEngineSimpleAlias(entry)
) {
// throw an error if you try to override a Custom engine, this is a short term error until we swap this to use the extension instead of the key to get the class
if (this._keyToClassNameMap[entry.key] === "Custom") {
throw new Error(
`An attempt was made to override the *already* overridden "${entry.key}" template syntax via the \`addExtension\` configuration API. A maximum of one override is currently supported. If you’re trying to add an alias to an existing syntax, make sure only the \`key\` property is present in the addExtension options object.`
);
}
this._keyToClassNameMap[entry.key] = "Custom";
}
}

@@ -88,2 +112,4 @@ }

// TODO these cached engines should be based on extensions not name, then we can remove the error in
// "Double override (not aliases) throws an error" test in TemplateRenderCustomTest.js
if (this.engineCache[name]) {

@@ -99,3 +125,3 @@ return this.engineCache[name];

// If the user providers a "Custom" engine using addExtension,
// If provided a "Custom" engine using addExtension,
// But that engine's instance is *not* custom,

@@ -102,0 +128,0 @@ // The user must be overriding an existing engine

@@ -21,2 +21,3 @@ const path = require("path");

// `page.filePathStem` see https://www.11ty.dev/docs/data-eleventy-supplied/#page-variable
getFullPathWithoutExtension() {

@@ -28,2 +29,7 @@ return "/" + TemplatePath.join(...this.dirs, this._getRawSlug());

let slug = this.filenameNoExt;
return this._stripDateFromSlug(slug);
}
/** Removes dates in the format of YYYY-MM-DD from a given slug string candidate. */
_stripDateFromSlug(slug) {
let reg = slug.match(/\d{4}-\d{2}-\d{2}-(.*)/);

@@ -36,2 +42,3 @@ if (reg) {

// `page.fileSlug` see https://www.11ty.dev/docs/data-eleventy-supplied/#page-variable
getSlug() {

@@ -41,3 +48,7 @@ let rawSlug = this._getRawSlug();

if (rawSlug === "index") {
return this.dirs.length ? this.dirs[this.dirs.length - 1] : "";
if (!this.dirs.length) {
return "";
}
let lastDir = this.dirs[this.dirs.length - 1];
return this._stripDateFromSlug(lastDir);
}

@@ -44,0 +55,0 @@

@@ -18,3 +18,3 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

if (path.charAt(0) === "!") {
return "!" + TemplateGlob.normalizePath(path.substr(1));
return "!" + TemplateGlob.normalizePath(path.slice(1));
} else {

@@ -21,0 +21,0 @@ return TemplateGlob.normalizePath(path);

@@ -17,8 +17,4 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

let resolvedPath = new TemplateLayoutPathResolver(
key,
inputDir,
extensionMap,
config
).getFullPath();
let resolver = new TemplateLayoutPathResolver(key, inputDir, extensionMap, config);
let resolvedPath = resolver.getFullPath();

@@ -30,3 +26,5 @@ super(resolvedPath, inputDir, config);

}
this.extensionMap = extensionMap;
this.key = resolver.getNormalizedLayoutKey();
this.dataKeyLayoutPath = key;

@@ -37,2 +35,14 @@ this.inputPath = resolvedPath;

getKey() {
return this.key;
}
getFullKey() {
return TemplateLayout.resolveFullKey(this.dataKeyLayoutPath, this.inputDir);
}
getCacheKeys() {
return new Set([this.dataKeyLayoutPath, this.getFullKey(), this.key]);
}
static resolveFullKey(key, inputDir) {

@@ -43,17 +53,17 @@ return TemplatePath.join(inputDir, key);

static getTemplate(key, inputDir, config, extensionMap) {
if (config.useTemplateCache) {
let fullKey = TemplateLayout.resolveFullKey(key, inputDir);
if (templateCache.has(fullKey)) {
debugDev("Found %o in TemplateCache", key);
return templateCache.get(fullKey);
}
if (!config.useTemplateCache) {
return new TemplateLayout(key, inputDir, extensionMap, config);
}
let tmpl = new TemplateLayout(key, inputDir, extensionMap, config);
let fullKey = TemplateLayout.resolveFullKey(key, inputDir);
if (!templateCache.has(fullKey)) {
let layout = new TemplateLayout(key, inputDir, extensionMap, config);
templateCache.add(layout);
debugDev("Added %o to TemplateCache", key);
templateCache.add(fullKey, tmpl);
return tmpl;
} else {
return new TemplateLayout(key, inputDir, extensionMap, config);
return layout;
}
return templateCache.get(fullKey);
}

@@ -63,5 +73,7 @@

return {
// Used by `TemplateLayout.getTemplate()`
key: this.dataKeyLayoutPath,
inputDir: this.inputDir,
template: this,
// used by `this.getData()`
frontMatterData: await this.getFrontMatterData(),

@@ -72,72 +84,136 @@ };

async getTemplateLayoutMap() {
if (this.mapCache) {
return this.mapCache;
if (!this.cachedLayoutMap) {
this.cachedLayoutMap = new Promise(async (resolve, reject) => {
try {
// For both the eleventy.layouts event and cyclical layout chain checking (e.g., a => b => c => a)
let layoutChain = new Set();
layoutChain.add(this.inputPath);
let cfgKey = this.config.keys.layout;
let map = [];
let mapEntry = await this.getTemplateLayoutMapEntry();
map.push(mapEntry);
while (mapEntry.frontMatterData && cfgKey in mapEntry.frontMatterData) {
// Layout of the current layout
let parentLayoutKey = mapEntry.frontMatterData[cfgKey];
let layout = TemplateLayout.getTemplate(
parentLayoutKey,
mapEntry.inputDir,
this.config,
this.extensionMap
);
// Abort if a circular layout chain is detected. Otherwise, we'll time out and run out of memory.
if (layoutChain.has(layout.inputPath)) {
throw new Error(
`Your layouts have a circular reference, starting at ${map[0].key}! The layout at ${layout.inputPath} was specified twice in this layout chain.`
);
}
// Keep track of this layout so we can detect duplicates in subsequent iterations
layoutChain.add(layout.inputPath);
// reassign for next loop
mapEntry = await layout.getTemplateLayoutMapEntry();
map.push(mapEntry);
}
this.layoutChain = Array.from(layoutChain);
resolve(map);
} catch (e) {
reject(e);
}
});
}
let cfgKey = this.config.keys.layout;
let map = [];
let mapEntry = await this.getTemplateLayoutMapEntry();
map.push(mapEntry);
return this.cachedLayoutMap;
}
while (mapEntry.frontMatterData && cfgKey in mapEntry.frontMatterData) {
let layout = TemplateLayout.getTemplate(
mapEntry.frontMatterData[cfgKey],
mapEntry.inputDir,
this.config,
this.extensionMap
);
mapEntry = await layout.getTemplateLayoutMapEntry();
map.push(mapEntry);
async getLayoutChain() {
if (!Array.isArray(this.layoutChain)) {
await this.getTemplateLayoutMap();
}
this.mapCache = map;
return map;
return this.layoutChain;
}
async getData() {
if (this.dataCache) {
return this.dataCache;
}
if (!this.dataCache) {
this.dataCache = new Promise(async (resolve, reject) => {
try {
let map = await this.getTemplateLayoutMap();
let dataToMerge = [];
for (let j = map.length - 1; j >= 0; j--) {
dataToMerge.push(map[j].frontMatterData);
}
let map = await this.getTemplateLayoutMap();
let dataToMerge = [];
let layoutChain = [];
for (let j = map.length - 1; j >= 0; j--) {
layoutChain.push(map[j].template.inputPath);
dataToMerge.push(map[j].frontMatterData);
// Deep merge of layout front matter
let data = TemplateData.mergeDeep(this.config, {}, ...dataToMerge);
delete data[this.config.keys.layout];
resolve(data);
} catch (e) {
reject(e);
}
});
}
// Deep merge of layout front matter
let data = TemplateData.mergeDeep(this.config, {}, ...dataToMerge);
delete data[this.config.keys.layout];
this.layoutChain = layoutChain.reverse();
this.dataCache = data;
return data;
return this.dataCache;
}
async _testGetLayoutChain() {
if (!this.layoutChain) {
await this.getData();
// Do only cache this layout’s render function and delegate the rest to the other templates.
async getCachedCompiledLayoutFunction() {
if (!this.cachedCompiledLayoutFunction) {
this.cachedCompiledLayoutFunction = new Promise(async (resolve, reject) => {
try {
let rawInput = await this.getPreRender();
let renderFunction = await this.compile(rawInput);
resolve(renderFunction);
} catch (e) {
reject(e);
}
});
}
return this.layoutChain;
return this.cachedCompiledLayoutFunction;
}
async getCompiledLayoutFunctions(layoutMap) {
async getCompiledLayoutFunctions() {
let layoutMap = await this.getTemplateLayoutMap();
let fns = [];
try {
for (let layoutEntry of layoutMap) {
fns.push(
await layoutEntry.template.compile(
await layoutEntry.template.getPreRender()
)
fns.push({
render: await this.getCachedCompiledLayoutFunction(),
});
if (layoutMap.length > 1) {
let [currentLayout, parentLayout] = layoutMap;
let { key, inputDir } = parentLayout;
let layoutTemplate = TemplateLayout.getTemplate(
key,
inputDir,
this.config,
this.extensionMap
);
// The parent already includes the rest of the layout chain
let upstreamFns = await layoutTemplate.getCompiledLayoutFunctions();
for (let j = 0, k = upstreamFns.length; j < k; j++) {
fns.push(upstreamFns[j]);
}
}
return fns;
} catch (e) {
debugDev("Clearing TemplateCache after error.");
templateCache.clear();
return Promise.reject(e);
throw e;
}
return fns;
}

@@ -151,5 +227,2 @@

data.layoutContent = templateContent;
// deprecated
data._layoutContent = templateContent;
}

@@ -162,9 +235,10 @@

// Trouble: layouts may need data variables present downstream/upstream
// This is called from Template->renderPageEntry
async render(data, templateContent) {
data = TemplateLayout.augmentDataWithContent(data, templateContent);
let layoutMap = await this.getTemplateLayoutMap();
let fns = await this.getCompiledLayoutFunctions(layoutMap);
for (let fn of fns) {
templateContent = await fn(data);
let compiledFunctions = await this.getCompiledLayoutFunctions();
for (let { render } of compiledFunctions) {
templateContent = await render(data);
data = TemplateLayout.augmentDataWithContent(data, templateContent);

@@ -175,4 +249,13 @@ }

}
resetCaches(types) {
super.resetCaches(types);
delete this.dataCache;
delete this.layoutChain;
delete this.cachedLayoutMap;
delete this.cachedCompiledLayoutFunction;
}
}
module.exports = TemplateLayout;

@@ -9,5 +9,3 @@ const fs = require("fs");

if (!config) {
throw new Error(
"Expected `config` in TemplateLayoutPathResolver constructor"
);
throw new Error("Expected `config` in TemplateLayoutPathResolver constructor");
}

@@ -21,5 +19,3 @@ this._config = config;

if (!extensionMap) {
throw new Error(
"Expected `extensionMap` in TemplateLayoutPathResolver constructor."
);
throw new Error("Expected `extensionMap` in TemplateLayoutPathResolver constructor.");
}

@@ -30,2 +26,6 @@

setAliases() {
this.aliases = Object.assign({}, this.config.layoutAliases, this.aliases);
}
set inputDir(dir) {

@@ -65,16 +65,12 @@ this._inputDir = dir;

let useLayoutResolution = this.config.layoutResolution;
this.pathAlreadyHasExtension = this.dir + "/" + this.path;
if (
this.path.split(".").length > 0 &&
fs.existsSync(this.pathAlreadyHasExtension)
) {
if (this.path.split(".").length > 0 && fs.existsSync(this.pathAlreadyHasExtension)) {
this.filename = this.path;
this.fullPath = TemplatePath.addLeadingDotSlash(
this.pathAlreadyHasExtension
);
} else {
this.fullPath = TemplatePath.addLeadingDotSlash(this.pathAlreadyHasExtension);
} else if (useLayoutResolution) {
this.filename = this.findFileName();
this.fullPath = TemplatePath.addLeadingDotSlash(
this.dir + "/" + this.filename
);
this.fullPath = TemplatePath.addLeadingDotSlash(this.dir + "/" + this.filename);
}

@@ -110,6 +106,3 @@ }

throw Error(
"TemplateLayoutPathResolver directory does not exist for " +
this.path +
": " +
this.dir
"TemplateLayoutPathResolver directory does not exist for " + this.path + ": " + this.dir
);

@@ -139,4 +132,8 @@ }

}
getNormalizedLayoutKey() {
return TemplatePath.stripLeadingSubPath(this.fullPath, this.getLayoutsDir());
}
}
module.exports = TemplateLayoutPathResolver;

@@ -1,11 +0,13 @@

const isPlainObject = require("./Util/IsPlainObject");
const DependencyGraph = require("dependency-graph").DepGraph;
const { isPlainObject } = require("@11ty/eleventy-utils");
const TemplateCollection = require("./TemplateCollection");
const EleventyErrorUtil = require("./EleventyErrorUtil");
const UsingCircularTemplateContentReferenceError = require("./Errors/UsingCircularTemplateContentReferenceError");
const EleventyBaseError = require("./EleventyBaseError");
const debug = require("debug")("Eleventy:TemplateMap");
const debugDev = require("debug")("Dev:Eleventy:TemplateMap");
const EleventyBaseError = require("./EleventyBaseError");
class TemplateMapConfigError extends EleventyBaseError {}

@@ -53,3 +55,3 @@

get tagPrefix() {
static get tagPrefix() {
return "___TAG___";

@@ -59,4 +61,10 @@ }

async add(template) {
// getTemplateMapEntries is where the Template.getData is first generated
for (let map of await template.getTemplateMapEntries()) {
if (!template) {
return;
}
// IMPORTANT: This is where the data is first generated for the template
let data = await template.getData();
for (let map of await template.getTemplateMapEntries(data)) {
this.map.push(map);

@@ -72,3 +80,3 @@ }

if (str.startsWith("collections.")) {
return str.substr("collections.".length);
return str.slice("collections.".length);
}

@@ -97,5 +105,42 @@ }

addTagsToGraph(graph, inputPath, tags) {
if (!Array.isArray(tags)) {
return;
}
for (let tag of tags) {
let tagWithPrefix = TemplateMap.tagPrefix + tag;
if (!graph.hasNode(tagWithPrefix)) {
graph.addNode(tagWithPrefix);
}
// Populates to collections.tagName
// Dependency from tag to inputPath
graph.addDependency(tagWithPrefix, inputPath);
}
}
addDeclaredDependenciesToGraph(graph, inputPath, deps) {
if (!Array.isArray(deps)) {
return;
}
for (let tag of deps) {
let tagWithPrefix = TemplateMap.tagPrefix + tag;
if (!graph.hasNode(tagWithPrefix)) {
graph.addNode(tagWithPrefix);
}
// Dependency from inputPath to collection/tag
graph.addDependency(inputPath, tagWithPrefix);
}
}
// Exclude: Pagination templates consuming `collections` or `collections.all`
// Exclude: Pagination templates that consume config API collections
// Include: Pagination templates that don’t consume config API collections
// Include: Templates that don’t use Pagination
getMappedDependencies() {
let graph = new DependencyGraph();
let tagPrefix = this.tagPrefix;
let tagPrefix = TemplateMap.tagPrefix;

@@ -129,26 +174,23 @@ graph.addNode(tagPrefix + "all");

if (!entry.data.eleventyExcludeFromCollections) {
// collections.all
// Populates to collections.all
graph.addDependency(tagPrefix + "all", entry.inputPath);
if (entry.data.tags) {
for (let tag of entry.data.tags) {
let tagWithPrefix = tagPrefix + tag;
if (!graph.hasNode(tagWithPrefix)) {
graph.addNode(tagWithPrefix);
}
this.addTagsToGraph(graph, entry.inputPath, entry.data.tags);
}
// collections.tagName
// Dependency from tag to inputPath
graph.addDependency(tagWithPrefix, entry.inputPath);
}
}
}
this.addDeclaredDependenciesToGraph(
graph,
entry.inputPath,
entry.data.eleventyImport?.collections
);
}
return graph.overallOrder();
return graph;
}
// Exclude: Pagination templates consuming `collections` or `collections.all`
// Include: Pagination templates that consume config API collections
getDelayedMappedDependencies() {
let graph = new DependencyGraph();
let tagPrefix = this.tagPrefix;
let tagPrefix = TemplateMap.tagPrefix;

@@ -162,3 +204,2 @@ graph.addNode(tagPrefix + "all");

graph.addNode(tagPrefix + tag);
// graph.addDependency( tagPrefix + tag, tagPrefix + "all" );
}

@@ -172,6 +213,3 @@

let paginationTagTarget = this.getPaginationTagTarget(entry);
if (
paginationTagTarget &&
this.isUserConfigCollectionName(paginationTagTarget)
) {
if (paginationTagTarget && this.isUserConfigCollectionName(paginationTagTarget)) {
if (!graph.hasNode(entry.inputPath)) {

@@ -183,33 +221,28 @@ graph.addNode(entry.inputPath);

if (!entry.data.eleventyExcludeFromCollections) {
// collections.all
// Populates into collections.all
graph.addDependency(tagPrefix + "all", entry.inputPath);
if (entry.data.tags) {
for (let tag of entry.data.tags) {
let tagWithPrefix = tagPrefix + tag;
if (!graph.hasNode(tagWithPrefix)) {
graph.addNode(tagWithPrefix);
}
// collections.tagName
// Dependency from tag to inputPath
graph.addDependency(tagWithPrefix, entry.inputPath);
}
}
this.addTagsToGraph(graph, entry.inputPath, entry.data.tags);
}
this.addDeclaredDependenciesToGraph(
graph,
entry.inputPath,
entry.data.eleventyImport?.collections
);
}
}
let order = graph.overallOrder();
return order;
return graph;
}
// Exclude: Pagination templates consuming `collections.all`
// Include: Pagination templates consuming `collections`
getPaginatedOverCollectionsMappedDependencies() {
let graph = new DependencyGraph();
let tagPrefix = this.tagPrefix;
let tagPrefix = TemplateMap.tagPrefix;
let allNodeAdded = false;
for (let entry of this.map) {
if (
this.isPaginationOverAllCollections(entry) &&
!this.getPaginationTagTarget(entry)
) {
if (this.isPaginationOverAllCollections(entry) && !this.getPaginationTagTarget(entry)) {
if (!allNodeAdded) {

@@ -227,12 +260,22 @@ graph.addNode(tagPrefix + "all");

graph.addDependency(tagPrefix + "all", entry.inputPath);
// Note that `tags` are otherwise ignored here
// TODO should we throw an error?
}
this.addDeclaredDependenciesToGraph(
graph,
entry.inputPath,
entry.data.eleventyImport?.collections
);
}
}
return graph.overallOrder();
return graph;
}
// Include: Pagination templates consuming `collections.all`
getPaginatedOverAllCollectionMappedDependencies() {
let graph = new DependencyGraph();
let tagPrefix = this.tagPrefix;
let tagPrefix = TemplateMap.tagPrefix;
let allNodeAdded = false;

@@ -255,17 +298,96 @@

if (!entry.data.eleventyExcludeFromCollections) {
// collections.all
// Populates into collections.all
// This is circular!
graph.addDependency(tagPrefix + "all", entry.inputPath);
// Note that `tags` are otherwise ignored here
// TODO should we throw an error?
}
this.addDeclaredDependenciesToGraph(
graph,
entry.inputPath,
entry.data.eleventyImport?.collections
);
}
}
return graph.overallOrder();
return graph;
}
getTemplateMapDependencyGraph() {
return [
this.getMappedDependencies(),
this.getDelayedMappedDependencies(),
this.getPaginatedOverCollectionsMappedDependencies(),
this.getPaginatedOverAllCollectionMappedDependencies(),
];
}
getFullTemplateMapOrder() {
// convert dependency graphs to ordered arrays
return this.getTemplateMapDependencyGraph().map((entry) => entry.overallOrder());
}
setupDependencyGraphChangesForIncrementalFile(incrementalFile) {
if (!incrementalFile) {
return new Set();
}
// Only dependents: things that consume templates that have deleted dependencies, e.g. if index.md consumes collections.post and a file removes its post tag, regenerate index.md
let newCollectionNames = new Set(); // collections published to
for (let entry of this.map) {
if (!entry.data.eleventyExcludeFromCollections) {
newCollectionNames.add("all");
if (Array.isArray(entry.data.tags)) {
for (let tag of entry.data.tags) {
newCollectionNames.add(tag);
}
}
}
}
let deletedCollectionNames = this.config.uses.findCollectionsRemovedFrom(
incrementalFile,
newCollectionNames
);
// Delete incremental from the dependency graph so we get fresh entries!
// This _must_ happen before any additions, the other ones are in Custom.js and GlobalDependencyMap.js (from the eleventy.layouts Event)
this.config.uses.resetNode(incrementalFile);
return this.config.uses.getTemplatesThatConsumeCollections(deletedCollectionNames);
}
// Similar to getTemplateMapDependencyGraph but adds those relationships to the global dependency graph used for incremental builds
addToGlobalDependencyGraph() {
for (let entry of this.map) {
let paginationTagTarget = this.getPaginationTagTarget(entry);
if (paginationTagTarget) {
this.config.uses.addDependencyConsumesCollection(entry.inputPath, paginationTagTarget);
}
if (!entry.data.eleventyExcludeFromCollections) {
this.config.uses.addDependencyPublishesToCollection(entry.inputPath, "all");
if (Array.isArray(entry.data.tags)) {
for (let tag of entry.data.tags) {
this.config.uses.addDependencyPublishesToCollection(entry.inputPath, tag);
}
}
}
if (Array.isArray(entry.data.eleventyImport?.collections)) {
for (let tag of entry.data.eleventyImport.collections) {
this.config.uses.addDependencyConsumesCollection(entry.inputPath, tag);
}
}
}
}
async setCollectionByTagName(tagName) {
if (this.isUserConfigCollectionName(tagName)) {
// async
this.collectionsData[tagName] = await this.getUserConfigCollection(
tagName
);
this.collectionsData[tagName] = await this.getUserConfigCollection(tagName);
} else {

@@ -289,7 +411,7 @@ this.collectionsData[tagName] = this.getTaggedCollection(tagName);

async initDependencyMap(dependencyMap) {
let tagPrefix = this.tagPrefix;
let tagPrefix = TemplateMap.tagPrefix;
for (let depEntry of dependencyMap) {
if (depEntry.startsWith(tagPrefix)) {
// is a tag (collection) entry
let tagName = depEntry.substr(tagPrefix.length);
let tagName = depEntry.slice(tagPrefix.length);
await this.setCollectionByTagName(tagName);

@@ -307,5 +429,3 @@ } else {

// We want these empty-data pagination templates to show up in the serverlessUrlMap.
map.template.initServerlessUrlsForEmptyPaginationTemplates(
map.data.permalink
);
await map.template.initServerlessUrlsForEmptyPaginationTemplates(map.data.permalink);
} else {

@@ -321,4 +441,3 @@ let counter = 0;

counter === 0 ||
(map.data.pagination &&
map.data.pagination.addAllPagesToCollections)
(map.data.pagination && map.data.pagination.addAllPagesToCollections)
) {

@@ -345,14 +464,8 @@ if (!map.data.eleventyExcludeFromCollections) {

let dependencyMap = this.getMappedDependencies();
let [dependencyMap, delayedDependencyMap, firstPaginatedDepMap, secondPaginatedDepMap] =
this.getFullTemplateMapOrder();
await this.initDependencyMap(dependencyMap);
let delayedDependencyMap = this.getDelayedMappedDependencies();
await this.initDependencyMap(delayedDependencyMap);
let firstPaginatedDepMap =
this.getPaginatedOverCollectionsMappedDependencies();
await this.initDependencyMap(firstPaginatedDepMap);
let secondPaginatedDepMap =
this.getPaginatedOverAllCollectionMappedDependencies();
await this.initDependencyMap(secondPaginatedDepMap);

@@ -368,8 +481,14 @@

);
let orderedMap = orderedPaths.map(
function (inputPath) {
return this.getMapEntryForInputPath(inputPath);
}.bind(this)
);
let orderedMap = orderedPaths.map((inputPath) => {
return this.getMapEntryForInputPath(inputPath);
});
await this.config.events.emitLazy("eleventy.contentMap", () => {
return {
inputPathToUrl: this.generateInputUrlContentMap(orderedMap),
urlToInputPath: this.generateUrlMap(orderedMap),
};
});
await this.populateContentDataInMap(orderedMap);

@@ -382,4 +501,5 @@

await this.config.events.emit(
"eleventy.serverlessUrlMap",
await this.config.events.emitLazy("eleventy.layouts", () => this.generateLayoutsMap());
await this.config.events.emitLazy("eleventy.serverlessUrlMap", () =>
this.generateServerlessUrlMap(orderedMap)

@@ -389,2 +509,21 @@ );

generateInputUrlContentMap(orderedMap) {
let entries = {};
for (let entry of orderedMap) {
entries[entry.inputPath] = entry._pages.map((entry) => entry.url);
}
return entries;
}
generateUrlMap(orderedMap) {
let entries = {};
for (let entry of orderedMap) {
for (let page of entry._pages) {
// duplicate urls throw an error, so we can return non array here
entries[page.url] = entry.inputPath;
}
}
return entries;
}
generateServerlessUrlMap(orderedMap) {

@@ -433,31 +572,14 @@ let entries = [];

getOrderedInputPaths(
dependencyMap,
delayedDependencyMap,
firstPaginatedDepMap,
secondPaginatedDepMap
) {
// Filter out any tag nodes
getOrderedInputPaths(...maps) {
let orderedMap = [];
let tagPrefix = this.tagPrefix;
let tagPrefix = TemplateMap.tagPrefix;
for (let dep of dependencyMap) {
if (!dep.startsWith(tagPrefix)) {
orderedMap.push(dep);
for (let map of maps) {
for (let dep of map) {
if (!dep.startsWith(tagPrefix)) {
orderedMap.push(dep);
}
}
}
for (let dep of delayedDependencyMap) {
if (!dep.startsWith(tagPrefix)) {
orderedMap.push(dep);
}
}
for (let dep of firstPaginatedDepMap) {
if (!dep.startsWith(tagPrefix)) {
orderedMap.push(dep);
}
}
for (let dep of secondPaginatedDepMap) {
if (!dep.startsWith(tagPrefix)) {
orderedMap.push(dep);
}
}
return orderedMap;

@@ -472,2 +594,3 @@ }

}
if (!map.template.behavior.isRenderable()) {

@@ -478,7 +601,6 @@ // Note that empty pagination templates will be skipped here as not renderable

// IMPORTANT: this is where template content is rendered
try {
for (let pageEntry of map._pages) {
pageEntry.templateContent = await map.template.getTemplateMapContent(
pageEntry
);
pageEntry.templateContent = await pageEntry.template.renderWithoutLayout(pageEntry.data);
}

@@ -492,5 +614,3 @@ } catch (e) {

}
debugDev(
"Added this.map[...].templateContent, outputPath, et al for one map entry"
);
debugDev("Added this.map[...].templateContent, outputPath, et al for one map entry");
}

@@ -501,5 +621,3 @@

for (let pageEntry of map._pages) {
pageEntry.templateContent = await map.template.getTemplateMapContent(
pageEntry
);
pageEntry.templateContent = await pageEntry.template.renderWithoutLayout(pageEntry.data);
}

@@ -562,4 +680,3 @@ } catch (e) {

isUserConfigCollectionName(name) {
let collections =
this.configCollections || this.userConfig.getCollections();
let collections = this.configCollections || this.userConfig.getCollections();
return name && !!collections[name];

@@ -569,10 +686,7 @@ }

getUserConfigCollectionNames() {
return Object.keys(
this.configCollections || this.userConfig.getCollections()
);
return Object.keys(this.configCollections || this.userConfig.getCollections());
}
async getUserConfigCollection(name) {
let configCollections =
this.configCollections || this.userConfig.getCollections();
let configCollections = this.configCollections || this.userConfig.getCollections();

@@ -588,4 +702,3 @@ // This works with async now

let collections = {};
let configCollections =
this.configCollections || this.userConfig.getCollections();
let configCollections = this.configCollections || this.userConfig.getCollections();

@@ -595,5 +708,3 @@ for (let name in configCollections) {

debug(
`Collection: collections.${name} size: ${collections[name].length}`
);
debug(`Collection: collections.${name} size: ${collections[name].length}`);
}

@@ -645,3 +756,5 @@

for (let page of entry._pages) {
promises.push(page.template.resolveRemainingComputedData(page.data));
if (this.config.keys.computed in page.data) {
promises.push(page.template.resolveRemainingComputedData(page.data));
}
}

@@ -652,2 +765,34 @@ }

async generateLayoutsMap() {
let layouts = {};
for (let entry of this.map) {
for (let page of entry._pages) {
let tmpl = page.template;
let layoutKey = page.data[this.config.keys.layout];
if (layoutKey) {
let layout = tmpl.getLayout(layoutKey);
let layoutChain = await layout.getLayoutChain();
let priors = [];
for (let filepath of layoutChain) {
if (!layouts[filepath]) {
layouts[filepath] = new Set();
}
layouts[filepath].add(page.inputPath);
for (let prior of priors) {
layouts[filepath].add(prior);
}
priors.push(filepath);
}
}
}
}
for (let key in layouts) {
layouts[key] = Array.from(layouts[key]);
}
return layouts;
}
checkForDuplicatePermalinks() {

@@ -660,12 +805,10 @@ let permalinks = {};

// do nothing (also serverless)
} else if (!permalinks[page.url]) {
permalinks[page.url] = [entry.inputPath];
} else if (!permalinks[page.outputPath]) {
permalinks[page.outputPath] = [entry.inputPath];
} else {
warnings[
warnings[page.outputPath] = `Output conflict: multiple input files are writing to \`${
page.outputPath
] = `Output conflict: multiple input files are writing to \`${
page.outputPath
}\`. Use distinct \`permalink\` values to resolve this conflict.
1. ${entry.inputPath}
${permalinks[page.url]
${permalinks[page.outputPath]
.map(function (inputPath, index) {

@@ -677,3 +820,3 @@ return ` ${index + 2}. ${inputPath}\n`;

permalinks[page.url].push(entry.inputPath);
permalinks[page.outputPath].push(entry.inputPath);
}

@@ -680,0 +823,0 @@ }

@@ -5,8 +5,9 @@ const fs = require("fs");

const copy = require("recursive-copy");
const fastglob = require("fast-glob");
const { TemplatePath } = require("@11ty/eleventy-utils");
const debug = require("debug")("Eleventy:TemplatePassthrough");
const EleventyBaseError = require("./EleventyBaseError");
const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck");
const debug = require("debug")("Eleventy:TemplatePassthrough");
class TemplatePassthroughError extends EleventyBaseError {}

@@ -34,2 +35,4 @@

this.copyOptions = path.copyOptions; // custom options for recursive-copy
this.isDryRun = false;

@@ -39,2 +42,3 @@ this.isIncremental = false;

/* { inputPath, outputPath } though outputPath is *not* the full path: just the output directory */
getPath() {

@@ -45,3 +49,3 @@ return this.rawPath;

getOutputPath(inputFileFromGlob) {
const { inputDir, outputDir, outputPath, inputPath } = this;
let { inputDir, outputDir, outputPath, inputPath } = this;

@@ -52,6 +56,3 @@ if (outputPath === true) {

outputDir,
TemplatePath.stripLeadingSubPath(
inputFileFromGlob || inputPath,
inputDir
)
TemplatePath.stripLeadingSubPath(inputFileFromGlob || inputPath, inputDir)
)

@@ -68,11 +69,11 @@ );

// https://github.com/11ty/eleventy/issues/2278
let fullOutputPath = TemplatePath.normalize(
TemplatePath.join(outputDir, outputPath)
);
let fullOutputPath = TemplatePath.normalize(TemplatePath.join(outputDir, outputPath));
if (this.isIncremental && TemplatePath.isDirectorySync(fullOutputPath)) {
if (
fs.existsSync(inputPath) &&
!TemplatePath.isDirectorySync(inputPath) &&
TemplatePath.isDirectorySync(fullOutputPath)
) {
let filename = path.parse(inputPath).base;
return TemplatePath.normalize(
TemplatePath.join(outputDir, outputPath, filename)
);
return TemplatePath.normalize(TemplatePath.join(fullOutputPath, filename));
}

@@ -94,2 +95,6 @@

setRunMode(runMode) {
this.runMode = runMode;
}
setIsIncremental(isIncremental) {

@@ -99,11 +104,12 @@ this.isIncremental = isIncremental;

setFileSystemSearch(fileSystemSearch) {
this.fileSystemSearch = fileSystemSearch;
}
async getFiles(glob) {
debug("Searching for: %o", glob);
let b = this.benchmarks.aggregate.get("Searching the file system");
let b = this.benchmarks.aggregate.get("Searching the file system (passthrough)");
b.before();
const files = TemplatePath.addLeadingDotSlashArray(
await fastglob(glob, {
caseSensitiveMatch: false,
dot: true,
})
let files = TemplatePath.addLeadingDotSlashArray(
await this.fileSystemSearch.search("passthrough", glob)
);

@@ -114,2 +120,44 @@ b.after();

// dir is guaranteed to exist by context
// dir may not be a directory
addTrailingSlashIfDirectory(dir) {
if (dir && typeof dir === "string") {
if (dir.endsWith(path.sep)) {
return dir;
}
if (fs.statSync(dir).isDirectory()) {
return `${dir}/`;
}
}
return dir;
}
// maps input paths to output paths
async getFileMap() {
// TODO VirtualFileSystem candidate
if (!isGlob(this.inputPath) && fs.existsSync(this.inputPath)) {
// When inputPath is a directory, make sure it has a slash for passthrough copy aliasing
// https://github.com/11ty/eleventy/issues/2709
let inputPath = this.addTrailingSlashIfDirectory(this.inputPath);
return [
{
inputPath,
outputPath: this.getOutputPath(),
},
];
}
let paths = [];
// If not directory or file, attempt to get globs
let files = await this.getFiles(this.inputPath);
for (let inputPath of files) {
paths.push({
inputPath,
outputPath: this.getOutputPath(inputPath),
});
}
return paths;
}
/* Types:

@@ -122,3 +170,3 @@ * 1. via glob, individual files found

if (
!TemplatePath.stripLeadingDotSlash(dest).includes(
!TemplatePath.stripLeadingDotSlash(dest).startsWith(
TemplatePath.stripLeadingDotSlash(this.outputDir)

@@ -136,3 +184,2 @@ )

let map = {};
// returns a promise

@@ -146,3 +193,3 @@ return copy(src, dest, copyOptions)

})
.on(copy.events.COPY_FILE_COMPLETE, () => {
.on(copy.events.COPY_FILE_COMPLETE, (copyOp) => {
fileCopyCount++;

@@ -167,26 +214,38 @@ this.benchmarks.aggregate.get("Passthrough Copy File").after();

const copyOptions = {
overwrite: true,
dot: true,
junk: false,
debug("Copying %o", this.inputPath);
let fileMap = await this.getFileMap();
// default options for recursive-copy
// see https://www.npmjs.com/package/recursive-copy#arguments
let copyOptionsDefault = {
overwrite: true, // overwrite output. fails when input is directory (mkdir) and output is file
dot: true, // copy dotfiles
junk: false, // copy cache files like Thumbs.db
results: false,
expand: false, // follow symlinks (matches recursive-copy default)
debug: false, // (matches recursive-copy default)
// Note: `filter` callback function only passes in a relative path, which is unreliable
// See https://github.com/timkendrick/recursive-copy/blob/4c9a8b8a4bf573285e9c4a649a30a2b59ccf441c/lib/copy.js#L59
// e.g. `{ filePaths: [ './img/coolkid.jpg' ], relativePaths: [ '' ] }`
};
let promises = [];
debug("Copying %o", this.inputPath);
let copyOptions = Object.assign(copyOptionsDefault, this.copyOptions);
if (!isGlob(this.inputPath) && fs.existsSync(this.inputPath)) {
promises.push(
this.copy(this.inputPath, this.getOutputPath(), copyOptions)
);
} else {
// If not directory or file, attempt to get globs
let files = await this.getFiles(this.inputPath);
let promises = fileMap.map((entry) => {
// For-free passthrough copy
if (checkPassthroughCopyBehavior(this.config, this.runMode)) {
let aliasMap = {};
aliasMap[entry.inputPath] = entry.outputPath;
promises = files.map((inputFile) => {
let target = this.getOutputPath(inputFile);
return this.copy(inputFile, target, copyOptions);
});
}
return Promise.resolve({
count: 0,
map: aliasMap,
});
}
// Copy the files (only in build mode)
return this.copy(entry.inputPath, entry.outputPath, copyOptions);
});
// IMPORTANT: this returns an array of promises, does not await for promise to finish

@@ -210,6 +269,3 @@ return Promise.all(promises)

.catch((err) => {
throw new TemplatePassthroughError(
`Error copying passthrough files: ${err.message}`,
err
);
throw new TemplatePassthroughError(`Error copying passthrough files: ${err.message}`, err);
});

@@ -216,0 +272,0 @@ }

@@ -8,2 +8,3 @@ const multimatch = require("multimatch");

const TemplatePassthrough = require("./TemplatePassthrough");
const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck");

@@ -19,5 +20,3 @@ const debug = require("debug")("Eleventy:TemplatePassthroughManager");

if (!eleventyConfig) {
throw new TemplatePassthroughManagerConfigError(
"Missing `config` argument."
);
throw new TemplatePassthroughManagerConfigError("Missing `config` argument.");
}

@@ -59,2 +58,6 @@ this.eleventyConfig = eleventyConfig;

setRunMode(runMode) {
this.runMode = runMode;
}
setIncrementalFile(path) {

@@ -66,8 +69,7 @@ if (path) {

_normalizePaths(path, outputPath) {
_normalizePaths(path, outputPath, copyOptions = {}) {
return {
inputPath: TemplatePath.addLeadingDotSlash(path),
outputPath: outputPath
? TemplatePath.stripLeadingDotSlash(outputPath)
: true,
outputPath: outputPath ? TemplatePath.stripLeadingDotSlash(outputPath) : true,
copyOptions,
};

@@ -78,6 +80,6 @@ }

let paths = [];
let target = this.config.passthroughCopies || {};
debug("`addPassthroughCopy` config API paths: %o", target);
for (let path in target) {
paths.push(this._normalizePaths(path, target[path]));
let pathsRaw = this.config.passthroughCopies || {};
debug("`addPassthroughCopy` config API paths: %o", pathsRaw);
for (let [inputPath, { outputPath, copyOptions }] of Object.entries(pathsRaw)) {
paths.push(this._normalizePaths(inputPath, outputPath, copyOptions));
}

@@ -109,10 +111,14 @@ debug("`addPassthroughCopy` config API normalized paths: %o", paths);

setFileSystemSearch(fileSystemSearch) {
this.fileSystemSearch = fileSystemSearch;
}
getTemplatePassthroughForPath(path, isIncremental = false) {
let inst = new TemplatePassthrough(
path,
this.outputDir,
this.inputDir,
this.config
);
let inst = new TemplatePassthrough(path, this.outputDir, this.inputDir, this.config);
inst.setFileSystemSearch(this.fileSystemSearch);
inst.setIsIncremental(isIncremental);
inst.setDryRun(this.isDryRun);
inst.setRunMode(this.runMode);
return inst;

@@ -128,5 +134,15 @@ }

let path = pass.getPath();
pass.setDryRun(this.isDryRun);
let { inputPath } = pass.getPath();
// TODO https://github.com/11ty/eleventy/issues/2452
// De-dupe both the input and output paired together to avoid the case
// where an input/output pair has been added via multiple passthrough methods (glob, file suffix, etc)
// Probably start with the `filter` callback in recursive-copy but it only passes relative paths
// See the note in TemplatePassthrough.js->write()
// Also note that `recursive-copy` handles repeated overwrite copy to the same destination just fine.
// e.g. `for(let j=0, k=1000; j<k; j++) { copy("coolkid.jpg", "_site/coolkid.jpg"); }`
// Eventually we’ll want to move all of this to use Node’s fs.cp, which is experimental and only on Node 16+
return pass

@@ -138,12 +154,19 @@ .write()

if (this.conflictMap[dest]) {
throw new TemplatePassthroughManagerCopyError(
`Multiple passthrough copy files are trying to write to the same output file (${dest}). ${src} and ${this.conflictMap[dest]}`
);
if (src !== this.conflictMap[dest]) {
throw new TemplatePassthroughManagerCopyError(
`Multiple passthrough copy files are trying to write to the same output file (${dest}). ${src} and ${this.conflictMap[dest]}`
);
} else {
// Multiple entries from the same source
debug(
"A passthrough copy entry (%o) caused the same file (%o) to be copied more than once to the output (%o). This is atomically safe but a waste of build resources.",
inputPath,
src,
dest
);
}
}
debugDev(
"Adding %o to passthrough copy conflict map, from %o",
dest,
src
);
debugDev("Adding %o to passthrough copy conflict map, from %o", dest, src);
this.conflictMap[dest] = src;

@@ -155,4 +178,4 @@ }

debug(
"Skipped %o (either from --dryrun or --incremental)",
path.inputPath
"Skipped %o (either from --dryrun or --incremental or for-free passthrough copy)",
inputPath
);

@@ -162,12 +185,16 @@ } else {

this.count += count;
debug("Copied %o (%d files)", inputPath, count || 0);
} else {
debug("Skipped copying %o (emulated passthrough copy)", inputPath);
}
debug("Copied %o (%d files)", path.inputPath, count || 0);
}
return {
count,
map,
};
})
.catch(function (e) {
return Promise.reject(
new TemplatePassthroughManagerCopyError(
`Having trouble copying '${path.inputPath}'`,
e
)
new TemplatePassthroughManagerCopyError(`Having trouble copying '${inputPath}'`, e)
);

@@ -203,15 +230,7 @@ });

if (this.incrementalFile) {
let isPassthrough = this.isPassthroughCopyFile(
paths,
this.incrementalFile
);
let isPassthrough = this.isPassthroughCopyFile(paths, this.incrementalFile);
if (isPassthrough) {
if (isPassthrough.outputPath) {
return [
this._normalizePaths(
this.incrementalFile,
isPassthrough.outputPath
),
];
return [this._normalizePaths(this.incrementalFile, isPassthrough.outputPath)];
}

@@ -221,11 +240,14 @@

}
return [];
// Fixes https://github.com/11ty/eleventy/issues/2491
if (!checkPassthroughCopyBehavior(this.config, this.runMode)) {
return [];
}
}
let normalizedPaths = [];
let pathsFromConfigurationFile = this.getConfigPaths();
for (let path of pathsFromConfigurationFile) {
debug("TemplatePassthrough copying from config: %o", path);
normalizedPaths.push(path);
let normalizedPaths = this.getConfigPaths();
if (debug.enabled) {
for (let path of normalizedPaths) {
debug("TemplatePassthrough copying from config: %o", path);
}
}

@@ -237,5 +259,7 @@

let normalizedPath = this._normalizePaths(path);
debug(
`TemplatePassthrough copying from non-matching file extension: ${normalizedPath.inputPath}`
);
normalizedPaths.push(normalizedPath);

@@ -248,21 +272,38 @@ }

// keys: output
// values: input
getAliasesFromPassthroughResults(result) {
let entries = {};
for (let entry of result) {
for (let src in entry.map) {
let dest = TemplatePath.stripLeadingSubPath(entry.map[src], this.outputDir);
entries["/" + dest] = src;
}
}
return entries;
}
// Performance note: these can actually take a fair bit of time, but aren’t a
// bottleneck to eleventy. The copies are performed asynchronously and don’t affect eleventy
// write times in a significant way.
async copyAll(paths) {
async copyAll(templateExtensionPaths) {
debug("TemplatePassthrough copy started.");
let normalizedPaths = this.getAllNormalizedPaths(paths);
let passthroughs = [];
for (let path of normalizedPaths) {
let normalizedPaths = this.getAllNormalizedPaths(templateExtensionPaths);
let passthroughs = normalizedPaths.map((path) => {
// if incrementalFile is set but it isn’t a passthrough copy, normalizedPaths will be an empty array
let isIncremental = !!this.incrementalFile;
passthroughs.push(
this.getTemplatePassthroughForPath(path, isIncremental)
);
}
return Promise.all(
passthroughs.map((pass) => this.copyPassthrough(pass))
).then(() => {
return this.getTemplatePassthroughForPath(path, isIncremental);
});
let promises = passthroughs.map((pass) => this.copyPassthrough(pass));
return Promise.all(promises).then(async (results) => {
let aliases = this.getAliasesFromPassthroughResults(results);
await this.config.events.emit("eleventy.passthrough", {
map: aliases,
});
debug(`TemplatePassthrough copy finished. Current count: ${this.count}`);
return results;
});

@@ -269,0 +310,0 @@ }

const path = require("path");
const normalize = require("normalize-path");
const { TemplatePath } = require("@11ty/eleventy-utils");
const { TemplatePath, isPlainObject } = require("@11ty/eleventy-utils");
const isPlainObject = require("./Util/IsPlainObject");
const serverlessUrlFilter = require("./Filters/ServerlessUrl");

@@ -70,2 +69,10 @@

setUrlTransforms(transforms) {
this._urlTransforms = transforms;
}
get urlTransforms() {
return this._urlTransforms || [];
}
setServerlessPathData(data) {

@@ -80,3 +87,3 @@ this.serverlessPathData = data;

_addDefaultLinkFilename(link) {
return link + (link.substr(-1) === "/" ? "index.html" : "");
return link + (link.slice(-1) === "/" ? "index.html" : "");
}

@@ -100,2 +107,30 @@

// Used in url transforms feature
static getUrlStem(original) {
let subject = original;
if (original.endsWith(".html")) {
subject = original.slice(0, -1 * ".html".length);
}
return TemplatePermalink.normalizePathToUrl(subject);
}
static normalizePathToUrl(original) {
let compare = original || "";
let needleHtml = "/index.html";
let needleBareTrailingSlash = "/index/";
let needleBare = "/index";
if (compare.endsWith(needleHtml)) {
return compare.slice(0, compare.length - needleHtml.length) + "/";
} else if (compare.endsWith(needleBareTrailingSlash)) {
return (
compare.slice(0, compare.length - needleBareTrailingSlash.length) + "/"
);
} else if (compare.endsWith(needleBare)) {
return compare.slice(0, compare.length - needleBare.length) + "/";
}
return original;
}
// This method is used to generate the `page.url` variable.

@@ -138,9 +173,13 @@ // Note that in serverless mode this should still exist to generate the content map

(transformedLink.charAt(0) !== "/" ? "/" : "") + transformedLink;
let needle = "/index.html";
if (original === needle) {
return "/";
} else if (original.substr(-1 * needle.length) === needle) {
return original.substr(0, original.length - needle.length) + "/";
let normalized = TemplatePermalink.normalizePathToUrl(original) || "";
for (let transform of this.urlTransforms) {
original =
transform({
url: normalized,
urlStem: TemplatePermalink.getUrlStem(original),
}) ?? original;
}
return original;
return TemplatePermalink.normalizePathToUrl(original);
}

@@ -191,8 +230,9 @@

) {
let hasDupeFolder = TemplatePermalink._hasDuplicateFolder(
dir,
filenameNoExt
);
let path;
if (fileExtension === "html") {
let hasDupeFolder = TemplatePermalink._hasDuplicateFolder(
dir,
filenameNoExt
);
path =

@@ -199,0 +239,0 @@ (dir ? dir + "/" : "") +

@@ -6,2 +6,3 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

const EleventyExtensionMap = require("./EleventyExtensionMap");
const CustomEngine = require("./Engines/Custom.js");
// const debug = require("debug")("Eleventy:TemplateRender");

@@ -16,5 +17,3 @@

if (!tmplPath) {
throw new Error(
`TemplateRender requires a tmplPath argument, instead of ${tmplPath}`
);
throw new Error(`TemplateRender requires a tmplPath argument, instead of ${tmplPath}`);
}

@@ -32,6 +31,3 @@ if (!config) {

this.inputDir = inputDir ? inputDir : this.config.dir.input;
this.includesDir = TemplatePath.join(
this.inputDir,
this.config.dir.includes
);
this.includesDir = TemplatePath.join(this.inputDir, this.config.dir.includes);

@@ -64,2 +60,9 @@ this.parseMarkdownWith = this.config.markdownTemplateEngine;

getEngineByName(name) {
let engine = this.extensionMap.engineManager.getEngine(name, this.getDirs(), this.extensionMap);
engine.config = this.config;
return engine;
}
// Runs once per template

@@ -75,9 +78,3 @@ init(engineNameOrPath) {

this._engine = this.extensionMap.engineManager.getEngine(
this._engineName,
this.getDirs(),
this.extensionMap
);
this._engine.config = this.config;
this._engine.initRequireCache(engineNameOrPath);
this._engine = this.getEngineByName(this._engineName);

@@ -104,2 +101,6 @@ if (this.useMarkdown === undefined) {

static parseEngineOverrides(engineName) {
if (typeof (engineName || "") !== "string") {
throw new Error("Expected String passed to parseEngineOverrides. Received: " + engineName);
}
let overlappingEngineWarningCount = 0;

@@ -150,13 +151,21 @@ let engines = [];

getReadableEnginesList() {
return (
this.getReadableEnginesListDifferingFromFileExtension() || this.engineName
);
return this.getReadableEnginesListDifferingFromFileExtension() || this.engineName;
}
getReadableEnginesListDifferingFromFileExtension() {
if (
this.engineName === "md" &&
this.useMarkdown &&
this.parseMarkdownWith
) {
let keyFromFilename = this.extensionMap.getKey(this.engineNameOrPath);
if (this.engine instanceof CustomEngine) {
if (
this.engine.entry &&
this.engine.entry.name &&
keyFromFilename !== this.engine.entry.name
) {
return this.engine.entry.name;
} else {
// We don’t have a name for it so we return nothing so we don’t misreport (per #2386)
return;
}
}
if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) {
return this.parseMarkdownWith;

@@ -169,3 +178,2 @@ }

// templateEngineOverride in play and template language differs from file extension
let keyFromFilename = this.extensionMap.getKey(this.engineNameOrPath);
if (keyFromFilename !== this.engineName) {

@@ -176,2 +184,31 @@ return this.engineName;

// TODO templateEngineOverride
getPreprocessorEngine() {
if (this.engineName === "md" && this.parseMarkdownWith) {
return this.parseMarkdownWith;
}
if (this.engineName === "html" && this.parseHtmlWith) {
return this.parseHtmlWith;
}
return this.extensionMap.getKey(this.engineNameOrPath);
}
// We pass in templateEngineOverride here because it isn’t yet applied to templateRender
getEnginesList(engineOverride) {
if (engineOverride) {
let engines = TemplateRender.parseEngineOverrides(engineOverride).reverse();
return engines.join(",");
}
if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) {
return `${this.parseMarkdownWith},md`;
}
if (this.engineName === "html" && this.parseHtmlWith) {
return this.parseHtmlWith;
}
// templateEngineOverride in play
return this.extensionMap.getKey(this.engineNameOrPath);
}
setEngineOverride(engineName, bypassMarkdown) {

@@ -248,7 +285,3 @@ let engines = TemplateRender.parseEngineOverrides(engineName);

} else if (this.engineName === "html") {
return this.engine.compile(
str,
this.engineNameOrPath,
this.parseHtmlWith
);
return this.engine.compile(str, this.engineNameOrPath, this.parseHtmlWith);
} else {

@@ -255,0 +288,0 @@ return this.engine.compile(str, this.engineNameOrPath);

@@ -10,2 +10,3 @@ const { TemplatePath } = require("@11ty/eleventy-utils");

const EleventyErrorUtil = require("./EleventyErrorUtil");
const FileSystemSearch = require("./FileSystemSearch");
const ConsoleLogger = require("./Util/ConsoleLogger");

@@ -39,3 +40,2 @@

this.needToSearchForFiles = null;
this.templateFormats = templateFormats;

@@ -48,2 +48,3 @@

this.skippedCount = 0;
this.isRunInitialBuild = true;

@@ -65,6 +66,2 @@ this._templatePathCache = new Map();

set templateFormats(value) {
if (value !== this._templateFormats) {
this.needToSearchForFiles = true;
}
this._templateFormats = value;

@@ -116,6 +113,3 @@ }

if (!this._extensionMap) {
this._extensionMap = new EleventyExtensionMap(
this.templateFormats,
this.eleventyConfig
);
this._extensionMap = new EleventyExtensionMap(this.templateFormats, this.eleventyConfig);
}

@@ -145,2 +139,3 @@ return this._extensionMap;

this._eleventyFiles.setInput(this.inputDir, this.input);
this._eleventyFiles.setFileSystemSearch(new FileSystemSearch());
this._eleventyFiles.init();

@@ -153,20 +148,24 @@ }

async _getAllPaths() {
if (!this.allPaths || this.needToSearchForFiles) {
this.allPaths = await this.eleventyFiles.getFiles();
debug("Found: %o", this.allPaths);
}
return this.allPaths;
// this is now cached upstream by FileSystemSearch
return this.eleventyFiles.getFiles();
}
_isIncrementalFileAPassthroughCopy(paths) {
if (!this.incrementalFile) {
return false;
}
let passthroughManager = this.eleventyFiles.getPassthroughManager();
return passthroughManager.isPassthroughCopyFile(
paths,
this.incrementalFile
);
return passthroughManager.isPassthroughCopyFile(paths, this.incrementalFile);
}
_createTemplate(path, allPaths, to = "fs") {
_createTemplate(path, to = "fs") {
let tmpl = this._templatePathCache.get(path);
if (!tmpl) {
let wasCached = false;
if (tmpl) {
wasCached = true;
// TODO reset other constructor things here like inputDir/outputDir/extensionMap/
tmpl.setTemplateData(this.templateData);
} else {
tmpl = new Template(

@@ -180,2 +179,3 @@ path,

);
tmpl.setOutputFormat(to);

@@ -207,27 +207,10 @@

tmpl.setDryRun(this.isDryRun);
tmpl.setIsVerbose(this.isVerbose);
tmpl.reset();
// --incremental only writes files that trigger a build during --watch
if (this.incrementalFile) {
// incremental file is a passthrough copy (not a template)
if (this._isIncrementalFileAPassthroughCopy(allPaths)) {
tmpl.setDryRun(true);
// Passthrough copy check is above this (order is important)
} else if (
tmpl.isFileRelevantToThisTemplate(this.incrementalFile, {
isFullTemplate: this.eleventyFiles.isFullTemplateFile(
allPaths,
this.incrementalFile
),
})
) {
tmpl.setDryRun(this.isDryRun);
} else {
tmpl.setDryRun(true);
}
} else {
tmpl.setDryRun(this.isDryRun);
}
return tmpl;
return {
template: tmpl,
wasCached,
};
}

@@ -237,9 +220,108 @@

let promises = [];
let isIncrementalFileAFullTemplate = this.eleventyFiles.isFullTemplateFile(
paths,
this.incrementalFile
);
let isIncrementalFileAPassthroughCopy = this._isIncrementalFileAPassthroughCopy(paths);
let relevantToDeletions = new Set();
// Update the data cascade and the global dependency map for the one incremental template before everything else (only full templates)
if (isIncrementalFileAFullTemplate && this.incrementalFile) {
let path = this.incrementalFile;
let { template: tmpl, wasCached } = this._createTemplate(path, to);
if (wasCached) {
tmpl.resetCaches(); // reset internal caches on the cached template instance
}
// Render overrides are only used when `--ignore-initial` is in play and an initial build is not run
if (!this.isRunInitialBuild) {
tmpl.setRenderableOverride(undefined); // reset to render enabled
}
let p = this.templateMap.add(tmpl);
promises.push(p);
await p;
debug(`${path} adding to template map.`);
// establish new relationships for this template
relevantToDeletions = this.templateMap.setupDependencyGraphChangesForIncrementalFile(
tmpl.inputPath
);
this.templateMap.addToGlobalDependencyGraph();
}
for (let path of paths) {
if (this.extensionMap.hasEngine(path)) {
promises.push(
this.templateMap.add(this._createTemplate(path, paths, to))
if (!this.extensionMap.hasEngine(path)) {
continue;
}
if (isIncrementalFileAPassthroughCopy) {
this.skippedCount++;
continue;
}
// We already updated the data cascade for this template above
if (isIncrementalFileAFullTemplate && this.incrementalFile === path) {
continue;
}
let { template: tmpl, wasCached } = this._createTemplate(path, to);
if (!this.incrementalFile) {
// Render overrides are only used when `--ignore-initial` is in play and an initial build is not run
if (!this.isRunInitialBuild) {
if (wasCached) {
tmpl.setRenderableOverride(undefined); // enable render
} else {
tmpl.setRenderableOverride(false); // disable render
}
}
if (wasCached) {
tmpl.resetCaches();
}
} else {
let isTemplateRelevantToDeletedCollections = relevantToDeletions.has(
TemplatePath.stripLeadingDotSlash(tmpl.inputPath)
);
if (
isTemplateRelevantToDeletedCollections ||
tmpl.isFileRelevantToThisTemplate(this.incrementalFile, {
isFullTemplate: isIncrementalFileAFullTemplate,
})
) {
// Related to the template but not the template (reset the render cache, not the read cache)
tmpl.resetCaches({
data: true,
render: true,
});
// Render overrides are only used when `--ignore-initial` is in play and an initial build is not run
if (!this.isRunInitialBuild) {
tmpl.setRenderableOverride(undefined); // reset to render enabled
}
} else {
// During incremental we only reset the data cache for non-matching templates, see https://github.com/11ty/eleventy/issues/2710
// Keep caches for read/render
tmpl.resetCaches({
data: true,
});
// Render overrides are only used when `--ignore-initial` is in play and an initial build is not run
if (!this.isRunInitialBuild) {
tmpl.setRenderableOverride(false); // false to disable render
}
tmpl.setDryRunViaIncremental();
this.skippedCount++;
}
}
debug(`${path} begun adding to map.`);
// This fetches the data cascade for this template, which we want to avoid if not applicable to incremental
promises.push(this.templateMap.add(tmpl));
debug(`${path} adding to template map.`);
}

@@ -254,2 +336,6 @@

await this._addToTemplateMap(paths, to);
// write new template relationships to the global dependency graph for next time
this.templateMap.addToGlobalDependencyGraph();
await this.templateMap.cache();

@@ -265,3 +351,2 @@

return tmpl.generateMapEntry(mapEntry, to).then((pages) => {
this.skippedCount += tmpl.getSkippedCount();
this.writeCount += tmpl.getWriteCount();

@@ -272,11 +357,9 @@ return pages;

async writePassthroughCopy(paths) {
async writePassthroughCopy(templateExtensionPaths) {
let passthroughManager = this.eleventyFiles.getPassthroughManager();
passthroughManager.setIncrementalFile(this.incrementalFile);
return passthroughManager.copyAll(paths).catch((e) => {
return passthroughManager.copyAll(templateExtensionPaths).catch((e) => {
this.errorHandler.warn(e, "Error with passthrough copy");
return Promise.reject(
new EleventyPassthroughCopyError("Having trouble copying", e)
);
return Promise.reject(new EleventyPassthroughCopyError("Having trouble copying", e));
});

@@ -305,3 +388,3 @@ }

new EleventyTemplateError(
`Having trouble writing template: "${mapEntry.outputPath}"`,
`Having trouble writing to "${mapEntry.outputPath}" from "${mapEntry.inputPath}"`,
e

@@ -320,3 +403,3 @@ )

new EleventyTemplateError(
`Having trouble writing template (second pass): "${mapEntry.outputPath}"`,
`Having trouble writing to (second pass) "${mapEntry.outputPath}" from "${mapEntry.inputPath}"`,
e

@@ -336,2 +419,3 @@ )

// The ordering here is important to destructuring in Eleventy->_watch
promises.push(this.writePassthroughCopy(paths));

@@ -374,2 +458,8 @@

setRunInitialBuild(runInitialBuild) {
this.isRunInitialBuild = runInitialBuild;
}
setIncrementalBuild(isIncremental) {
this.isIncremental = isIncremental;
}
setIncrementalFile(incrementalFile) {

@@ -386,6 +476,2 @@ this.incrementalFile = incrementalFile;

getSkippedCopyCount() {
return this.eleventyFiles.getPassthroughManager().getSkippedCount();
}
getWriteCount() {

@@ -398,4 +484,8 @@ return this.writeCount;

}
get caches() {
return ["_templatePathCache"];
}
}
module.exports = TemplateWriter;
const chalk = require("kleur");
const semver = require("semver");
const { DateTime } = require("luxon");
const EventEmitter = require("./Util/AsyncEventEmitter");
const EleventyCompatibility = require("./Util/Compatibility");
const EleventyBaseError = require("./EleventyBaseError");
const BenchmarkManager = require("./BenchmarkManager");
const merge = require("./Util/Merge");
const debug = require("debug")("Eleventy:UserConfig");
const pkg = require("../package.json");
class UserConfigError extends EleventyBaseError {}
const ComparisonAsyncFunction = (async () => {}).constructor;
// API to expose configuration options in config file

@@ -38,3 +41,5 @@ class UserConfig {

this.liquidPairedShortcodes = {};
this.nunjucksEnvironmentOptions = {};
this.nunjucksPrecompiledTemplates = {};
this.nunjucksFilters = {};

@@ -48,5 +53,7 @@ this.nunjucksAsyncFilters = {};

this.nunjucksAsyncPairedShortcodes = {};
this.handlebarsHelpers = {};
this.handlebarsShortcodes = {};
this.handlebarsPairedShortcodes = {};
this.javascriptFunctions = {};

@@ -60,2 +67,4 @@ this.pugOptions = {};

this.layoutAliases = {};
this.layoutResolution = true; // extension-less layout files
this.linters = {};

@@ -68,5 +77,9 @@ this.transforms = {};

this.useGitIgnore = true;
this.ignores = new Set();
this.ignores.add("node_modules/**");
let defaultIgnores = new Set();
defaultIgnores.add("**/node_modules/**");
defaultIgnores.add(".git/**");
this.ignores = new Set(defaultIgnores);
this.watchIgnores = new Set(defaultIgnores);
this.dataDeepMerge = true;

@@ -76,3 +89,3 @@ this.extensionMap = new Set();

this.additionalWatchTargets = [];
this.browserSyncConfig = {};
this.serverOptions = {};
this.globalData = {};

@@ -92,13 +105,18 @@ this.chokidarConfig = {};

this.dataFilterSelectors = new Set();
this.libraryAmendments = {};
this.serverPassthroughCopyBehavior = "copy"; // or "passthrough"
this.urlTransforms = [];
// Defaults in `defaultConfig.js`
this.dataFileSuffixesOverride = false;
this.dataFileDirBaseNameOverride = false;
}
versionCheck(expected) {
if (
!semver.satisfies(pkg.version, expected, {
includePrerelease: true,
})
) {
throw new UserConfigError(
`This project requires the Eleventy version to match '${expected}' but found ${pkg.version}. Use \`npm update @11ty/eleventy -g\` to upgrade the eleventy global or \`npm update @11ty/eleventy --save\` to upgrade your local project version.`
);
// compatibleRange is optional in 2.0.0-beta.2
versionCheck(compatibleRange) {
let compat = new EleventyCompatibility(compatibleRange);
if (!compat.isCompatible()) {
throw new UserConfigError(compat.getErrorMessage());
}

@@ -140,13 +158,5 @@ }

if (this.liquidTags[name]) {
debug(
chalk.yellow(
"Warning, overwriting a Liquid tag with `addLiquidTag(%o)`"
),
name
);
debug(chalk.yellow("Warning, overwriting a Liquid tag with `addLiquidTag(%o)`"), name);
}
this.liquidTags[name] = this.benchmarks.config.add(
`"${name}" Liquid Custom Tag`,
tagFn
);
this.liquidTags[name] = this.benchmarks.config.add(`"${name}" Liquid Custom Tag`, tagFn);
}

@@ -158,14 +168,6 @@

if (this.liquidFilters[name]) {
debug(
chalk.yellow(
"Warning, overwriting a Liquid filter with `addLiquidFilter(%o)`"
),
name
);
debug(chalk.yellow("Warning, overwriting a Liquid filter with `addLiquidFilter(%o)`"), name);
}
this.liquidFilters[name] = this.benchmarks.config.add(
`"${name}" Liquid Filter`,
callback
);
this.liquidFilters[name] = this.benchmarks.config.add(`"${name}" Liquid Filter`, callback);
}

@@ -178,5 +180,3 @@

debug(
chalk.yellow(
"Warning, overwriting a Nunjucks filter with `addNunjucksAsyncFilter(%o)`"
),
chalk.yellow("Warning, overwriting a Nunjucks filter with `addNunjucksAsyncFilter(%o)`"),
name

@@ -202,5 +202,3 @@ );

debug(
chalk.yellow(
"Warning, overwriting a Nunjucks filter with `addNunjucksFilter(%o)`"
),
chalk.yellow("Warning, overwriting a Nunjucks filter with `addNunjucksFilter(%o)`"),
name

@@ -222,5 +220,3 @@ );

debug(
chalk.yellow(
"Warning, overwriting a Handlebars helper with `addHandlebarsHelper(%o)`."
),
chalk.yellow("Warning, overwriting a Handlebars helper with `addHandlebarsHelper(%o)`."),
name

@@ -237,2 +233,8 @@ );

addFilter(name, callback) {
// This method *requires* `async function` and will not work with `function` that returns a promise
if (callback instanceof ComparisonAsyncFunction) {
this.addAsyncFilter(name, callback);
return;
}
debug("Adding universal filter %o", this.getNamespacedName(name));

@@ -242,5 +244,14 @@

this.addLiquidFilter(name, callback);
this.addNunjucksFilter(name, callback);
this.addJavaScriptFunction(name, callback);
this.addNunjucksFilter(name, function (...args) {
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;
});
// TODO remove Handlebars helpers in Universal Filters. Use shortcodes instead (the Handlebars template syntax is the same).

@@ -250,2 +261,18 @@ this.addHandlebarsHelper(name, callback);

// Liquid, Nunjucks, and JS only
addAsyncFilter(name, callback) {
debug("Adding universal async filter %o", this.getNamespacedName(name));
// namespacing happens downstream
this.addLiquidFilter(name, callback);
this.addJavaScriptFunction(name, callback);
this.addNunjucksAsyncFilter(name, async function (...args) {
let cb = args.pop();
let ret = await callback.call(this, ...args);
cb(null, ret);
});
// Note: no handlebars
}
getFilter(name) {

@@ -270,14 +297,6 @@ return (

if (this.nunjucksTags[name]) {
debug(
chalk.yellow(
"Warning, overwriting a Nunjucks tag with `addNunjucksTag(%o)`"
),
name
);
debug(chalk.yellow("Warning, overwriting a Nunjucks tag with `addNunjucksTag(%o)`"), name);
}
this.nunjucksTags[name] = this.benchmarks.config.add(
`"${name}" Nunjucks Custom Tag`,
tagFn
);
this.nunjucksTags[name] = this.benchmarks.config.add(`"${name}" Nunjucks Custom Tag`, tagFn);
}

@@ -296,5 +315,3 @@

debug(
chalk.yellow(
"Warning, overwriting a Nunjucks global with `addNunjucksGlobal(%o)`"
),
chalk.yellow("Warning, overwriting a Nunjucks global with `addNunjucksGlobal(%o)`"),
name

@@ -317,3 +334,3 @@ );

this.transforms[name] = callback;
this.transforms[name] = this.benchmarks.config.add(`"${name}" Transform`, callback);
}

@@ -324,3 +341,3 @@

this.linters[name] = callback;
this.linters[name] = this.benchmarks.config.add(`"${name}" Linter`, callback);
}

@@ -332,2 +349,11 @@

setLayoutResolution(resolution) {
this.layoutResolution = !!resolution;
}
// compat
enableLayoutResolution() {
this.layoutResolution = true;
}
// get config defined collections

@@ -366,6 +392,3 @@ getCollections() {

return plugin.name;
} else if (
plugin.configFunction &&
typeof plugin.configFunction === "function"
) {
} else if (plugin.configFunction && typeof plugin.configFunction === "function") {
return plugin.configFunction.name;

@@ -378,5 +401,3 @@ }

debug(`Adding ${name || "anonymous"} plugin`);
let pluginBenchmark = this.benchmarks.aggregate.get(
"Configuration addPlugin"
);
let pluginBenchmark = this.benchmarks.aggregate.get("Configuration addPlugin");
if (typeof plugin === "function") {

@@ -426,10 +447,15 @@ pluginBenchmark.before();

* 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.
* @memberof EleventyConfig
*/
addPassthroughCopy(fileOrDir) {
addPassthroughCopy(fileOrDir, copyOptions = {}) {
if (typeof fileOrDir === "string") {
this.passthroughCopies[fileOrDir] = true;
this.passthroughCopies[fileOrDir] = { outputPath: true, copyOptions };
} else {
Object.assign(this.passthroughCopies, fileOrDir);
for (let [inputPath, outputPath] of Object.entries(fileOrDir)) {
this.passthroughCopies[inputPath] = { outputPath, copyOptions };
}
}

@@ -440,9 +466,22 @@

_normalizeTemplateFormats(templateFormats) {
if (typeof templateFormats === "string") {
templateFormats = templateFormats
.split(",")
.map((format) => format.trim());
_normalizeTemplateFormats(templateFormats, existingValues) {
// setTemplateFormats(null) should return null
if (templateFormats === null || templateFormats === undefined) {
return null;
}
return templateFormats;
let set = new Set();
if (Array.isArray(templateFormats)) {
set = new Set(templateFormats.map((format) => format.trim()));
} else if (typeof templateFormats === "string") {
for (let format of templateFormats.split(",")) {
set.add(format.trim());
}
}
for (let format of existingValues || []) {
set.add(format);
}
return Array.from(set);
}

@@ -456,7 +495,5 @@

addTemplateFormats(templateFormats) {
if (!this.templateFormatsAdded) {
this.templateFormatsAdded = [];
}
this.templateFormatsAdded = this.templateFormatsAdded.concat(
this._normalizeTemplateFormats(templateFormats)
this.templateFormatsAdded = this._normalizeTemplateFormats(
templateFormats,
this.templateFormatsAdded
);

@@ -471,6 +508,3 @@ }

);
} else if (
engineName === "njk" &&
Object.keys(this.nunjucksEnvironmentOptions).length
) {
} else if (engineName === "njk" && Object.keys(this.nunjucksEnvironmentOptions).length) {
debug(

@@ -484,2 +518,12 @@ "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."

/* 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);
}
setPugOptions(options) {

@@ -497,2 +541,6 @@ this.pugOptions = options;

setNunjucksPrecompiledTemplates(templates) {
this.nunjucksPrecompiledTemplates = templates;
}
setEjsOptions(options) {

@@ -511,16 +559,26 @@ this.ejsOptions = options;

addShortcode(name, callback) {
// This method *requires* `async function` and will not work with `function` that returns a promise
if (callback instanceof ComparisonAsyncFunction) {
this.addAsyncShortcode(name, callback); // Note: no handlebars
return;
}
debug("Adding universal shortcode %o", this.getNamespacedName(name));
this.addLiquidShortcode(name, callback);
this.addJavaScriptFunction(name, callback);
this.addNunjucksShortcode(name, callback);
this.addLiquidShortcode(name, callback);
// Note: Handlebars is sync-only
this.addHandlebarsShortcode(name, callback);
this.addJavaScriptFunction(name, callback);
}
// Undocumented method as a mitigation to reduce risk of #498
addAsyncShortcode(name, callback) {
debug("Adding universal async shortcode %o", this.getNamespacedName(name));
// Related: #498
this.addNunjucksAsyncShortcode(name, callback);
this.addLiquidShortcode(name, callback);
this.addJavaScriptFunction(name, callback);
// not supported in Handlebars
// Note: Handlebars is not async-friendly
}

@@ -554,5 +612,3 @@

debug(
chalk.yellow(
"Warning, overwriting a Nunjucks Shortcode with `addNunjucksShortcode(%o)`"
),
chalk.yellow("Warning, overwriting a Nunjucks Shortcode with `addNunjucksShortcode(%o)`"),
name

@@ -574,5 +630,3 @@ );

debug(
chalk.yellow(
"Warning, overwriting a Liquid Shortcode with `addLiquidShortcode(%o)`"
),
chalk.yellow("Warning, overwriting a Liquid Shortcode with `addLiquidShortcode(%o)`"),
name

@@ -607,7 +661,15 @@ );

addPairedShortcode(name, callback) {
// This method *requires* `async function` and will not work with `function` that returns a promise
if (callback instanceof ComparisonAsyncFunction) {
this.addPairedAsyncShortcode(name, callback); // Note: no handlebars
return;
}
debug("Adding universal paired shortcode %o", this.getNamespacedName(name));
this.addPairedNunjucksShortcode(name, callback);
this.addPairedLiquidShortcode(name, callback);
this.addJavaScriptFunction(name, callback);
// Note: Handlebars is sync-only
this.addPairedHandlebarsShortcode(name, callback);
this.addJavaScriptFunction(name, callback);
}

@@ -617,10 +679,7 @@

addPairedAsyncShortcode(name, callback) {
debug(
"Adding universal async paired shortcode %o",
this.getNamespacedName(name)
);
debug("Adding universal async paired shortcode %o", this.getNamespacedName(name));
this.addPairedNunjucksAsyncShortcode(name, callback);
this.addPairedLiquidShortcode(name, callback);
this.addJavaScriptFunction(name, callback);
// not supported in Handlebars
// Note: Handlebars is sync-only
}

@@ -739,10 +798,17 @@

setBrowserSyncConfig(options = {}, mergeOptions = true) {
if (mergeOptions) {
this.browserSyncConfig = merge(this.browserSyncConfig, options);
setServerOptions(options = {}, override = false) {
if (override) {
this.serverOptions = options;
} else {
this.browserSyncConfig = options;
this.serverOptions = merge(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 = {}) {

@@ -765,15 +831,46 @@ this.chokidarConfig = options;

addExtension(fileExtension, options = {}) {
this.extensionMap.add(
Object.assign(
{
key: fileExtension,
extension: 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) {
this.extensionMap.add(
Object.assign(
{
key: extension,
extension: extension,
},
options
)
);
}
}
addDataExtension(formatExtension, formatParser) {
this.dataExtensions.set(formatExtension, formatParser);
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,
});
}
}

@@ -789,4 +886,22 @@

// "passthrough" is the default, no other value is explicitly required in code
// but opt-out via "copy" is suggested
setServerPassthroughCopyBehavior(behavior) {
this.serverPassthroughCopyBehavior = behavior;
}
addUrlTransform(callback) {
this.urlTransforms.push(callback);
}
setDataFileSuffixes(suffixArray) {
this.dataFileSuffixesOverride = suffixArray;
}
setDataFileBaseName(baseName) {
this.dataFileDirBaseNameOverride = baseName;
}
getMergingConfigObject() {
return {
let obj = {
templateFormats: this.templateFormats,

@@ -799,2 +914,3 @@ templateFormatsAdded: this.templateFormatsAdded,

layoutAliases: this.layoutAliases,
layoutResolution: this.layoutResolution,
passthroughCopies: this.passthroughCopies,

@@ -807,2 +923,3 @@ liquidOptions: this.liquidOptions,

nunjucksEnvironmentOptions: this.nunjucksEnvironmentOptions,
nunjucksPrecompiledTemplates: this.nunjucksPrecompiledTemplates,
nunjucksFilters: this.nunjucksFilters,

@@ -827,6 +944,7 @@ nunjucksAsyncFilters: this.nunjucksAsyncFilters,

ignores: this.ignores,
watchIgnores: this.watchIgnores,
dataDeepMerge: this.dataDeepMerge,
watchJavaScriptDependencies: this.watchJavaScriptDependencies,
additionalWatchTargets: this.additionalWatchTargets,
browserSyncConfig: this.browserSyncConfig,
serverOptions: this.serverOptions,
chokidarConfig: this.chokidarConfig,

@@ -844,3 +962,17 @@ watchThrottleWaitTime: this.watchThrottleWaitTime,

dataFilterSelectors: this.dataFilterSelectors,
libraryAmendments: this.libraryAmendments,
serverPassthroughCopyBehavior: this.serverPassthroughCopyBehavior,
urlTransforms: this.urlTransforms,
};
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;
}

@@ -847,0 +979,0 @@ }

@@ -21,4 +21,31 @@ const EventEmitter = require("events");

}
/**
* @param {string} type - The event name to emit.
* @param {*[]} args - Additional lazy-executed function arguments that get passed to listeners.
* @returns {Promise<*[]>} - Promise resolves once all listeners were invoked
*/
async emitLazy(type, ...args) {
let listeners = this.listeners(type);
if (listeners.length === 0) {
return [];
}
let argsMap = [];
for (let arg of args) {
if (typeof arg === "function") {
let r = arg();
if (r instanceof Promise) {
r = await r;
}
argsMap.push(r);
} else {
argsMap.push(arg);
}
}
return this.emit.call(this, type, ...argsMap);
}
}
module.exports = AsyncEventEmitter;

@@ -51,2 +51,7 @@ const chalk = require("kleur");

/** @param {string} msg */
info(msg) {
this.message(msg, "warn", "blue");
}
/** @param {string} msg */
warn(msg) {

@@ -56,3 +61,2 @@ this.message(msg, "warn", "yellow");

// Is this used?
/** @param {string} msg */

@@ -59,0 +63,0 @@ error(msg) {

const path = require("path");
const { TemplatePath } = require("@11ty/eleventy-utils");
const debug = require("debug")("Eleventy:DeleteRequireCache");

@@ -6,7 +8,18 @@ /**

* The keys of the nodejs require cache are file paths based on the current operating system.
* @param {string} absoluteModulePath An absolute POSIX path to the module.
* @param {string} absolutePath An absolute POSIX path to the file.
*/
module.exports = function deleteRequireCache(absoluteModulePath) {
const normalizedPath = path.normalize(absoluteModulePath);
function deleteRequireCacheAbsolute(absolutePath) {
const normalizedPath = path.normalize(absolutePath);
debug("Deleting %o from `require` cache.", normalizedPath);
delete require.cache[normalizedPath];
};
}
function deleteRequireCache(localPath) {
let absolutePath = TemplatePath.absolutePath(localPath);
deleteRequireCacheAbsolute(absolutePath);
}
module.exports = deleteRequireCache; // will transform local paths to absolute
// Export for testing only
module.exports.deleteRequireCacheAbsolute = deleteRequireCacheAbsolute;

@@ -1,10 +0,14 @@

const isPlainObject = require("./IsPlainObject");
const { isPlainObject } = require("@11ty/eleventy-utils");
const OVERRIDE_PREFIX = "override:";
function getMergedItem(target, source, parentKey) {
// if key is prefixed with OVERRIDE_PREFIX, it just keeps the new source value (no merging)
if (parentKey && parentKey.indexOf(OVERRIDE_PREFIX) === 0) {
return source;
function cleanKey(key, prefix) {
if (prefix && key.startsWith(prefix)) {
return key.slice(prefix.length);
}
return key;
}
function getMergedItem(target, source, prefixes = {}) {
let { override } = prefixes;
// deep copy objects to avoid sharing and to effect key renaming

@@ -19,22 +23,32 @@ if (!target && isPlainObject(source)) {

if (isPlainObject(source)) {
for (var key in source) {
let newKey = key;
if (key.indexOf(OVERRIDE_PREFIX) === 0) {
newKey = key.substr(OVERRIDE_PREFIX.length);
}
target[newKey] = getMergedItem(target[key], source[key], newKey);
for (let key in source) {
let overrideKey = cleanKey(key, override);
target[overrideKey] = getMergedItem(target[key], source[key], prefixes);
}
}
return target;
} else {
// number, string, class instance, etc
return source;
}
// number, string, class instance, etc
return source;
}
// The same as Merge but without override prefixes
function DeepCopy(targetObject, ...sources) {
for (let source of sources) {
if (!source) {
continue;
}
targetObject = getMergedItem(targetObject, source);
}
return targetObject;
}
function Merge(target, ...sources) {
// Remove override prefixes from root target.
if (isPlainObject(target)) {
for (var key in target) {
for (let key in target) {
if (key.indexOf(OVERRIDE_PREFIX) === 0) {
target[key.substr(OVERRIDE_PREFIX.length)] = target[key];
target[key.slice(OVERRIDE_PREFIX.length)] = target[key];
delete target[key];

@@ -45,7 +59,9 @@ }

for (var source of sources) {
for (let source of sources) {
if (!source) {
continue;
}
target = getMergedItem(target, source);
target = getMergedItem(target, source, {
override: OVERRIDE_PREFIX,
});
}

@@ -57,1 +73,2 @@

module.exports = Merge;
module.exports.DeepCopy = DeepCopy;

@@ -56,7 +56,7 @@ class Sortable {

setSortDescending() {
this.isSortAscending = false;
setSortDescending(isDescending = true) {
this.isSortAscending = !isDescending;
}
setSortAscending(isAscending) {
setSortAscending(isAscending = true) {
this.isSortAscending = isAscending;

@@ -63,0 +63,0 @@ }

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc