Skip to content

ESM loader chaining while loading loaders regression in 19.8.0 #47526

Closed
@clemyan

Description

@clemyan

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

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

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

addCustomLoaders(userLoaders) {
const { Hooks } = require('internal/modules/esm/hooks');
this.#hooks = new Hooks(userLoaders);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    esmIssues and PRs related to the ECMAScript Modules implementation.loadersIssues and PRs related to ES module loaders

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions