@11ty/eleventy
Advanced tools
Comparing version 1.0.2 to 2.0.0
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
476110
105
13679
40
40
+ Addedbcp-47-normalize@^1.1.1
+ Addediso-639-1@^2.1.15
+ Addedlodash.chunk@^4.2.0
+ Addedlodash.get@^4.4.2
+ Addedlodash.set@^4.3.2
+ Addedmicromatch@^4.0.5
+ Addedposthtml@^0.16.6
+ Addedposthtml-urls@^1.0.0
+ Added@11ty/eleventy-dev-server@1.0.4(transitive)
+ Addedany-promise@0.1.0(transitive)
+ Addedbcp-47@1.0.8(transitive)
+ Addedbcp-47-match@1.0.3(transitive)
+ Addedbcp-47-normalize@1.1.1(transitive)
+ Addeddom-serializer@1.4.1(transitive)
+ Addeddomelementtype@2.3.0(transitive)
+ Addeddomhandler@4.3.1(transitive)
+ Addeddomutils@2.8.0(transitive)
+ Addedentities@2.2.03.0.1(transitive)
+ Addedfinalhandler@1.2.0(transitive)
+ Addedhtmlparser2@7.2.0(transitive)
+ Addedhttp-equiv-refresh@1.0.0(transitive)
+ Addedis-alphabetical@1.0.4(transitive)
+ Addedis-alphanumerical@1.0.4(transitive)
+ Addedis-decimal@1.0.4(transitive)
+ Addedis-json@2.0.1(transitive)
+ Addediso-639-1@2.1.15(transitive)
+ Addedlinkify-it@4.0.1(transitive)
+ Addedliquidjs@10.14.0(transitive)
+ Addedlist-to-array@1.1.0(transitive)
+ Addedlodash.chunk@4.2.0(transitive)
+ Addedlodash.get@4.4.2(transitive)
+ Addedlodash.set@4.3.2(transitive)
+ Addedluxon@3.4.4(transitive)
+ Addedmarkdown-it@13.0.2(transitive)
+ Addedmime@3.0.0(transitive)
+ Addedminipass@3.3.6(transitive)
+ Addedmorphdom@2.7.3(transitive)
+ Addedon-finished@2.4.1(transitive)
+ Addedparse-srcset@1.0.2(transitive)
+ Addedposthtml@0.16.6(transitive)
+ Addedposthtml-parser@0.11.0(transitive)
+ Addedposthtml-render@3.0.0(transitive)
+ Addedposthtml-urls@1.0.0(transitive)
+ Addedpromise-each@2.2.0(transitive)
+ Addedssri@8.0.1(transitive)
+ Addedws@8.18.0(transitive)
+ Addedyallist@4.0.0(transitive)
- Removedbrowser-sync@^2.27.10
- Removedlodash@^4.17.21
- Removedpretty@^2.0.0
- Removed@isaacs/cliui@8.0.2(transitive)
- Removed@one-ini/wasm@0.1.1(transitive)
- Removed@pkgjs/parseargs@0.11.0(transitive)
- Removed@socket.io/component-emitter@3.1.2(transitive)
- Removed@types/cookie@0.4.1(transitive)
- Removed@types/cors@2.8.17(transitive)
- Removed@types/node@20.14.10(transitive)
- Removedabbrev@2.0.0(transitive)
- Removedaccepts@1.3.8(transitive)
- Removedansi-regex@5.0.16.0.1(transitive)
- Removedansi-styles@6.2.1(transitive)
- Removedasync@2.6.4(transitive)
- Removedasync-each-series@0.1.1(transitive)
- Removedaxios@0.21.4(transitive)
- Removedbase64id@2.0.0(transitive)
- Removedbatch@0.6.1(transitive)
- Removedbrowser-sync@2.29.3(transitive)
- Removedbrowser-sync-client@2.29.3(transitive)
- Removedbrowser-sync-ui@2.29.3(transitive)
- Removedbs-recipes@1.3.4(transitive)
- Removedbytes@3.1.2(transitive)
- Removedcliui@7.0.48.0.1(transitive)
- Removedcommander@2.20.3(transitive)
- Removedcondense-newlines@0.2.1(transitive)
- Removedconfig-chain@1.1.13(transitive)
- Removedconnect@3.6.6(transitive)
- Removedconnect-history-api-fallback@1.6.0(transitive)
- Removedcookie@0.4.2(transitive)
- Removedcors@2.8.5(transitive)
- Removeddebug@4.3.2(transitive)
- Removeddepd@1.1.22.0.0(transitive)
- Removeddestroy@1.0.4(transitive)
- Removedeastasianwidth@0.2.0(transitive)
- Removedeasy-extender@2.3.4(transitive)
- Removedeazy-logger@4.0.1(transitive)
- Removededitorconfig@1.0.4(transitive)
- Removedemoji-regex@8.0.09.2.2(transitive)
- Removedengine.io@6.5.5(transitive)
- Removedengine.io-client@6.5.4(transitive)
- Removedengine.io-parser@5.2.2(transitive)
- Removedentities@2.1.0(transitive)
- Removedescalade@3.1.2(transitive)
- Removedetag@1.8.1(transitive)
- Removedeventemitter3@4.0.7(transitive)
- Removedfinalhandler@1.1.0(transitive)
- Removedfollow-redirects@1.15.6(transitive)
- Removedforeground-child@3.2.1(transitive)
- Removedfresh@0.5.2(transitive)
- Removedfs-extra@3.0.1(transitive)
- Removedget-caller-file@2.0.5(transitive)
- Removedglob@10.4.2(transitive)
- Removedhttp-errors@1.6.32.0.0(transitive)
- Removedhttp-proxy@1.18.1(transitive)
- Removediconv-lite@0.4.24(transitive)
- Removedimmutable@3.8.2(transitive)
- Removedinherits@2.0.3(transitive)
- Removedini@1.3.8(transitive)
- Removedis-buffer@1.1.6(transitive)
- Removedis-fullwidth-code-point@3.0.0(transitive)
- Removedis-number-like@1.0.8(transitive)
- Removedis-whitespace@0.3.0(transitive)
- Removedis-wsl@1.1.0(transitive)
- Removedjackspeak@3.4.0(transitive)
- Removedjs-beautify@1.15.1(transitive)
- Removedjs-cookie@3.0.5(transitive)
- Removedjsonfile@3.0.1(transitive)
- Removedkind-of@3.2.2(transitive)
- Removedlimiter@1.1.5(transitive)
- Removedlinkify-it@3.0.3(transitive)
- Removedliquidjs@9.43.0(transitive)
- Removedlocaltunnel@2.0.2(transitive)
- Removedlodash@4.17.21(transitive)
- Removedlodash.isfinite@3.3.2(transitive)
- Removedlru-cache@10.3.0(transitive)
- Removedluxon@2.5.2(transitive)
- Removedmarkdown-it@12.3.2(transitive)
- Removedmime@1.4.1(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedminimatch@9.0.19.0.5(transitive)
- Removedminipass@7.1.2(transitive)
- Removedmitt@1.2.0(transitive)
- Removednegotiator@0.6.3(transitive)
- Removednopt@7.2.1(transitive)
- Removedon-finished@2.3.0(transitive)
- Removedopenurl@1.1.1(transitive)
- Removedopn@5.3.0(transitive)
- Removedpackage-json-from-dist@1.0.0(transitive)
- Removedpath-scurry@1.11.1(transitive)
- Removedportscanner@2.2.0(transitive)
- Removedpretty@2.0.0(transitive)
- Removedproto-list@1.2.4(transitive)
- Removedrange-parser@1.2.1(transitive)
- Removedraw-body@2.5.2(transitive)
- Removedrequire-directory@2.1.1(transitive)
- Removedrequires-port@1.0.0(transitive)
- Removedresp-modifier@6.0.2(transitive)
- Removedrx@4.1.0(transitive)
- Removedsafer-buffer@2.1.2(transitive)
- Removedsend@0.16.2(transitive)
- Removedserve-index@1.9.1(transitive)
- Removedserve-static@1.13.2(transitive)
- Removedserver-destroy@1.0.1(transitive)
- Removedsetprototypeof@1.1.01.2.0(transitive)
- Removedsignal-exit@4.1.0(transitive)
- Removedsocket.io@4.7.5(transitive)
- Removedsocket.io-adapter@2.5.5(transitive)
- Removedsocket.io-client@4.7.5(transitive)
- Removedsocket.io-parser@4.2.4(transitive)
- Removedstatuses@1.3.11.4.01.5.0(transitive)
- Removedstream-throttle@0.1.3(transitive)
- Removedstring-width@4.2.35.1.2(transitive)
- Removedstrip-ansi@6.0.17.1.0(transitive)
- Removedtoidentifier@1.0.1(transitive)
- Removedua-parser-js@1.0.38(transitive)
- Removedundici-types@5.26.5(transitive)
- Removeduniversalify@0.1.2(transitive)
- Removedutils-merge@1.0.1(transitive)
- Removedvary@1.1.2(transitive)
- Removedwrap-ansi@7.0.08.1.0(transitive)
- Removedws@8.17.1(transitive)
- Removedxmlhttprequest-ssl@2.0.0(transitive)
- Removedy18n@5.0.8(transitive)
- Removedyargs@17.1.117.7.2(transitive)
- Removedyargs-parser@20.2.921.1.1(transitive)
Updatedfast-glob@^3.2.12
Updatedliquidjs@^10.4.0
Updatedluxon@^3.2.1
Updatedmarkdown-it@^13.0.1
Updatedminimist@^1.2.7
Updatedmoo@^0.5.2
Updatedsemver@^7.3.8