-
-
Notifications
You must be signed in to change notification settings - Fork 33.5k
Description
Version
19.8.0
Platform
Microsoft Windows NT 10.0.19045.0 x64
Subsystem
esm
What steps will reproduce the bug?
Issue occurs when chaining 3 or more loaders
loader-a.mjs
export function resolve(id, parent, next) { console.log('loader-a', id); return next(id, parent); }loader-b.mjs
export function resolve(id, parent, next) { console.log('loader-b', id); return next(id, parent); }loader-c.mjs
export function resolve(id, parent, next) { console.log('loader-c', id); return next(id, parent); }$ node --loader ./loader-a.mjs --loader ./loader-b.mjs --loader ./loader-c.mjs a.mjs
How often does it reproduce? Is there a required condition?
No response
What is the expected behavior? Why is that the expected behavior?
The above node run would output
loader-a ./loader-b.mjs
loader-b ./loader-c.mjs
loader-a ./loader-c.mjs
loader-c <cwd>/a.mjs
loader-b <cwd>/a.mjs
loader-a <cwd>/a.mjs
In other words, each loader would be chained to resolve subsequent loaders as per #43772 and docs:
When hooks are used they apply to each subsequent loader [...]
What do you see instead?
loader-a ./loader-b.mjs
loader-b ./loader-c.mjs
loader-c <cwd>/a.mjs
loader-b <cwd>/a.mjs
loader-a <cwd>/a.mjs
Only the immediately preceding loader is used to load a loader
Additional information
This regression was introduced in #45869, first released in v19.8.0
The feature in question is implemented by added loaded loaders to the internal ESM loader by calling ESMLoader#addCustomLoaders in a loop
node/lib/internal/process/esm_loader.js
Lines 46 to 58 in 4221dce
| for (let i = 0; i < customLoaders.length; i++) { | |
| const customLoader = customLoaders[i]; | |
| // Importation must be handled by internal loader to avoid polluting user-land | |
| const keyedExportsSublist = await internalEsmLoader.import( | |
| [customLoader], | |
| parentURL, | |
| kEmptyObject, | |
| ); | |
| internalEsmLoader.addCustomLoaders(keyedExportsSublist); | |
| ArrayPrototypePushApply(allLoaders, keyedExportsSublist); | |
| } |
Before the regression, ESMLoader#addCustomLoaders mutates this.#hooks, thus multiple calls to it will add to the internal loader chain
node/lib/internal/modules/esm/loader.js
Lines 317 to 359 in 00a428e
| addCustomLoaders( | |
| customLoaders = [], | |
| ) { | |
| for (let i = 0; i < customLoaders.length; i++) { | |
| const { | |
| exports, | |
| url, | |
| } = customLoaders[i]; | |
| const { | |
| globalPreload, | |
| resolve, | |
| load, | |
| } = ESMLoader.pluckHooks(exports); | |
| if (globalPreload) { | |
| ArrayPrototypePush( | |
| this.#hooks.globalPreload, | |
| { | |
| fn: globalPreload, | |
| url, | |
| }, | |
| ); | |
| } | |
| if (resolve) { | |
| ArrayPrototypePush( | |
| this.#hooks.resolve, | |
| { | |
| fn: resolve, | |
| url, | |
| }, | |
| ); | |
| } | |
| if (load) { | |
| ArrayPrototypePush( | |
| this.#hooks.load, | |
| { | |
| fn: load, | |
| url, | |
| }, | |
| ); | |
| } | |
| } | |
| } |
In #45869, ESMLoader#addCustomLoaders was changed to (re-)instantiating a Hooks object, making multiple calls to it overwriting this.#hooks
node/lib/internal/modules/esm/loader.js
Lines 108 to 111 in 4221dce
| addCustomLoaders(userLoaders) { | |
| const { Hooks } = require('internal/modules/esm/hooks'); | |
| this.#hooks = new Hooks(userLoaders); | |
| } |