Repository: 11ty/eleventy
Pull request #3384 (2024-07-27 03:26:02)

Refactor to fix TypeScript errors in UserConfig.js
From #3097, related: #3383


Patch:
diff --git a/.gitignore b/.gitignore
index c7d550f3..59f96803 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 73d4d3fd..474c5f0a 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,4 +1,3 @@
-jsdoc
 docs
 docs-src
 test
diff --git a/package.json b/package.json
index c2700fd1..1c0a3c27 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 cd126fda..df6dea7d 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 8720fbcd..ee82f6b5 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 0e56b521..cdad90be 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 5241790f..16628229 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 fbaec98f..890f4bc3 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 34f56154..91ac59fd 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 48ad1035..05050b68 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 0a2aa372..dd5fbc64 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 2d7d5ee8..6e76c5f8 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 25f17c80..bed8d490 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 2466bebd..b51e68ae 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 7e942aa0..b0d21aa4 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<object>} 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<object>} 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 1c98933f..5a9e7b49 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<function>|object} PluginDefinition
+	 * @property {Function} [configFunction]
+	 * @property {string} [eleventyPackage]
+	 * @property {object} [eleventyPluginOptions={}]
+	 * @property {boolean} [eleventyPluginOptions.unique]
+	 */
+
+	/**
+	 * addPlugin: async friendly in 3.0
+	 *
+	 * @param {PluginDefinition} plugin
+	 */
 	addPlugin(plugin, options = {}) {
 		// First addPlugin of a unique plugin wins
 		if (plugin?.eleventyPluginOptions?.unique && this.hasPlugin(plugin)) {
@@ -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 4aea5fc9..d14c35da 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 5cb307de..076b215e 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 8a44bd14..5785e583 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 150ece28..9ce8737d 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 b356d788..f37040ef 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 f3c1f88e..6fd20d45 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 d30e871f..4da849b2 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<string>} 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/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..a01ba080
--- /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 '<reference>'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. */
+	}
+}


Test patch:
diff --git a/test/JavaScriptFrontMatterTest.js b/test/JavaScriptFrontMatterTest.js
index ca2e93c9..a010a8e7 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 08e91b20..16cabcc6 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";