diff --git a/.gitignore b/.gitignore index c7d550f32..59f96803e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ node_modules/ .nyc_output/ coverage/ docs/_data/coverage.json -jsdoc/ # Ignore API documentation api-docs/ diff --git a/.npmignore b/.npmignore index 73d4d3fd3..474c5f0a3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,3 @@ -jsdoc docs docs-src test diff --git a/package.json b/package.json index c2700fd13..1c0a3c27e 100755 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "access": "public" }, "type": "module", - "main": "src/Eleventy.js", + "main": "./src/Eleventy.js", "exports": { "import": "./src/Eleventy.js", "require": "./src/EleventyCommonJs.cjs" @@ -45,9 +45,9 @@ "test": "npm run test:node && npm run test:ava", "test:ava": "ava --verbose --timeout 20s", "test:node": "node --test test_node/tests.js", - "jsdoc": "rm -rf jsdoc && npx jsdoc src/* -r -d jsdoc", "format": "prettier . --write", "check": "eslint src", + "check-types": "tsc", "lint-staged": "lint-staged", "coverage": "npx c8 ava && npx c8 report --reporter=json-summary && cp coverage/coverage-summary.json docs/_data/coverage.json && node cmd.cjs --config=docs/eleventy.coverage.js", "prepare": "husky" @@ -86,6 +86,7 @@ "@eslint/js": "^9.7.0", "@iarna/toml": "^2.2.5", "@mdx-js/node-loader": "^3.0.1", + "@types/node": "^20.14.12", "@vue/server-renderer": "^3.4.31", "@zachleat/noop": "^1.0.3", "ava": "^6.1.3", @@ -95,7 +96,6 @@ "eslint-config-prettier": "^9.1.0", "globals": "^15.8.0", "husky": "^9.0.11", - "jsdoc": "^4.0.3", "lint-staged": "^15.2.7", "markdown-it-emoji": "^3.0.0", "marked": "^13.0.2", @@ -106,6 +106,7 @@ "rimraf": "^6.0.1", "sass": "^1.77.8", "tsx": "^4.16.2", + "typescript": "^5.5.4", "vue": "^3.4.31", "zod": "^3.23.8", "zod-validation-error": "^3.3.0" diff --git a/src/Benchmark/Benchmark.js b/src/Benchmark/Benchmark.js index cd126fda7..df6dea7dd 100644 --- a/src/Benchmark/Benchmark.js +++ b/src/Benchmark/Benchmark.js @@ -2,7 +2,16 @@ import { performance } from "node:perf_hooks"; class Benchmark { constructor() { - this.reset(); + // TypeScript slop + this.timeSpent = 0; + this.timesCalled = 0; + this.beforeTimers = []; + } + + reset() { + this.timeSpent = 0; + this.timesCalled = 0; + this.beforeTimers = []; } getNewTimestamp() { @@ -12,12 +21,6 @@ class Benchmark { return new Date().getTime(); } - reset() { - this.timeSpent = 0; - this.timesCalled = 0; - this.beforeTimers = []; - } - incrementCount() { this.timesCalled++; } diff --git a/src/Benchmark/BenchmarkGroup.js b/src/Benchmark/BenchmarkGroup.js index 8720fbcdb..ee82f6b59 100644 --- a/src/Benchmark/BenchmarkGroup.js +++ b/src/Benchmark/BenchmarkGroup.js @@ -11,7 +11,7 @@ class BenchmarkGroup { this.benchmarks = {}; // Warning: aggregate benchmarks automatically default to false via BenchmarkManager->getBenchmarkGroup this.isVerbose = true; - this.logger = new ConsoleLogger(this.isVerbose); + this.logger = new ConsoleLogger(); this.minimumThresholdMs = 50; this.minimumThresholdPercent = 8; } @@ -31,6 +31,7 @@ class BenchmarkGroup { add(type, callback) { let benchmark = (this.benchmarks[type] = new Benchmark()); + /** @this {any} */ let fn = function (...args) { benchmark.before(); let ret = callback.call(this, ...args); diff --git a/src/Eleventy.js b/src/Eleventy.js index 0e56b5219..cdad90be0 100644 --- a/src/Eleventy.js +++ b/src/Eleventy.js @@ -39,102 +39,156 @@ const pkg = getEleventyPackageJson(); const debug = debugUtil("Eleventy"); /** + * Eleventy’s programmatic API * @module 11ty/eleventy/Eleventy - */ - -/** - * Runtime of eleventy. * - * @param {String} input - Directory or filename for input/sources files. - * @param {String} output - Directory serving as the target for writing the output files. - * @returns {module:11ty/eleventy/Eleventy~Eleventy} + * This line is required for IDE autocomplete in config files + * @typedef {import('./UserConfig.js').default} UserConfig */ -class Eleventy { - #logger; /* Console output */ - #projectPackageJson; /* userspace package.json file contents */ - #directories; /* ProjectDirectories instance */ - #templateFormats; /* ProjectTemplateFormats instance */ +class Eleventy { + /** + * Userspace package.json file contents + * @type {object|undefined} + */ + #projectPackageJson; + /** @type {ProjectTemplateFormats|undefined} */ + #templateFormats; + /** @type {ConsoleLogger|undefined} */ + #logger; + /** @type {ProjectDirectories|undefined} */ + #directories; + /** @type {boolean|undefined} */ #verboseOverride; - #isVerboseMode; // Boolean + /** @type {boolean} */ + #isVerboseMode = true; + /** @type {boolean|undefined} */ #preInitVerbose; + /** @type {boolean} */ #hasConfigInitialized = false; + /** @type {boolean} */ + #needsInit = true; + /** @type {Promise|undefined} */ + #initPromise; + /** @type {EleventyErrorHandler|undefined} */ + #errorHandler; + /** @type {Map} */ + #privateCaches = new Map(); + /** @type {boolean} */ + #isStopping = false; + /** @type {boolean|undefined} */ + #isEsm; + /** + * @typedef {object} EleventyOptions + * @property {'cli'|'script'=} source + * @property {'build'|'serve'|'watch'=} runMode + * @property {boolean=} dryRun + * @property {string=} configPath + * @property {string=} pathPrefix + * @property {boolean=} quietMode + * @property {Function=} config + * @property {string=} inputDir + + * @param {string} [input] - Directory or filename for input/sources files. + * @param {string} [output] - Directory serving as the target for writing the output files. + * @param {EleventyOptions} [options={}] + * @param {TemplateConfig} [eleventyConfig] + */ constructor(input, output, options = {}, eleventyConfig = null) { - /** @member {String} - Holds the path to the input (might be a file or folder) */ + /** + * @type {string|undefined} + * @description Holds the path to the input (might be a file or folder) + */ this.rawInput = input || undefined; - /** @member {String} - Holds the path to the output directory */ + /** + * @type {string|undefined} + * @description holds the path to the output directory + */ this.rawOutput = output || undefined; - /** @member {module:11ty/eleventy/TemplateConfig} - Override the config instance (for centralized config re-use) */ + /** + * @type {module:11ty/eleventy/TemplateConfig} + * @description Override the config instance (for centralized config re-use) + */ this.eleventyConfig = eleventyConfig; /** - * @member {Object} - Options object passed to the Eleventy constructor + * @type {EleventyOptions} + * @description Options object passed to the Eleventy constructor * @default {} */ this.options = options; /** - * @member {String} - Called via CLI (`cli`) or Programmatically (`script`) + * @type {'cli'|'script'} + * @description Called via CLI (`cli`) or Programmatically (`script`) * @default "script" */ - this.source = this.options.source || "script"; + this.source = options.source || "script"; /** - * @member {String} - One of build, serve, or watch + * @type {string} + * @description One of build, serve, or watch * @default "build" */ - this.runMode = this.options.runMode || "build"; + this.runMode = options.runMode || "build"; /** - * @member {Boolean} - Is Eleventy running in dry mode? + * @type {boolean} + * @description Is Eleventy running in dry mode? * @default false */ - this.isDryRun = this.options.dryRun ?? false; + this.isDryRun = options.dryRun ?? false; /** - * @member {Boolean} - Does the init() method still need to be run (or hasn’t finished yet) - * @default true - */ - this.needsInit = true; - - /** - * @member {Boolean} - Is this an incremental build? (only operates on a subset of input files) + * @type {boolean} + * @description Is this an incremental build? (only operates on a subset of input files) * @default false */ this.isIncremental = false; /** - * @member {String} - If an incremental build, this is the file we’re operating on. + * @type {string|undefined} + * @description If an incremental build, this is the file we’re operating on. * @default null */ this.programmaticApiIncrementalFile = undefined; /** - * @member {Boolean} - Should we process files on first run? (The --ignore-initial feature) + * @type {boolean} + * @description Should we process files on first run? (The --ignore-initial feature) * @default true */ this.isRunInitialBuild = true; /** - * @member {Number} - Number of builds run on this instance. + * @type {Number} + * @description Number of builds run on this instance. * @default 0 */ this.buildCount = 0; + + /** + * @type {Number} + * @description The timestamp of Eleventy start. + */ + this.start = this.getNewTimestamp(); } /** - * @member {String} - The path to Eleventy's config file. - * @default null + * @type {string|undefined} + * @description An override of Eleventy's default config file paths + * @default undefined */ get configPath() { return this.options.configPath; } /** - * @member {String} - The top level directory the site pretends to reside in + * @type {string} + * @description The top level directory the site pretends to reside in * @default "/" */ get pathPrefix() { @@ -180,7 +234,8 @@ class Eleventy { } /** - * @member {Object} - Initialize Eleventy environment variables + * @type {object} + * @description Initialize Eleventy environment variables * @default null */ // this.runMode need to be set before this @@ -191,13 +246,15 @@ class Eleventy { await this.eleventyConfig.init(initOverrides); /** - * @member {Object} - Initialize Eleventy’s configuration, including the user config file + * @type {object} + * @description Initialize Eleventy’s configuration, including the user config file */ this.config = this.eleventyConfig.getConfig(); // this.directories. /** - * @member {Object} - Singleton BenchmarkManager instance + * @type {object} + * @description Singleton BenchmarkManager instance */ this.bench = this.config.benchmarkManager; @@ -205,29 +262,22 @@ class Eleventy { debug("Eleventy warm up time: %o (ms)", performance.now()); } - /** @member {Number} - The timestamp of Eleventy start. */ - this.start = this.getNewTimestamp(); - - /** @member {Object} - tbd. */ + /** @type {object} */ this.eleventyServe = new EleventyServe(); this.eleventyServe.eleventyConfig = this.eleventyConfig; - /** @member {Object} - tbd. */ + /** @type {object} */ this.watchManager = new EleventyWatch(); - /** @member {Object} - tbd. */ + /** @type {object} */ this.watchTargets = new EleventyWatchTargets(this.eleventyConfig); this.watchTargets.addAndMakeGlob(this.config.additionalWatchTargets); - /** @member {Object} - tbd. */ + /** @type {object} */ this.fileSystemSearch = new FileSystemSearch(); this.#hasConfigInitialized = true; - /** - * @member {Boolean} - Is Eleventy running in verbose mode? - * @default true - */ this.setIsVerbose(this.#preInitVerbose ?? !this.config.quietMode); } @@ -238,7 +288,7 @@ class Eleventy { return new Date().getTime(); } - /** @member {module:11ty/eleventy/Util/ProjectDirectories} */ + /** @type {ProjectDirectories} */ get directories() { if (!this.#directories) { this.#directories = new ProjectDirectories(); @@ -253,17 +303,17 @@ class Eleventy { return this.#directories; } - /** @type {String} */ + /** @type {string} */ get input() { return this.directories.inputFile || this.directories.input || this.config.dir.input; } - /** @type {String} */ + /** @type {string} */ get inputFile() { return this.directories.inputFile; } - /** @type {String} */ + /** @type {string} */ get inputDir() { return this.directories.input; } @@ -275,7 +325,7 @@ class Eleventy { ); } - /** @type {String} */ + /** @type {string} */ get outputDir() { return this.directories.output || this.config.dir.output; } @@ -283,8 +333,7 @@ class Eleventy { /** * Updates the dry-run mode of Eleventy. * - * @method - * @param {Boolean} isDryRun - Shall Eleventy run in dry mode? + * @param {boolean} isDryRun - Shall Eleventy run in dry mode? */ setDryRun(isDryRun) { this.isDryRun = !!isDryRun; @@ -293,8 +342,7 @@ class Eleventy { /** * Sets the incremental build mode. * - * @method - * @param {Boolean} isIncremental - Shall Eleventy run in incremental build mode and only write the files that trigger watch updates + * @param {boolean} isIncremental - Shall Eleventy run in incremental build mode and only write the files that trigger watch updates */ setIncrementalBuild(isIncremental) { this.isIncremental = !!isIncremental; @@ -310,8 +358,7 @@ class Eleventy { /** * Set whether or not to do an initial build * - * @method - * @param {Boolean} ignoreInitialBuild - Shall Eleventy ignore the default initial build before watching in watch/serve mode? + * @param {boolean} ignoreInitialBuild - Shall Eleventy ignore the default initial build before watching in watch/serve mode? * @default true */ setIgnoreInitial(ignoreInitialBuild) { @@ -325,8 +372,7 @@ class Eleventy { /** * Updates the path prefix used in the config. * - * @method - * @param {String} pathPrefix - The new path prefix. + * @param {string} pathPrefix - The new path prefix. */ setPathPrefix(pathPrefix) { if (pathPrefix || pathPrefix === "") { @@ -338,9 +384,6 @@ class Eleventy { /** * Restarts Eleventy. - * - * @async - * @method */ async restart() { debug("Restarting"); @@ -354,8 +397,7 @@ class Eleventy { /** * Logs some statistics after a complete run of Eleventy. * - * @method - * @returns {String} ret - The log message. + * @returns {string} ret - The log message. */ logFinished() { if (!this.writer) { @@ -404,18 +446,14 @@ class Eleventy { return ret.join(" "); } - _cache(key, inst) { - if (!this._privateCaches) { - this._privateCaches = new Map(); - } - + #cache(key, inst) { if (!("caches" in inst)) { - throw new Error("To use _cache you need a `caches` getter object"); + 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); + if (this.#privateCaches.has(key)) { + let c = this.#privateCaches.get(key); for (let cacheKey in c) { inst[cacheKey] = c[cacheKey]; } @@ -425,15 +463,12 @@ class Eleventy { for (let cacheKey of inst.caches || []) { c[cacheKey] = inst[cacheKey]; } - this._privateCaches.set(key, c); + this.#privateCaches.set(key, c); } } /** * Starts Eleventy. - * - * @async - * @method */ async init(options = {}) { options = Object.assign({ viaConfigReset: false }, options); @@ -490,7 +525,7 @@ class Eleventy { if (!options.viaConfigReset) { // set or restore cache - this._cache("TemplateWriter", this.writer); + this.#cache("TemplateWriter", this.writer); } this.writer.logger = this.logger; @@ -516,7 +551,7 @@ Verbose Output: ${this.verboseMode}`; this.writer.setVerboseOutput(this.verboseMode); this.writer.setDryRun(this.isDryRun); - this.needsInit = false; + this.#needsInit = false; } // These are all set as initial global data under eleventy.env.* (see TemplateData->environmentVariables) @@ -565,17 +600,17 @@ Verbose Output: ${this.verboseMode}`; process.env.ELEVENTY_RUN_MODE = env.runMode; } - /* Setter for verbose mode */ + /** @param {boolean} value */ set verboseMode(value) { this.setIsVerbose(value); } - /* Getter for verbose mode */ + /** @type {boolean} */ get verboseMode() { return this.#isVerboseMode; } - /* Getter for Logger */ + /** @type {ConsoleLogger} */ get logger() { if (!this.#logger) { this.#logger = new ConsoleLogger(); @@ -585,7 +620,7 @@ Verbose Output: ${this.verboseMode}`; return this.#logger; } - /* Setter for Logger */ + /** @param {ConsoleLogger} logger */ set logger(logger) { this.eleventyConfig.setLogger(logger); this.#logger = logger; @@ -595,22 +630,22 @@ Verbose Output: ${this.verboseMode}`; this.logger.overrideLogger(false); } - /* Getter for error handler */ + /** @type {EleventyErrorHandler} */ get errorHandler() { - if (!this._errorHandler) { - this._errorHandler = new EleventyErrorHandler(); - this._errorHandler.isVerbose = this.verboseMode; - this._errorHandler.logger = this.logger; + if (!this.#errorHandler) { + this.#errorHandler = new EleventyErrorHandler(); + this.#errorHandler.isVerbose = this.verboseMode; + this.#errorHandler.logger = this.logger; } - return this._errorHandler; + return this.#errorHandler; } /** * Updates the verbose mode of Eleventy. * * @method - * @param {Boolean} isVerbose - Shall Eleventy run in verbose mode? + * @param {boolean} isVerbose - Shall Eleventy run in verbose mode? */ setIsVerbose(isVerbose) { if (!this.#hasConfigInitialized) { @@ -654,7 +689,7 @@ Verbose Output: ${this.verboseMode}`; * Updates the template formats of Eleventy. * * @method - * @param {String} formats - The new template formats. + * @param {string} formats - The new template formats. */ setFormats(formats) { this.templateFormats.setViaCommandLine(formats); @@ -664,7 +699,7 @@ Verbose Output: ${this.verboseMode}`; * Updates the run mode of Eleventy. * * @method - * @param {String} runMode - One of "build", "watch", or "serve" + * @param {string} runMode - One of "build", "watch", or "serve" */ setRunMode(runMode) { this.runMode = runMode; @@ -675,7 +710,7 @@ Verbose Output: ${this.verboseMode}`; * This method is also wired up to the CLI --incremental=incrementalFile * * @method - * @param {String} incrementalFile - File path (added or modified in a project) + * @param {string} incrementalFile - File path (added or modified in a project) */ setIncrementalFile(incrementalFile) { if (incrementalFile) { @@ -701,7 +736,7 @@ Verbose Output: ${this.verboseMode}`; * Reads the version of Eleventy. * * @static - * @returns {String} - The version of Eleventy. + * @returns {string} - The version of Eleventy. */ static getVersion() { return pkg.version; @@ -718,7 +753,7 @@ Verbose Output: ${this.verboseMode}`; * Shows a help message including usage. * * @static - * @returns {String} - The help message. + * @returns {string} - The help message. */ static getHelp() { return `Usage: eleventy @@ -796,13 +831,10 @@ Arguments: } /** - * tbd. - * - * @private - * @method - * @param {String} changedFilePath - File that triggered a re-run (added or modified) + * @param {string} changedFilePath - File that triggered a re-run (added or modified) + * @param {boolean} [isResetConfig] - are we doing a config reset */ - async _addFileToWatchQueue(changedFilePath, isResetConfig) { + async #addFileToWatchQueue(changedFilePath, isResetConfig) { // Currently this is only for 11ty.js deps but should be extended with usesGraph let usedByDependants = []; if (this.watchTargets) { @@ -850,7 +882,7 @@ Arguments: } // Checks the build queue to see if any configuration related files have changed - _shouldResetConfig(activeQueue = []) { + #shouldResetConfig(activeQueue = []) { if (!activeQueue.length) { return false; } @@ -862,13 +894,7 @@ Arguments: ); } - /** - * tbd. - * - * @private - * @method - */ - async _watch(isResetConfig = false) { + async #watch(isResetConfig = false) { if (this.watchManager.isBuildRunning()) { return; } @@ -899,7 +925,7 @@ Arguments: this.watchTargets.reset(); - await this._initWatchDependencies(); + await this.#initWatchDependencies(); // Add new deps to chokidar this.watcher.add(this.watchTargets.getNewTargetsSinceLastReset()); @@ -944,7 +970,7 @@ Arguments: queueSize !== 1 ? "s" : "" })`, ); - await this._watch(); + await this.#watch(); } else { this.logger.log("Watching…"); } @@ -981,7 +1007,7 @@ Arguments: "Watching JavaScript Dependencies (disable with `eleventyConfig.setWatchJavaScriptDependencies(false)`)", ); benchmark.before(); - await this._initWatchDependencies(); + await this.#initWatchDependencies(); benchmark.after(); } @@ -994,26 +1020,22 @@ Arguments: } get isEsm() { - if (this._isEsm === undefined) { + if (this.#isEsm === undefined) { try { - this._isEsm = this.projectPackageJson?.type === "module"; + this.#isEsm = this.projectPackageJson?.type === "module"; } catch (e) { debug("Could not find a project package.json for project’s ES Modules check: %O", e); - this._isEsm = false; + this.#isEsm = false; } } - return this._isEsm; + return this.#isEsm; } /** * Starts watching dependencies. - * - * @private - * @async - * @method */ - async _initWatchDependencies() { + async #initWatchDependencies() { if (!this.eleventyConfig.shouldSpiderJavaScriptDependencies()) { return; } @@ -1127,14 +1149,14 @@ Arguments: let watchRun = async (path) => { path = TemplatePath.normalize(path); try { - let isResetConfig = this._shouldResetConfig([path]); - this._addFileToWatchQueue(path, isResetConfig); + let isResetConfig = this.#shouldResetConfig([path]); + this.#addFileToWatchQueue(path, isResetConfig); clearTimeout(watchDelay); await new Promise((resolve, reject) => { watchDelay = setTimeout(async () => { - this._watch(isResetConfig).then(resolve, reject); + this.#watch(isResetConfig).then(resolve, reject); }, this.config.watchThrottleWaitTime); }); } catch (e) { @@ -1175,10 +1197,10 @@ Arguments: async stopWatch() { // Prevent multiple invocations. - if (this?._isStopping) { + if (this.#isStopping) { return; } - this._isStopping = true; + this.#isStopping = true; debug("Cleaning up chokidar and server instances, if they exist."); await this.eleventyServe.close(); @@ -1237,12 +1259,12 @@ Arguments: * @returns {Promise<{Array,ReadableStream}>} ret - tbd. */ async executeBuild(to = "fs") { - if (this.needsInit) { - if (!this._initing) { - this._initing = this.init(); + if (this.#needsInit) { + if (!this.#initPromise) { + this.#initPromise = this.init(); } - await this._initing; - this.needsInit = false; + await this.#initPromise; + this.#needsInit = false; } if (!this.writer) { @@ -1300,7 +1322,7 @@ Arguments: if (to === "ndjson") { // return a stream // TODO this outputs all ndjson rows after all the templates have been written to the stream - returnObj = this.logger.closeStream(to); + returnObj = this.logger.closeStream(); } else if (to === "json") { // Backwards compat returnObj = resolved.templates; @@ -1332,7 +1354,6 @@ Arguments: if (to === "fs") { this.logger.logWithOptions({ message: this.logFinished(), - type: "info", color: hasError ? "red" : "green", force: true, }); @@ -1421,7 +1442,3 @@ export { */ IdAttributePlugin, }; - -/** - * @typedef {import('./UserConfig.js').default} UserConfig - */ diff --git a/src/EleventyExtensionMap.js b/src/EleventyExtensionMap.js index 5241790f6..166282291 100644 --- a/src/EleventyExtensionMap.js +++ b/src/EleventyExtensionMap.js @@ -8,8 +8,10 @@ class EleventyExtensionMapConfigError extends EleventyBaseError {} class EleventyExtensionMap { constructor(config) { this.config = config; - this._spiderJsDepsCache = {}; + + /** @type {Array} */ + this.validTemplateLanguageKeys; } setFormats(formatKeys = []) { @@ -59,13 +61,11 @@ class EleventyExtensionMap { } let files = []; - this.validTemplateLanguageKeys.forEach( - function (key) { - this.getExtensionsFromKey(key).forEach(function (extension) { - files.push((dir ? dir + "/" : "") + path + "." + extension); - }); - }.bind(this), - ); + this.validTemplateLanguageKeys.forEach((key) => { + this.getExtensionsFromKey(key).forEach(function (extension) { + files.push((dir ? dir + "/" : "") + path + "." + extension); + }); + }); return files; } diff --git a/src/EleventyServe.js b/src/EleventyServe.js index fbaec98f1..890f4bc30 100644 --- a/src/EleventyServe.js +++ b/src/EleventyServe.js @@ -26,7 +26,7 @@ const DEFAULT_SERVER_OPTIONS = { class EleventyServe { constructor() { - this.logger = new ConsoleLogger(true); + this.logger = new ConsoleLogger(); this._initOptionsFetched = false; this._aliases = undefined; this._watchedFiles = new Set(); diff --git a/src/Engines/Liquid.js b/src/Engines/Liquid.js index 34f561542..91ac59fd9 100644 --- a/src/Engines/Liquid.js +++ b/src/Engines/Liquid.js @@ -57,12 +57,16 @@ class Liquid extends TemplateEngine { } static wrapFilter(name, fn) { + /** + * @this {object} + */ return function (...args) { // Set this.eleventy and this.page if (typeof this.context?.get === "function") { augmentObject(this, { source: this.context, getter: (key, context) => context.get([key]), + lazy: this.context.strictVariables, }); } @@ -271,6 +275,7 @@ class Liquid extends TemplateEngine { parseForSymbols(str) { let tokenizer = new liquidLib.Tokenizer(str); + /** @type {Array} */ let tokens = tokenizer.readTopLevelTokens(); let symbols = tokens .filter((token) => token.kind === liquidLib.TokenKind.Output) @@ -282,6 +287,7 @@ class Liquid extends TemplateEngine { } // Don’t return a boolean if permalink is a function (see TemplateContent->renderPermalink) + /** @returns {boolean|undefined} */ permalinkNeedsCompilation(str) { if (typeof str === "string") { return this.needsCompilation(str); diff --git a/src/Engines/TemplateEngine.js b/src/Engines/TemplateEngine.js index 48ad10352..05050b68b 100644 --- a/src/Engines/TemplateEngine.js +++ b/src/Engines/TemplateEngine.js @@ -106,7 +106,7 @@ class TemplateEngine { } async _testRender(str, data) { - /* TODO compile needs to pass in inputPath? */ + // @ts-ignore let fn = await this.compile(str); return fn(data); } @@ -141,12 +141,13 @@ class TemplateEngine { return true; } + /** @returns {boolean|undefined} */ permalinkNeedsCompilation(str) { - return this.needsCompilation(str); + return this.needsCompilation(); } // whether or not compile is needed or can we return the plaintext? - needsCompilation(/*str*/) { + needsCompilation(str) { return true; } diff --git a/src/Engines/Util/ContextAugmenter.js b/src/Engines/Util/ContextAugmenter.js index 0a2aa3720..dd5fbc640 100644 --- a/src/Engines/Util/ContextAugmenter.js +++ b/src/Engines/Util/ContextAugmenter.js @@ -8,6 +8,7 @@ function augmentFunction(fn, options = {}) { ); } + /** @this {object} */ return function (...args) { let context = augmentObject(this || {}, options); return fn.call(context, ...args); @@ -41,7 +42,7 @@ function augmentObject(targetObject, options = {}) { // lazy getter important for Liquid strictVariables support Object.defineProperty(targetObject, key, { - writeable: true, + writable: true, configurable: true, enumerable: true, value, diff --git a/src/Errors/EleventyBaseError.js b/src/Errors/EleventyBaseError.js index 2d7d5ee83..6e76c5f8e 100644 --- a/src/Errors/EleventyBaseError.js +++ b/src/Errors/EleventyBaseError.js @@ -5,7 +5,7 @@ class EleventyBaseError extends Error { /** * @param {string} message - The error message to display. - * @param {Error} originalError - The original error caught. + * @param {unknown} [originalError] - The original error caught. */ constructor(message, originalError) { super(message); diff --git a/src/Plugins/HtmlBasePlugin.js b/src/Plugins/HtmlBasePlugin.js index 25f17c803..bed8d490d 100644 --- a/src/Plugins/HtmlBasePlugin.js +++ b/src/Plugins/HtmlBasePlugin.js @@ -74,23 +74,30 @@ function eleventyHtmlBasePlugin(eleventyConfig, defaultOptions = {}) { }); // Apply to one URL - eleventyConfig.addFilter("htmlBaseUrl", function (url, baseOverride, pageUrlOverride) { - let base = baseOverride || opts.baseHref; + eleventyConfig.addFilter( + "htmlBaseUrl", - // Do nothing with a default base - if (base === "/") { - return url; - } + /** @this {object} */ + function (url, baseOverride, pageUrlOverride) { + let base = baseOverride || opts.baseHref; - return transformUrl(url, base, { - pathPrefix: eleventyConfig.pathPrefix, - pageUrl: pageUrlOverride || this.page?.url, - }); - }); + // Do nothing with a default base + if (base === "/") { + return url; + } + + return transformUrl(url, base, { + pathPrefix: eleventyConfig.pathPrefix, + pageUrl: pageUrlOverride || this.page?.url, + }); + }, + ); // Apply to a block of HTML eleventyConfig.addAsyncFilter( "transformWithHtmlBase", + + /** @this {object} */ function (content, baseOverride, pageUrlOverride) { let base = baseOverride || opts.baseHref; @@ -111,6 +118,8 @@ function eleventyHtmlBasePlugin(eleventyConfig, defaultOptions = {}) { // Apply to all HTML output in your project eleventyConfig.htmlTransformer.addUrlTransform( opts.extensions, + + /** @this {object} */ function (urlInMarkup) { // baseHref override is via renderTransforms filter for adding the absolute URL (e.g. https://example.com/pathPrefix/) for RSS/Atom/JSON feeds return transformUrl(urlInMarkup.trim(), this.baseHref || opts.baseHref, { diff --git a/src/Plugins/RenderPlugin.js b/src/Plugins/RenderPlugin.js index 2466bebd4..b51e68ae2 100644 --- a/src/Plugins/RenderPlugin.js +++ b/src/Plugins/RenderPlugin.js @@ -14,7 +14,10 @@ import Liquid from "../Engines/Liquid.js"; class EleventyNunjucksError extends EleventyBaseError {} -async function compile(content, templateLang, { templateConfig, extensionMap } = {}) { +/** @this {object} */ +async function compile(content, templateLang, options = {}) { + let { templateConfig, extensionMap } = options; + if (!templateConfig) { templateConfig = new TemplateConfig(null, false); templateConfig.setDirectories(new ProjectDirectories()); @@ -49,7 +52,8 @@ async function compile(content, templateLang, { templateConfig, extensionMap } = } // No templateLang default, it should infer from the inputPath. -async function compileFile(inputPath, { templateConfig, extensionMap, config } = {}, templateLang) { +async function compileFile(inputPath, options = {}, templateLang) { + let { templateConfig, extensionMap, config } = options; if (!inputPath) { throw new Error("Missing file path argument passed to the `renderFile` shortcode."); } @@ -91,6 +95,7 @@ async function compileFile(inputPath, { templateConfig, extensionMap, config } = return tr.getCompiledTemplate(content); } +/** @this {object} */ async function renderShortcodeFn(fn, data) { if (fn === undefined) { return; @@ -108,6 +113,7 @@ async function renderShortcodeFn(fn, 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 @@ -128,11 +134,11 @@ async function renderShortcodeFn(fn, data) { * * @since 1.0.0 * @param {module:11ty/eleventy/UserConfig} eleventyConfig - User-land configuration instance. - * @param {Object} options - Plugin options + * @param {object} options - Plugin options */ function eleventyRenderPlugin(eleventyConfig, options = {}) { /** - * @typedef {Object} options + * @typedef {object} options * @property {string} [tagName] - The shortcode name to render a template string. * @property {string} [tagNameFile] - The shortcode name to render a template file. * @property {module:11ty/eleventy/TemplateConfig} [templateConfig] - Configuration object @@ -328,6 +334,7 @@ function eleventyRenderPlugin(eleventyConfig, options = {}) { extensionMap = map; }); + /** @this {object} */ 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") { @@ -343,16 +350,14 @@ function eleventyRenderPlugin(eleventyConfig, options = {}) { return renderShortcodeFn.call(this, fn, data); } + /** @this {object} */ async function _renderFileShortcodeFn(inputPath, data = {}, templateLang) { - let fn = await compileFile.call( - this, - inputPath, - { - templateConfig: opts.templateConfig || templateConfig, - extensionMap, - }, - templateLang, - ); + let options = { + templateConfig: opts.templateConfig || templateConfig, + extensionMap, + }; + + let fn = await compileFile.call(this, inputPath, options, templateLang); return renderShortcodeFn.call(this, fn, data); } @@ -385,6 +390,9 @@ function eleventyRenderPlugin(eleventyConfig, options = {}) { // Will re-use the same configuration instance both at a top level and across any nested renders class RenderManager { + /** @type {Promise|undefined} */ + #hasConfigInitialized; + constructor() { this.templateConfig = new TemplateConfig(null, false); this.templateConfig.setDirectories(new ProjectDirectories()); @@ -394,19 +402,17 @@ class RenderManager { templateConfig: this.templateConfig, accessGlobalData: true, }); - - this._hasConfigInitialized = false; } async init() { - if (this._hasConfigInitialized) { - return this._hasConfigInitialized; + if (this.#hasConfigInitialized) { + return this.#hasConfigInitialized; } if (this.templateConfig.hasInitialized()) { return true; } - this._hasConfigInitialized = this.templateConfig.init(); - await this._hasConfigInitialized; + this.#hasConfigInitialized = this.templateConfig.init(); + await this.#hasConfigInitialized; return true; } diff --git a/src/TemplateConfig.js b/src/TemplateConfig.js index 7e942aa00..b0d21aa42 100644 --- a/src/TemplateConfig.js +++ b/src/TemplateConfig.js @@ -20,14 +20,8 @@ const debugDev = debugUtil("Dev:Eleventy:TemplateConfig"); /** * Config as used by the template. - * @typedef {Object} module:11ty/eleventy/TemplateConfig~TemplateConfig~config - * @property {String=} pathPrefix - The path prefix. - */ - -/** - * Object holding override information for the template config. - * @typedef {Object} module:11ty/eleventy/TemplateConfig~TemplateConfig~override - * @property {String=} pathPrefix - The path prefix. + * @typedef {object} module:11ty/eleventy/TemplateConfig~TemplateConfig~config + * @property {String} [pathPrefix] - The path prefix. */ /** @@ -52,15 +46,16 @@ class TemplateConfig { #templateFormats; #runMode; #configManuallyDefined = false; + /** @type {UserConfig} */ + #userConfig = new UserConfig(); constructor(customRootConfig, projectConfigPath) { - this.userConfig = new UserConfig(); - - /** @member {module:11ty/eleventy/TemplateConfig~TemplateConfig~override} - tbd. */ + /** @type {object} */ this.overrides = {}; /** - * @member {String} - Path to local project config. + * @type {String} + * @description Path to local project config. * @default .eleventy.js */ if (projectConfigPath !== undefined) { @@ -83,7 +78,8 @@ class TemplateConfig { if (customRootConfig) { /** - * @member {?{}} - Custom root config. + * @type {object} + * @description Custom root config. */ this.customRootConfig = customRootConfig; debug("Warning: Using custom root config!"); @@ -95,6 +91,14 @@ class TemplateConfig { this.isEsm = false; } + get userConfig() { + return this.#userConfig; + } + + get aggregateBenchmark() { + return this.userConfig.benchmarks.aggregate; + } + /* Setter for Logger */ setLogger(logger) { this.logger = logger; @@ -139,7 +143,7 @@ class TemplateConfig { * Normalises local project config file path. * * @method - * @returns {String} - The normalised local project config file path. + * @returns {String|undefined} - The normalised local project config file path. */ getLocalProjectConfigFile() { let configFiles = this.getLocalProjectConfigFiles(); @@ -224,6 +228,7 @@ class TemplateConfig { if (!this.hasConfigMerged) { throw new Error("Invalid call to .getConfig(). Needs an .init() first."); } + return this.config; } @@ -274,7 +279,7 @@ class TemplateConfig { throw new Error("Config has not yet merged. Needs `init()`."); } - return this.config.pathPrefix; + return this.config?.pathPrefix; } /** @@ -298,7 +303,7 @@ class TemplateConfig { /* * 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. + * @param {object} - a subset of the return Object from the user’s config file. */ appendToRootConfig(obj) { Object.assign(this.rootConfig, obj); @@ -307,7 +312,7 @@ class TemplateConfig { /* * Process the userland plugins from the Config * - * @param {Object} - the return Object from the user’s config file. + * @param {object} - the return Object from the user’s config file. */ async processPlugins({ dir, pathPrefix }) { this.userConfig.dir = dir; @@ -349,7 +354,7 @@ class TemplateConfig { /** * Fetches and executes the local configuration file * - * @returns {{}} merged - The merged config file object. + * @returns {Promise} merged - The merged config file object. */ async requireLocalConfigFile() { let localConfig = {}; @@ -387,13 +392,14 @@ class TemplateConfig { // Removed a check for `filters` in 3.0.0-alpha.6 (now using addTransform instead) https://www.11ty.dev/docs/config/#transforms } catch (err) { + let isModuleError = + err instanceof Error && (err?.message || "").includes("Cannot find module"); + // TODO the error message here is bad and I feel bad (needs more accurate info) return Promise.reject( new EleventyConfigError( `Error in your Eleventy config file '${path}'.` + - (err.message && err.message.includes("Cannot find module") - ? chalk.cyan(" You may need to run `npm install`.") - : ""), + (isModuleError ? chalk.cyan(" You may need to run `npm install`.") : ""), err, ), ); @@ -414,8 +420,7 @@ class TemplateConfig { /** * Merges different config files together. * - * @param {String} projectConfigPath - Path to project config. - * @returns {{}} merged - The merged config file. + * @returns {Promise} merged - The merged config file. */ async mergeConfig() { let { localConfig, exportedConfig } = await this.requireLocalConfigFile(); @@ -487,8 +492,7 @@ class TemplateConfig { await this.userConfig.events.emit("eleventy.beforeConfig", this.userConfig); - let benchmarkManager = this.userConfig.benchmarkManager.get("Aggregate"); - let pluginsBench = benchmarkManager.get("Processing plugins in config"); + let pluginsBench = this.aggregateBenchmark.get("Processing plugins in config"); pluginsBench.before(); await this.processPlugins(mergedConfig); pluginsBench.after(); diff --git a/src/UserConfig.js b/src/UserConfig.js index 1c98933fc..5a9e7b492 100644 --- a/src/UserConfig.js +++ b/src/UserConfig.js @@ -2,18 +2,17 @@ import chalk from "kleur"; import { DateTime } from "luxon"; import yaml from "js-yaml"; import matter from "gray-matter"; - import debugUtil from "debug"; + import { DeepCopy, TemplatePath } from "@11ty/eleventy-utils"; import HtmlBasePlugin from "./Plugins/HtmlBasePlugin.js"; import RenderPlugin from "./Plugins/RenderPlugin.js"; import InputPathToUrlPlugin from "./Plugins/InputPathToUrl.js"; -// import I18nPlugin from "./Plugins/I18nPlugin.js"; import isAsyncFunction from "./Util/IsAsyncFunction.js"; import objectFilter from "./Util/Objects/ObjectFilter.js"; -import EventEmitter from "./Util/AsyncEventEmitter.js"; +import AsyncEventEmitter from "./Util/AsyncEventEmitter.js"; import EleventyCompatibility from "./Util/Compatibility.js"; import EleventyBaseError from "./Errors/EleventyBaseError.js"; import BenchmarkManager from "./Benchmark/BenchmarkManager.js"; @@ -25,47 +24,83 @@ const debug = debugUtil("Eleventy:UserConfig"); class UserConfigError extends EleventyBaseError {} /** - * API to expose configuration options in user-land configuration files + * Eleventy’s user-land Configuration API * @module 11ty/eleventy/UserConfig */ class UserConfig { + /** @type {boolean} */ #pluginExecution = false; + /** @type {boolean} */ #quietModeLocked = false; + /** @type {boolean} */ #dataDeepMergeModified = false; + /** @type {number|undefined} */ + #uniqueId; constructor() { - this._uniqueId = Math.random(); + // These are completely unnecessary lines to satisfy TypeScript + this.plugins = []; + this.templateFormatsAdded = []; + this.additionalWatchTargets = []; + this.extensionMap = new Set(); + this.dataExtensions = new Map(); + this.urlTransforms = []; + this.customDateParsingCallbacks = new Set(); + this.ignores = new Set(); + this.events = new AsyncEventEmitter(); + + /** @type {object} */ + this.directories = {}; + /** @type {undefined} */ + this.logger; + /** @type {string} */ + this.dir; + /** @type {string} */ + this.pathPrefix; + this.reset(); + this.#uniqueId = Math.random(); } // Internally used in TemplateContent for cache keys _getUniqueId() { - return this._uniqueId; + return this.#uniqueId; } reset() { debug("Resetting EleventyConfig to initial values."); - this.events = new EventEmitter(); + /** @type {AsyncEventEmitter} */ + this.events = new AsyncEventEmitter(); + + /** @type {BenchmarkManager} */ this.benchmarkManager = new BenchmarkManager(); + + /** @type {object} */ this.benchmarks = { + /** @type {import('./Benchmark/BenchmarkGroup.js')} */ config: this.benchmarkManager.get("Configuration"), + /** @type {import('./Benchmark/BenchmarkGroup.js')} */ aggregate: this.benchmarkManager.get("Aggregate"), }; + /** @type {object} */ this.directoryAssignments = {}; - + /** @type {object} */ this.collections = {}; + /** @type {object} */ this.precompiledCollections = {}; this.templateFormats = undefined; this.templateFormatsAdded = []; + /** @type {object} */ this.universal = { filters: {}, shortcodes: {}, pairedShortcodes: {}, }; + /** @type {object} */ this.liquid = { options: {}, tags: {}, @@ -75,6 +110,7 @@ class UserConfig { parameterParsing: "legacy", // or builtin }; + /** @type {object} */ this.nunjucks = { // `dev: true` gives us better error messaging environmentOptions: { dev: true }, @@ -89,6 +125,7 @@ class UserConfig { asyncPairedShortcodes: {}, }; + /** @type {object} */ this.javascript = { functions: {}, filters: {}, @@ -97,14 +134,22 @@ class UserConfig { }; this.markdownHighlighter = null; + + /** @type {object} */ this.libraryOverrides = {}; + /** @type {object} */ this.passthroughCopies = {}; + + /** @type {object} */ this.layoutAliases = {}; this.layoutResolution = true; // extension-less layout files + /** @type {object} */ this.linters = {}; + /** @type {object} */ this.transforms = {}; + /** @type {object} */ this.preprocessors = {}; this.activeNamespace = ""; @@ -121,11 +166,15 @@ class UserConfig { this.dataDeepMerge = true; this.extensionMap = new Set(); + /** @type {object} */ this.extensionConflictMap = {}; this.watchJavaScriptDependencies = true; this.additionalWatchTargets = []; + /** @type {object} */ this.serverOptions = {}; + /** @type {object} */ this.globalData = {}; + /** @type {object} */ this.chokidarConfig = {}; this.watchThrottleWaitTime = 0; //ms @@ -139,6 +188,7 @@ class UserConfig { this.useTemplateCache = true; this.dataFilterSelectors = new Set(); + /** @type {object} */ this.libraryAmendments = {}; this.serverPassthroughCopyBehavior = "copy"; // or "passthrough" this.urlTransforms = []; @@ -147,6 +197,7 @@ class UserConfig { this.dataFileSuffixesOverride = false; this.dataFileDirBaseNameOverride = false; + /** @type {object} */ this.frontMatterParsingOptions = { // Set a project-wide default. // language: "yaml", @@ -160,13 +211,18 @@ class UserConfig { javascript: JavaScriptFrontMatter, // Needed for fallback behavior in the new `javascript` engine + // @ts-ignore jsLegacy: matter.engines.javascript, - // for compatibility - node: JavaScriptFrontMatter, + node: function () { + throw new Error( + "The `node` front matter type was a 3.0 canary-only feature, removed for stable release. Rename to `javascript` instead!", + ); + }, }, }; + /** @type {object} */ this.virtualTemplates = {}; this.freezeReservedData = true; this.customDateParsingCallbacks = new Set(); @@ -250,7 +306,7 @@ class UserConfig { let { description, functionName } = options; if (typeof callback !== "function") { - throw new Error(`Invalid definition for "${name}" ${description}.`); + throw new Error(`Invalid definition for "${originalName}" ${description}.`); } let name = this.getNamespacedName(originalName); @@ -266,7 +322,7 @@ class UserConfig { debug(`Adding new ${description} "%o" via \`%o(%o)\``, name, functionName, originalName); } - target[name] = this.#decorateCallback(`"${name}" ${description}`, callback, options); + target[name] = this.#decorateCallback(`"${name}" ${description}`, callback); } #decorateCallback(type, callback) { @@ -340,16 +396,20 @@ class UserConfig { this.addLiquidFilter(name, callback); this.addJavaScriptFilter(name, callback); - this.addNunjucksFilter(name, function (...args) { - // Note that `callback` is already a function as the `#add` method throws an error if not. - let ret = callback.call(this, ...args); - if (ret instanceof Promise) { - throw new Error( - `Nunjucks *is* async-friendly with \`addFilter("${name}", async function() {})\` but you need to supply an \`async function\`. You returned a promise from \`addFilter("${name}", function() {})\`. Alternatively, use the \`addAsyncFilter("${name}")\` configuration API method.`, - ); - } - return ret; - }); + this.addNunjucksFilter( + name, + /** @this {any} */ + function (...args) { + // Note that `callback` is already a function as the `#add` method throws an error if not. + let ret = callback.call(this, ...args); + if (ret instanceof Promise) { + throw new Error( + `Nunjucks *is* async-friendly with \`addFilter("${name}", async function() {})\` but you need to supply an \`async function\`. You returned a promise from \`addFilter("${name}", function() {})\`. Alternatively, use the \`addAsyncFilter("${name}")\` configuration API method.`, + ); + } + return ret; + }, + ); } // Liquid, Nunjucks, and JS only @@ -362,12 +422,16 @@ class UserConfig { this.addLiquidFilter(name, callback); this.addJavaScriptFilter(name, callback); - this.addNunjucksAsyncFilter(name, async function (...args) { - let cb = args.pop(); - // Note that `callback` is already a function as the `#add` method throws an error if not. - let ret = await callback.call(this, ...args); - cb(null, ret); - }); + this.addNunjucksAsyncFilter( + name, + /** @this {any} */ + async function (...args) { + let cb = args.pop(); + // Note that `callback` is already a function as the `#add` method throws an error if not. + let ret = await callback.call(this, ...args); + cb(null, ret); + }, + ); } /* @@ -560,7 +624,19 @@ class UserConfig { return this.#pluginExecution; } - /* Async friendly in 3.0 */ + /** + * @typedef {function|Promise|object} PluginDefinition + * @property {Function} [configFunction] + * @property {string} [eleventyPackage] + * @property {object} [eleventyPluginOptions={}] + * @property {boolean} [eleventyPluginOptions.unique] + */ + + /** + * addPlugin: async friendly in 3.0 + * + * @param {PluginDefinition} plugin + */ addPlugin(plugin, options = {}) { // First addPlugin of a unique plugin wins if (plugin?.eleventyPluginOptions?.unique && this.hasPlugin(plugin)) { @@ -580,6 +656,7 @@ class UserConfig { } } + /** @param {string} name */ resolvePlugin(name) { let filenameLookup = { "@11ty/eleventy/html-base-plugin": HtmlBasePlugin, @@ -608,6 +685,7 @@ class UserConfig { return filenameLookup[name]; } + /** @param {string|PluginDefinition} plugin */ hasPlugin(plugin) { let pluginName; if (typeof plugin === "string") { @@ -615,10 +693,12 @@ class UserConfig { } else { pluginName = this._getPluginName(plugin); } + return this.plugins.some((entry) => this._getPluginName(entry.plugin) === pluginName); } // Using Function.name https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name#examples + /** @param {PluginDefinition} plugin */ _getPluginName(plugin) { if (plugin?.eleventyPackage) { return plugin.eleventyPackage; @@ -662,6 +742,7 @@ class UserConfig { return ret; } + /** @param {string} name */ getNamespacedName(name) { return this.activeNamespace + name; } @@ -689,7 +770,6 @@ class UserConfig { * 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, copyOptions = {}) { if (typeof fileOrDir === "string") { @@ -859,6 +939,7 @@ class UserConfig { } this.extensionConflictMap[extension] = true; + /** @type {object} */ let extensionOptions = Object.assign( { // Might be overridden for aliasing in options.key diff --git a/src/Util/AsyncEventEmitter.js b/src/Util/AsyncEventEmitter.js index 4aea5fc97..d14c35da9 100644 --- a/src/Util/AsyncEventEmitter.js +++ b/src/Util/AsyncEventEmitter.js @@ -1,15 +1,21 @@ -import EventEmitter from "node:events"; +import { EventEmitter } from "node:events"; /** * This class emits events asynchronously. * It can be used for time measurements during a build. */ class AsyncEventEmitter extends EventEmitter { + // TypeScript slop + constructor(...args) { + super(...args); + } + /** * @param {string} type - The event name to emit. * @param {...*} args - Additional arguments that get passed to listeners. * @returns {Promise} - Promise resolves once all listeners were invoked */ + /** @ts-expect-error */ async emit(type, ...args) { let listeners = this.listeners(type); if (listeners.length === 0) { diff --git a/src/Util/ConsoleLogger.js b/src/Util/ConsoleLogger.js index 5cb307de0..076b215ea 100644 --- a/src/Util/ConsoleLogger.js +++ b/src/Util/ConsoleLogger.js @@ -6,40 +6,44 @@ const debug = debugUtil("Eleventy:Logger"); /** * Logger implementation that logs to STDOUT. - * @ignore + * @typedef {'error'|'log'|'warn'|'info'} LogType */ class ConsoleLogger { + /** @type {boolean} */ + #isVerbose = true; + /** @type {boolean} */ + #isChalkEnabled = true; + /** @type {object|undefined} */ + #logger; + constructor() { - /** @private */ - this._isVerbose = true; - /** @type {Readable} */ this.outputStream = new Readable({ - read(size) {}, + read() {}, }); } get isVerbose() { - return this._isVerbose; + return this.#isVerbose; } set isVerbose(verbose) { - this._isVerbose = !!verbose; + this.#isVerbose = !!verbose; } - /** @returns {boolean} */ get isChalkEnabled() { - if (this._isChalkEnabled !== undefined) { - return this._isChalkEnabled; - } - return true; + return this.#isChalkEnabled; } set isChalkEnabled(enabled) { - this._isChalkEnabled = !!enabled; + this.#isChalkEnabled = !!enabled; } overrideLogger(logger) { - this._logger = logger; + this.#logger = logger; + } + + get logger() { + return this.#logger || console; } /** @param {string} msg */ @@ -47,11 +51,15 @@ class ConsoleLogger { this.message(msg); } - /** @param {string} prefix */ - /** @param {string} message */ - /** @param {string} type */ - /** @param {string} color */ - /** @param {boolean} force */ + /** + * @typedef LogOptions + * @property {string} message + * @property {string=} prefix + * @property {LogType=} type + * @property {string=} color + * @property {boolean=} force + * @param {LogOptions} options + */ logWithOptions({ message, type, prefix, color, force }) { this.message(message, type, color, force, prefix); } @@ -90,17 +98,23 @@ class ConsoleLogger { * Formats the message to log. * * @param {string} message - The raw message to log. - * @param {'log'|'warn'|'error'} [type='log'] - The error level to log. - * @param {boolean} [chalkColor=false] - Use coloured log output? + * @param {LogType} [type='log'] - The error level to log. + * @param {string|undefined} [chalkColor=undefined] - Color name or falsy to disable * @param {boolean} [forceToConsole=false] - Enforce a log on console instead of specified target. */ - message(message, type = "log", chalkColor = false, forceToConsole = false, prefix = "[11ty]") { + message( + message, + type = "log", + chalkColor = undefined, + forceToConsole = false, + prefix = "[11ty]", + ) { if (!forceToConsole && (!this.isVerbose || process.env.DEBUG)) { debug(message); - } else if (this._logger !== false) { + } else if (this.logger !== false) { message = `${chalk.gray(prefix)} ${message.split("\n").join(`\n${chalk.gray(prefix)} `)}`; - let logger = this._logger || console; + let logger = this.logger; if (chalkColor && this.isChalkEnabled) { logger[type](chalk[chalkColor](message)); } else { diff --git a/src/Util/HtmlTransformer.js b/src/Util/HtmlTransformer.js index 8a44bd14c..5785e5835 100644 --- a/src/Util/HtmlTransformer.js +++ b/src/Util/HtmlTransformer.js @@ -4,7 +4,6 @@ import { FilePathUtil } from "./FilePathUtil.js"; class HtmlTransformer { constructor() { - this.validExtensions; // execution order is important (not order of addition/object key order) this.callbacks = {}; this.posthtmlProcessOptions = {}; diff --git a/src/Util/Objects/ObjectFilter.js b/src/Util/Objects/ObjectFilter.js index 150ece288..9ce8737dd 100644 --- a/src/Util/Objects/ObjectFilter.js +++ b/src/Util/Objects/ObjectFilter.js @@ -1,6 +1,6 @@ export default function objectFilter(obj, callback) { let newObject = {}; - for (let [key, value] of Object.entries(obj)) { + for (let [key, value] of Object.entries(obj || {})) { if (callback(value, key)) { newObject[key] = value; } diff --git a/src/Util/ProjectTemplateFormats.js b/src/Util/ProjectTemplateFormats.js index b356d7888..f37040ef1 100644 --- a/src/Util/ProjectTemplateFormats.js +++ b/src/Util/ProjectTemplateFormats.js @@ -45,9 +45,10 @@ class ProjectTemplateFormats { } isWildcard() { - return this.#isUseAll.cli || this.#isUseAll.config || false; + return this.#useAll.cli || this.#useAll.config || false; } + /** @returns {boolean} */ #isUseAll(rawFormats) { if (rawFormats === "") { return false; diff --git a/src/Util/Require.js b/src/Util/Require.js index f3c1f88ed..6fd20d45b 100644 --- a/src/Util/Require.js +++ b/src/Util/Require.js @@ -16,6 +16,7 @@ const { port1, port2 } = new MessageChannel(); // ENV variable for https://github.com/11ty/eleventy/issues/3371 if ("register" in module && !process?.env?.ELEVENTY_SKIP_ESM_RESOLVER) { module.register("./EsmResolver.js", import.meta.url, { + parentURL: import.meta.url, data: { port: port2, }, @@ -30,14 +31,20 @@ const requestPromiseCache = new Map(); // Used for JSON imports, suffering from Node warning that import assertions experimental but also // throwing an error if you try to import() a JSON file without an import assertion. +/** + * + * @returns {string|undefined} + */ function loadContents(path, options = {}) { let rawInput; + /** @type {string} */ let encoding = "utf8"; // JSON is utf8 - if ("encoding" in options) { + if (options?.encoding || options?.encoding === null) { encoding = options.encoding; } try { + // @ts-expect-error This is an error in the upstream types rawInput = fs.readFileSync(path, encoding); } catch (e) { // if file does not exist, return nothing @@ -74,6 +81,9 @@ async function dynamicImportAbsolutePath(absolutePath, type, returnRaw = false) if (absolutePath.endsWith(".json") || type === "json") { // https://v8.dev/features/import-assertions#dynamic-import() is still experimental in Node 20 let rawInput = loadContents(absolutePath); + if (!rawInput) { + return; + } return JSON.parse(rawInput); } diff --git a/src/defaultConfig.js b/src/defaultConfig.js index d30e871f3..4da849b2a 100644 --- a/src/defaultConfig.js +++ b/src/defaultConfig.js @@ -21,26 +21,26 @@ import MemoizeUtil from "./Util/MemoizeFunction.js"; */ /** - * @typedef {Object} config + * @typedef {object} config * @property {addFilter} addFilter - Register a new global filter. */ /** - * @typedef {Object} defaultConfig + * @typedef {object} defaultConfig * @property {Array} templateFormats - An array of accepted template formats. * @property {string} [pathPrefix='/'] - The directory under which all output files should be written to. * @property {string} [markdownTemplateEngine='liquid'] - Template engine to process markdown files with. * @property {string} [htmlTemplateEngine='liquid'] - Template engine to process html files with. * @property {boolean} [dataTemplateEngine=false] - Changed in v1.0 * @property {string} [jsDataFileSuffix='.11tydata'] - File suffix for jsData files. - * @property {Object} keys + * @property {object} keys * @property {string} [keys.package='pkg'] - Global data property for package.json data * @property {string} [keys.layout='layout'] * @property {string} [keys.permalink='permalink'] * @property {string} [keys.permalinkRoot='permalinkBypassOutputDir'] * @property {string} [keys.engineOverride='templateEngineOverride'] * @property {string} [keys.computed='eleventyComputed'] - * @property {Object} dir + * @property {object} dir * @property {string} [dir.input='.'] * @property {string} [dir.includes='_includes'] * @property {string} [dir.data='_data'] diff --git a/test/JavaScriptFrontMatterTest.js b/test/JavaScriptFrontMatterTest.js index ca2e93c9a..a010a8e7f 100644 --- a/test/JavaScriptFrontMatterTest.js +++ b/test/JavaScriptFrontMatterTest.js @@ -16,7 +16,7 @@ test("Custom Front Matter Parsing Options (using JavaScript node-retrieve-global let elev = new Eleventy("./test/stubs/script-frontmatter/test-default.njk", "./_site", { config: (eleventyConfig) => { eleventyConfig.setFrontMatterParsingOptions({ - language: "node", + language: "js", }); }, }); diff --git a/test/stubs/script-frontmatter/test.njk b/test/stubs/script-frontmatter/test.njk index 08e91b20d..16cabcc6f 100644 --- a/test/stubs/script-frontmatter/test.njk +++ b/test/stubs/script-frontmatter/test.njk @@ -1,4 +1,4 @@ ----node +---js import {noopSync} from "@zachleat/noop"; const myString = "Hi"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..a01ba080f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,117 @@ +{ + "include": [ + // "src/Eleventy.js", + "src/UserConfig.js", + "src/Util/ConsoleLogger.js", + ], + "exclude": [], + + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ES2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "Node16", /* Specify what module code is generated. */ + // "rootDir": "./src/", /* Specify the root folder within your source files. */ + "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + + // WARNING: this causes missing `node` types even with "types": ["node"] + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + "maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./types/", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./types/", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + // "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + // "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}