From aeb2217020203aa9a4f510ec2df60f7340bf2d71 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 18:54:11 +0000 Subject: [PATCH 1/3] refactor: modernize to ES6+ (Node 10) with perf improvements Bump engines.node to >=10.13.0 and modernize the loader runner: - Replace `arguments`/`Array.prototype.slice.call` and `.apply(null, arguments)` with rest parameters and spread calls across `runSyncOrAsync`, `iterateNormalLoaders`, `iteratePitchingLoaders`, and `processResource`. - Convert the normal/pitching iteration from recursion to iterative while loops so chains of already-executed or pitch-less loaders no longer grow the call stack and no longer allocate closures per skipped loader. - Add `escapeHash`, a short-circuiting `#`-escape helper, so the `request` and `resource` getters skip the regex scan when the input contains no `#`. - Replace `.slice().map().concat().join("!")` request composition with a single `joinRequests` loop (and a dedicated small loop for `previousRequest`), avoiding the intermediate arrays. - Destructure the object-form `request` assignment, collapse the `null`/`undefined` options branch, and clean up the getters with arrow helpers and `.slice()` for dependency getters. - In `loadLoader.js`, drop the legacy `process.nextTick` EMFILE fallback and use `setImmediate(loadLoader, loader, callback)` directly, and remove the obsolete eslint-disable for `pathToFileURL`. - Drop the `Error.captureStackTrace` shim in `LoaderLoadingError` since Node 10+ captures stack traces correctly for subclassed Errors. --- lib/LoaderLoadingError.js | 3 - lib/LoaderRunner.js | 355 ++++++++++++++++++++------------------ lib/loadLoader.js | 52 +++--- package.json | 2 +- 4 files changed, 209 insertions(+), 203 deletions(-) diff --git a/lib/LoaderLoadingError.js b/lib/LoaderLoadingError.js index 343364e..d85a884 100644 --- a/lib/LoaderLoadingError.js +++ b/lib/LoaderLoadingError.js @@ -4,9 +4,6 @@ class LoadingLoaderError extends Error { constructor(message) { super(message); this.name = "LoaderRunnerError"; - // For old Node.js engines remove it then we drop them support - // eslint-disable-next-line unicorn/no-useless-error-capture-stack-trace - Error.captureStackTrace(this, this.constructor); } } diff --git a/lib/LoaderRunner.js b/lib/LoaderRunner.js index 490bef9..a11b7a8 100644 --- a/lib/LoaderRunner.js +++ b/lib/LoaderRunner.js @@ -11,12 +11,22 @@ const readFile = fs.readFile.bind(fs); const loadLoader = require("./loadLoader"); +const BOM = 0xfeff; +const HASH_ESCAPE_REGEXP = /#/g; + function utf8BufferToString(buf) { const str = buf.toString("utf8"); - if (str.charCodeAt(0) === 0xfeff) { - return str.slice(1); - } - return str; + return str.charCodeAt(0) === BOM ? str.slice(1) : str; +} + +/** + * Escape `#` characters with a preceding `\0` byte. Short-circuits when the + * input contains no `#`, avoiding the regex scan for the common case. + * @param {string} str input string + * @returns {string} escaped string + */ +function escapeHash(str) { + return str.includes("#") ? str.replace(HASH_ESCAPE_REGEXP, "\0#") : str; } const PATH_QUERY_FRAGMENT_REGEXP = @@ -105,11 +115,7 @@ function createLoaderObject(loader) { Object.defineProperty(obj, "request", { enumerable: true, get() { - return ( - obj.path.replace(/#/g, "\0#") + - obj.query.replace(/#/g, "\0#") + - obj.fragment - ); + return escapeHash(obj.path) + escapeHash(obj.query) + obj.fragment; }, set(value) { if (typeof value === "string") { @@ -119,32 +125,34 @@ function createLoaderObject(loader) { obj.fragment = fragment; obj.options = undefined; obj.ident = undefined; + return; + } + + if (!value.loader) { + throw new Error( + `request should be a string or object with loader and options (${JSON.stringify( + value + )})` + ); + } + + const { loader: path, fragment, type, options, ident } = value; + obj.path = path; + obj.fragment = fragment || ""; + obj.type = type; + obj.options = options; + obj.ident = ident; + + if (options === null || options === undefined) { + obj.query = ""; + } else if (typeof options === "string") { + obj.query = `?${options}`; + } else if (ident) { + obj.query = `??${ident}`; + } else if (typeof options === "object" && options.ident) { + obj.query = `??${options.ident}`; } else { - if (!value.loader) { - throw new Error( - `request should be a string or object with loader and options (${JSON.stringify( - value - )})` - ); - } - obj.path = value.loader; - obj.fragment = value.fragment || ""; - obj.type = value.type; - obj.options = value.options; - obj.ident = value.ident; - if (obj.options === null) { - obj.query = ""; - } else if (obj.options === undefined) { - obj.query = ""; - } else if (typeof obj.options === "string") { - obj.query = `?${obj.options}`; - } else if (obj.ident) { - obj.query = `??${obj.ident}`; - } else if (typeof obj.options === "object" && obj.options.ident) { - obj.query = `??${obj.options.ident}`; - } else { - obj.query = `?${JSON.stringify(obj.options)}`; - } + obj.query = `?${JSON.stringify(options)}`; } }, }); @@ -162,7 +170,9 @@ function runSyncOrAsync(fn, context, args, callback) { let reportedError = false; // eslint-disable-next-line func-name-matching - const innerCallback = (context.callback = function innerCallback() { + const innerCallback = (context.callback = function innerCallback( + ...callbackArgs + ) { if (isDone) { if (reportedError) return; // ignore throw new Error("callback(): The callback was already called."); @@ -172,7 +182,7 @@ function runSyncOrAsync(fn, context, args, callback) { isSync = false; try { - callback.apply(null, arguments); + callback(...callbackArgs); } catch (err) { isError = true; throw err; @@ -237,30 +247,31 @@ function convertArgs(args, raw) { } function iterateNormalLoaders(options, loaderContext, args, callback) { - if (loaderContext.loaderIndex < 0) return callback(null, args); - - const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; + // Iterative walk: skip over executed loaders and those lacking a normal fn + // without recursing, which avoids deep stacks for chains of sync loaders. + while (loaderContext.loaderIndex >= 0) { + const currentLoaderObject = + loaderContext.loaders[loaderContext.loaderIndex]; + + if (currentLoaderObject.normalExecuted) { + loaderContext.loaderIndex--; + continue; + } - // iterate - if (currentLoaderObject.normalExecuted) { - loaderContext.loaderIndex--; - return iterateNormalLoaders(options, loaderContext, args, callback); - } + const fn = currentLoaderObject.normal; + currentLoaderObject.normalExecuted = true; - const fn = currentLoaderObject.normal; - currentLoaderObject.normalExecuted = true; - if (!fn) { - return iterateNormalLoaders(options, loaderContext, args, callback); - } + if (!fn) continue; - convertArgs(args, currentLoaderObject.raw); + convertArgs(args, currentLoaderObject.raw); - runSyncOrAsync(fn, loaderContext, args, function runSyncOrAsyncCallback(err) { - if (err) return callback(err); + return runSyncOrAsync(fn, loaderContext, args, (err, ...nextArgs) => { + if (err) return callback(err); + iterateNormalLoaders(options, loaderContext, nextArgs, callback); + }); + } - const args = Array.prototype.slice.call(arguments, 1); - iterateNormalLoaders(options, loaderContext, args, callback); - }); + return callback(null, args); } function processResource(options, loaderContext, callback) { @@ -269,72 +280,85 @@ function processResource(options, loaderContext, callback) { const { resourcePath } = loaderContext; - if (resourcePath) { - options.processResource( - loaderContext, - resourcePath, - function processResourceCallback(err) { - if (err) return callback(err); - const args = Array.prototype.slice.call(arguments, 1); + if (!resourcePath) { + return iterateNormalLoaders(options, loaderContext, [null], callback); + } - [options.resourceBuffer] = args; + options.processResource(loaderContext, resourcePath, (err, ...args) => { + if (err) return callback(err); - iterateNormalLoaders(options, loaderContext, args, callback); - } - ); - } else { - iterateNormalLoaders(options, loaderContext, [null], callback); - } + [options.resourceBuffer] = args; + + iterateNormalLoaders(options, loaderContext, args, callback); + }); } function iteratePitchingLoaders(options, loaderContext, callback) { - // abort after last loader - if (loaderContext.loaderIndex >= loaderContext.loaders.length) { - return processResource(options, loaderContext, callback); + // Iterative walk over already-pitched loaders without recursion. + while (loaderContext.loaderIndex < loaderContext.loaders.length) { + const currentLoaderObject = + loaderContext.loaders[loaderContext.loaderIndex]; + + if (currentLoaderObject.pitchExecuted) { + loaderContext.loaderIndex++; + continue; + } + + return loadLoader(currentLoaderObject, (err) => { + if (err) { + loaderContext.cacheable(false); + return callback(err); + } + const fn = currentLoaderObject.pitch; + currentLoaderObject.pitchExecuted = true; + if (!fn) return iteratePitchingLoaders(options, loaderContext, callback); + + runSyncOrAsync( + fn, + loaderContext, + [ + loaderContext.remainingRequest, + loaderContext.previousRequest, + (currentLoaderObject.data = {}), + ], + (pitchErr, ...args) => { + if (pitchErr) return callback(pitchErr); + // Determine whether to continue the pitching process based on + // argument values (as opposed to argument presence) in order + // to support synchronous and asynchronous usages. + const hasArg = args.some((value) => value !== undefined); + if (hasArg) { + loaderContext.loaderIndex--; + iterateNormalLoaders(options, loaderContext, args, callback); + } else { + iteratePitchingLoaders(options, loaderContext, callback); + } + } + ); + }); } - const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; + // Reached the end: move on to processing the resource itself. + return processResource(options, loaderContext, callback); +} - // iterate - if (currentLoaderObject.pitchExecuted) { - loaderContext.loaderIndex++; - return iteratePitchingLoaders(options, loaderContext, callback); +/** + * Join loader requests into a single `!`-separated string for a range of + * loader indices. Appends the resource (or "") to mirror the original + * `map().concat(resource || "").join("!")` semantics while avoiding the + * intermediate arrays. + * @param {object[]} loaders loader objects + * @param {number} start inclusive start index + * @param {number} end exclusive end index + * @param {string} resource resource string + * @returns {string} joined request + */ +function joinRequests(loaders, start, end, resource) { + let result = ""; + for (let i = start; i < end; i++) { + result += `${loaders[i].request}!`; } - - // load loader module - loadLoader(currentLoaderObject, (err) => { - if (err) { - loaderContext.cacheable(false); - return callback(err); - } - const fn = currentLoaderObject.pitch; - currentLoaderObject.pitchExecuted = true; - if (!fn) return iteratePitchingLoaders(options, loaderContext, callback); - - runSyncOrAsync( - fn, - loaderContext, - [ - loaderContext.remainingRequest, - loaderContext.previousRequest, - (currentLoaderObject.data = {}), - ], - function runSyncOrAsyncCallback(err) { - if (err) return callback(err); - const args = Array.prototype.slice.call(arguments, 1); - // Determine whether to continue the pitching process based on - // argument values (as opposed to argument presence) in order - // to support synchronous and asynchronous usages. - const hasArg = args.some((value) => value !== undefined); - if (hasArg) { - loaderContext.loaderIndex--; - iterateNormalLoaders(options, loaderContext, args, callback); - } else { - iteratePitchingLoaders(options, loaderContext, callback); - } - } - ); - }); + return result + resource; } module.exports.getContext = function getContext(resource) { @@ -345,13 +369,12 @@ module.exports.getContext = function getContext(resource) { module.exports.runLoaders = function runLoaders(options, callback) { // read options const resource = options.resource || ""; - let loaders = options.loaders || []; const loaderContext = options.context || {}; - const processResource = + const processResourceFn = options.processResource || - ((readResource, context, resource, callback) => { - context.addDependency(resource); - readResource(resource, callback); + ((readResource, context, res, cb) => { + context.addDependency(res); + readResource(res, cb); }).bind(null, options.readResource || readFile); const splittedResource = resource && parseIdentifier(resource); @@ -367,7 +390,7 @@ module.exports.runLoaders = function runLoaders(options, callback) { const missingDependencies = []; // prepare loader objects - loaders = loaders.map(createLoaderObject); + const loaders = (options.loaders || []).map(createLoaderObject); loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; @@ -377,31 +400,24 @@ module.exports.runLoaders = function runLoaders(options, callback) { loaderContext.resourceFragment = resourceFragment; loaderContext.async = null; loaderContext.callback = null; - loaderContext.cacheable = function cacheable(flag) { + loaderContext.cacheable = (flag) => { if (flag === false) { requestCacheable = false; } }; - loaderContext.dependency = loaderContext.addDependency = - function addDependency(file) { - fileDependencies.push(file); - }; - loaderContext.addContextDependency = function addContextDependency(context) { + loaderContext.dependency = loaderContext.addDependency = (file) => { + fileDependencies.push(file); + }; + loaderContext.addContextDependency = (context) => { contextDependencies.push(context); }; - loaderContext.addMissingDependency = function addMissingDependency(context) { + loaderContext.addMissingDependency = (context) => { missingDependencies.push(context); }; - loaderContext.getDependencies = function getDependencies() { - return [...fileDependencies]; - }; - loaderContext.getContextDependencies = function getContextDependencies() { - return [...contextDependencies]; - }; - loaderContext.getMissingDependencies = function getMissingDependencies() { - return [...missingDependencies]; - }; - loaderContext.clearDependencies = function clearDependencies() { + loaderContext.getDependencies = () => fileDependencies.slice(); + loaderContext.getContextDependencies = () => contextDependencies.slice(); + loaderContext.getMissingDependencies = () => missingDependencies.slice(); + loaderContext.clearDependencies = () => { fileDependencies.length = 0; contextDependencies.length = 0; missingDependencies.length = 0; @@ -411,68 +427,71 @@ module.exports.runLoaders = function runLoaders(options, callback) { enumerable: true, get() { return ( - loaderContext.resourcePath.replace(/#/g, "\0#") + - loaderContext.resourceQuery.replace(/#/g, "\0#") + + escapeHash(loaderContext.resourcePath) + + escapeHash(loaderContext.resourceQuery) + loaderContext.resourceFragment ); }, set(value) { - const splittedResource = value && parseIdentifier(value); - loaderContext.resourcePath = splittedResource ? splittedResource[0] : ""; - loaderContext.resourceQuery = splittedResource ? splittedResource[1] : ""; - loaderContext.resourceFragment = splittedResource - ? splittedResource[2] - : ""; + const splitted = value && parseIdentifier(value); + loaderContext.resourcePath = splitted ? splitted[0] : ""; + loaderContext.resourceQuery = splitted ? splitted[1] : ""; + loaderContext.resourceFragment = splitted ? splitted[2] : ""; }, }); Object.defineProperty(loaderContext, "request", { enumerable: true, get() { - return loaderContext.loaders - .map((loader) => loader.request) - .concat(loaderContext.resource || "") - .join("!"); + return joinRequests( + loaders, + 0, + loaders.length, + loaderContext.resource || "" + ); }, }); Object.defineProperty(loaderContext, "remainingRequest", { enumerable: true, get() { - if ( - loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && - !loaderContext.resource - ) { + const resourceStr = loaderContext.resource || ""; + if (loaderContext.loaderIndex >= loaders.length - 1 && !resourceStr) { return ""; } - return loaderContext.loaders - .slice(loaderContext.loaderIndex + 1) - .map((loader) => loader.request) - .concat(loaderContext.resource || "") - .join("!"); + return joinRequests( + loaders, + loaderContext.loaderIndex + 1, + loaders.length, + resourceStr + ); }, }); Object.defineProperty(loaderContext, "currentRequest", { enumerable: true, get() { - return loaderContext.loaders - .slice(loaderContext.loaderIndex) - .map((loader) => loader.request) - .concat(loaderContext.resource || "") - .join("!"); + return joinRequests( + loaders, + loaderContext.loaderIndex, + loaders.length, + loaderContext.resource || "" + ); }, }); Object.defineProperty(loaderContext, "previousRequest", { enumerable: true, get() { - return loaderContext.loaders - .slice(0, loaderContext.loaderIndex) - .map((loader) => loader.request) - .join("!"); + const end = loaderContext.loaderIndex; + if (end === 0) return ""; + let result = loaders[0].request; + for (let i = 1; i < end; i++) { + result += `!${loaders[i].request}`; + } + return result; }, }); Object.defineProperty(loaderContext, "query", { enumerable: true, get() { - const entry = loaderContext.loaders[loaderContext.loaderIndex]; + const entry = loaders[loaderContext.loaderIndex]; return entry.options && typeof entry.options === "object" ? entry.options : entry.query; @@ -481,7 +500,7 @@ module.exports.runLoaders = function runLoaders(options, callback) { Object.defineProperty(loaderContext, "data", { enumerable: true, get() { - return loaderContext.loaders[loaderContext.loaderIndex].data; + return loaders[loaderContext.loaderIndex].data; }, }); @@ -492,7 +511,7 @@ module.exports.runLoaders = function runLoaders(options, callback) { const processOptions = { resourceBuffer: null, - processResource, + processResource: processResourceFn, }; iteratePitchingLoaders(processOptions, loaderContext, (err, result) => { if (err) { diff --git a/lib/loadLoader.js b/lib/loadLoader.js index 2300ad6..90edc11 100644 --- a/lib/loadLoader.js +++ b/lib/loadLoader.js @@ -8,9 +8,7 @@ function handleResult(loader, module, callback) { if (typeof module !== "function" && typeof module !== "object") { return callback( new LoaderLoadingError( - `Module '${ - loader.path - }' is not a loader (export function or es6 module)` + `Module '${loader.path}' is not a loader (export function or es6 module)` ) ); } @@ -25,22 +23,21 @@ function handleResult(loader, module, callback) { ) { return callback( new LoaderLoadingError( - `Module '${ - loader.path - }' is not a loader (must have normal or pitch function)` + `Module '${loader.path}' is not a loader (must have normal or pitch function)` ) ); } callback(); } -module.exports = function loadLoader(loader, callback) { +function loadLoader(loader, callback) { if (loader.type === "module") { try { if (url === undefined) url = require("url"); - // eslint-disable-next-line n/no-unsupported-features/node-builtins const loaderUrl = url.pathToFileURL(loader.path); + // Use `eval` so older parsers (and the main module resolver) don't + // need to recognize the dynamic `import()` syntax at load time. // eslint-disable-next-line no-eval const modulePromise = eval( `import(${JSON.stringify(loaderUrl.toString())})` @@ -52,29 +49,22 @@ module.exports = function loadLoader(loader, callback) { } catch (err) { callback(err); } - } else { - let loadedModule; - - try { - loadedModule = require(loader.path); - } catch (err) { - // it is possible for node to choke on a require if the FD descriptor - // limit has been reached. give it a chance to recover. - if (err instanceof Error && err.code === "EMFILE") { - const retry = loadLoader.bind(null, loader, callback); - - if (typeof setImmediate === "function") { - // node >= 0.9.0 - return setImmediate(retry); - } - - // node < 0.9.0 - return process.nextTick(retry); - } + return; + } - return callback(err); + let loadedModule; + try { + loadedModule = require(loader.path); + } catch (err) { + // It is possible for node to choke on a require if the FD descriptor + // limit has been reached. Give it a chance to recover by deferring. + if (err instanceof Error && err.code === "EMFILE") { + return setImmediate(loadLoader, loader, callback); } - - return handleResult(loader, loadedModule, callback); + return callback(err); } -}; + + return handleResult(loader, loadedModule, callback); +} + +module.exports = loadLoader; diff --git a/package.json b/package.json index fa9f0a9..7f91594 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,6 @@ "should": "^8.0.2" }, "engines": { - "node": ">=6.11.5" + "node": ">=10.13.0" } } From 555ca982bcde4d201cf087bc8ee1645e9824860b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 23:13:24 +0000 Subject: [PATCH 2/3] perf: additional hot-path optimizations - `utf8BufferToString` detects the UTF-8 BOM (EF BB BF) on the Buffer directly and passes an offset to `toString("utf8", 3)`, avoiding the need to decode then `slice(1)` a JS string for BOM-prefixed inputs. - Replace `args.some(predicate)` in the pitch callback with a tight for-loop so we don't allocate an arrow closure per pitched loader. - Use a direct `args[0]` read in `processResource` instead of array destructuring when assigning `resourceBuffer`. - Drop the now-dead `Object.preventExtensions` guards (always present on Node 10+). - Drop the `|| ""` coalesces and the redundant `remainingRequest` short-circuit: `loaderContext.resource` is always a string, and `joinRequests` naturally returns `""` when start >= end with an empty resource, so the extra branches only added overhead. --- lib/LoaderRunner.js | 56 ++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/lib/LoaderRunner.js b/lib/LoaderRunner.js index a11b7a8..fd72c97 100644 --- a/lib/LoaderRunner.js +++ b/lib/LoaderRunner.js @@ -11,12 +11,25 @@ const readFile = fs.readFile.bind(fs); const loadLoader = require("./loadLoader"); -const BOM = 0xfeff; const HASH_ESCAPE_REGEXP = /#/g; +// UTF-8 encoding of the BOM: EF BB BF +const UTF8_BOM_0 = 0xef; +const UTF8_BOM_1 = 0xbb; +const UTF8_BOM_2 = 0xbf; + function utf8BufferToString(buf) { - const str = buf.toString("utf8"); - return str.charCodeAt(0) === BOM ? str.slice(1) : str; + // Detect and skip the BOM at the buffer level to avoid materializing the + // prefix as JS string and then re-slicing it. + if ( + buf.length >= 3 && + buf[0] === UTF8_BOM_0 && + buf[1] === UTF8_BOM_1 && + buf[2] === UTF8_BOM_2 + ) { + return buf.toString("utf8", 3); + } + return buf.toString("utf8"); } /** @@ -157,9 +170,7 @@ function createLoaderObject(loader) { }, }); obj.request = loader; - if (Object.preventExtensions) { - Object.preventExtensions(obj); - } + Object.preventExtensions(obj); return obj; } @@ -287,7 +298,8 @@ function processResource(options, loaderContext, callback) { options.processResource(loaderContext, resourcePath, (err, ...args) => { if (err) return callback(err); - [options.resourceBuffer] = args; + // eslint-disable-next-line prefer-destructuring + options.resourceBuffer = args[0]; iterateNormalLoaders(options, loaderContext, args, callback); }); @@ -325,8 +337,15 @@ function iteratePitchingLoaders(options, loaderContext, callback) { if (pitchErr) return callback(pitchErr); // Determine whether to continue the pitching process based on // argument values (as opposed to argument presence) in order - // to support synchronous and asynchronous usages. - const hasArg = args.some((value) => value !== undefined); + // to support synchronous and asynchronous usages. Inline loop + // avoids allocating a predicate closure per pitched loader. + let hasArg = false; + for (let i = 0; i < args.length; i++) { + if (args[i] !== undefined) { + hasArg = true; + break; + } + } if (hasArg) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); @@ -442,26 +461,17 @@ module.exports.runLoaders = function runLoaders(options, callback) { Object.defineProperty(loaderContext, "request", { enumerable: true, get() { - return joinRequests( - loaders, - 0, - loaders.length, - loaderContext.resource || "" - ); + return joinRequests(loaders, 0, loaders.length, loaderContext.resource); }, }); Object.defineProperty(loaderContext, "remainingRequest", { enumerable: true, get() { - const resourceStr = loaderContext.resource || ""; - if (loaderContext.loaderIndex >= loaders.length - 1 && !resourceStr) { - return ""; - } return joinRequests( loaders, loaderContext.loaderIndex + 1, loaders.length, - resourceStr + loaderContext.resource ); }, }); @@ -472,7 +482,7 @@ module.exports.runLoaders = function runLoaders(options, callback) { loaders, loaderContext.loaderIndex, loaders.length, - loaderContext.resource || "" + loaderContext.resource ); }, }); @@ -505,9 +515,7 @@ module.exports.runLoaders = function runLoaders(options, callback) { }); // finish loader context - if (Object.preventExtensions) { - Object.preventExtensions(loaderContext); - } + Object.preventExtensions(loaderContext); const processOptions = { resourceBuffer: null, From 3da9861eb9f2cba2d6597c7ebbc1e275f6da493c Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Thu, 23 Apr 2026 21:15:08 +0300 Subject: [PATCH 3/3] fix: improve performance --- lib/LoaderLoadingError.js | 5 +++++ lib/LoaderRunner.js | 29 +++++++++++++++-------------- lib/loadLoader.js | 1 + package.json | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/LoaderLoadingError.js b/lib/LoaderLoadingError.js index d85a884..3ec682c 100644 --- a/lib/LoaderLoadingError.js +++ b/lib/LoaderLoadingError.js @@ -4,6 +4,11 @@ class LoadingLoaderError extends Error { constructor(message) { super(message); this.name = "LoaderRunnerError"; + + if (Error.captureStackTrace) { + // eslint-disable-next-line unicorn/no-useless-error-capture-stack-trace + Error.captureStackTrace(this, this.constructor); + } } } diff --git a/lib/LoaderRunner.js b/lib/LoaderRunner.js index fd72c97..ff4d832 100644 --- a/lib/LoaderRunner.js +++ b/lib/LoaderRunner.js @@ -5,9 +5,7 @@ "use strict"; -const fs = require("fs"); - -const readFile = fs.readFile.bind(fs); +const { readFile } = require("fs"); const loadLoader = require("./loadLoader"); @@ -33,8 +31,7 @@ function utf8BufferToString(buf) { } /** - * Escape `#` characters with a preceding `\0` byte. Short-circuits when the - * input contains no `#`, avoiding the regex scan for the common case. + * Escape `#` characters with a preceding `\0` byte. Short-circuits when the input contains no `#`, avoiding the regex scan for the common case. * @param {string} str input string * @returns {string} escaped string */ @@ -170,7 +167,9 @@ function createLoaderObject(loader) { }, }); obj.request = loader; - Object.preventExtensions(obj); + if (Object.preventExtensions) { + Object.preventExtensions(obj); + } return obj; } @@ -258,8 +257,6 @@ function convertArgs(args, raw) { } function iterateNormalLoaders(options, loaderContext, args, callback) { - // Iterative walk: skip over executed loaders and those lacking a normal fn - // without recursing, which avoids deep stacks for chains of sync loaders. while (loaderContext.loaderIndex >= 0) { const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; @@ -362,10 +359,7 @@ function iteratePitchingLoaders(options, loaderContext, callback) { } /** - * Join loader requests into a single `!`-separated string for a range of - * loader indices. Appends the resource (or "") to mirror the original - * `map().concat(resource || "").join("!")` semantics while avoiding the - * intermediate arrays. + * Join loader requests into a single `!`-separated string for a range of loader indices. * @param {object[]} loaders loader objects * @param {number} start inclusive start index * @param {number} end exclusive end index @@ -461,7 +455,12 @@ module.exports.runLoaders = function runLoaders(options, callback) { Object.defineProperty(loaderContext, "request", { enumerable: true, get() { - return joinRequests(loaders, 0, loaders.length, loaderContext.resource); + return joinRequests( + loaders, + 0, + loaders.length, + loaderContext.resource || "" + ); }, }); Object.defineProperty(loaderContext, "remainingRequest", { @@ -515,7 +514,9 @@ module.exports.runLoaders = function runLoaders(options, callback) { }); // finish loader context - Object.preventExtensions(loaderContext); + if (Object.preventExtensions) { + Object.preventExtensions(loaderContext); + } const processOptions = { resourceBuffer: null, diff --git a/lib/loadLoader.js b/lib/loadLoader.js index 90edc11..f15bcbf 100644 --- a/lib/loadLoader.js +++ b/lib/loadLoader.js @@ -35,6 +35,7 @@ function loadLoader(loader, callback) { try { if (url === undefined) url = require("url"); + // eslint-disable-next-line n/no-unsupported-features/node-builtins const loaderUrl = url.pathToFileURL(loader.path); // Use `eval` so older parsers (and the main module resolver) don't // need to recognize the dynamic `import()` syntax at load time. diff --git a/package.json b/package.json index 7f91594..fa9f0a9 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,6 @@ "should": "^8.0.2" }, "engines": { - "node": ">=10.13.0" + "node": ">=6.11.5" } }