Skip to content

Trying to load two modules with the same URL throws an empty object #51694

@jsinterface

Description

@jsinterface

In a loader module I'm handling sources with omitted type import attribute on JSON imports,
by assigning context.importAttributes.type in the load hook, while caching the outputs.

When the attributes are consistently omitted from JSON imports, or consistently aren't, this works well.
But when the import signature changes on one occasion, the process breaks
with an empty error object ({}) on returning the cached output.

I can't tell the reason for this, for as far as the loader is concerned,
the load hook output should be idempotent whether the input context.importAttributes.type is defined or not:

[Object: null prototype] {
  format: 'json',
  responseURL: 'file:///home/ranger/loaded.json',
  source: <Buffer 7b 22 61 22 3a 31 7d 0a>,
  shortCircuit: true
}

Here's a loader module to reproduce:

// loader.js
export const address = new URL(import.meta.url).pathname;
export const file = address.replace(/.*\//, "");

// register loader
if (
  !globalThis.window &&
  process.execArgv.some((flag) =>
    new RegExp("^--import[= ][^ ]*" + file).test(flag),
  )
)
  Promise.all(
    ["module", "worker_threads"].map((module) => import(module)),
  ).then(
    ([{ register }, { MessageChannel, isMainThread }]) =>
      isMainThread &&
      [new MessageChannel(), import.meta.url].reduce(
        ({ port1, port2 }, parentURL) =>
          register(address, parentURL, {
            data: { socket: port2 },
            transferList: [port2],
          }) || port1.on("message", console.log),
      ),
  );

function initialize({ socket }) {
  socket.postMessage(" Registered loader module:\n " + import.meta.url + ".");
}

const load = function (source, context, next) {
  if (this[source])
    // return cached value.
    return console.log(this[source]) || this[source];
  if (source.endsWith("json")) context.importAttributes.type = "json";
  return next(source, context).then(
    (context) =>
      // cache output
      (this[source] = Object.assign(context, { shortCircuit: true })),
  );
}.bind({});

export { initialize, load };

and here's a file with inconsistent attribute signatures to load
(in my real use case the inconsistency was in separate modules, one importing the other):

// loaded.js
 import json1 from "./loaded.json";
 import json2 from "./loaded.json" with {type:"json"};

console.log(json1===json2);

// loaded.json
{"a":1}

Result with node 21.1.0 (only notable difference is <Buffer > being empty in nextLoad's output already on the first import):
node --watch --import=./loader.js ./loaded.js

 Registered loader module:
 file:///home/ranger/loader.js.
(node:1498) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
[Object: null prototype] {
  format: 'json',
  responseURL: 'file:///home/ranger/loaded.json',
  source: <Buffer >,
  shortCircuit: true
}

node:internal/process/esm_loader:40
      internalBinding('errors').triggerUncaughtException(
                                ^
{}

Node.js v21.1.0
Failed running './loaded.js'

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