Repository navigation
loader-returned sources always-cached when imported with assert { type: 'json' } #49724
Description
Version
v20.6.1
Platform
Linux duck 6.5.2-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 06 Sep 2023 21:01:01 +0000 x86_64 GNU/Linux
Subsystem
No response
What steps will reproduce the bug?
- clone this reproduction repo https://github.com/iambumblehead/nodejs-import-attributes-repro
- cd into the cloned directory and use
npm test
How often does it reproduce? Is there a required condition?
the issue is reproduced every time
What is the expected behavior? Why is that the expected behavior?
when json is imported with "assert" this way,
import JSONobj from './example.json' assert { type: 'json' }it should be possible for the loader to return dynamic, un-cached values to the importing file, following current behaviour around normally imported modules eg import JSobj from './example.js'
What do you see instead?
Instead, when "assert" is used to import a json file, cached results are returned to the importing file, irregardless of what source value is returned by the loader. The example test attached below fails, but should pass.
import test from 'node:test'
import assert from 'node:assert'
import module from 'node:module'
module.register('./loader.js', {
parentURL: import.meta.url
})
// _importing.js_
// ```
// import JSobj from './example.js'
// import JSONobj from './example.json' assert { type: 'json' }
//
// export { JSobj, JSONobj }
// ```
// for "module" type modules, the loader can return different versions
// of that "module" source to multiple import contexts (desired behaviour),
test('loader can return fresh source, js', async () => {
const importOne = await import('./importing.js?t=1')
const importTwo = await import('./importing.js?t=2')
assert.deepEqual(importOne.JSobj, { example: 'js-1' })
assert.deepEqual(importTwo.JSobj, { example: 'js-2' })
})
// for "json" type modules, the loader cannot return different versions
// of that "json" source to different import contexts and, instead, the
// first version is seemingly cache-returned to any subsequently-importing
// context (not-desired behaviour)
test('loader cannot return fresh source, json', async () => {
const importThree = await import('./importing.js?t=3')
const importFour = await import('./importing.js?t=4')
assert.deepEqual(importThree.JSONobj, { example: 'json-3' })
assert.deepEqual(importFour.JSONobj, { example: 'json-4' })
// ^^ fails: importFour.JSONobj == `{ example: 'json-3' }`
})Additional information
related link about import attributes https://github.com/tc39/proposal-import-attributes#history
2023-03: The proposal is renamed to Import attributes and moves back to Stage 3 (TODO: notes, slides). The restriction on the cache key is completely removed, and the keyword changes back from assert to with: import { x } from "./mod" with { type: "json" };. For compatibility with existing implementations, the assert keyword will still be supported until it's safe to remove it, if it will ever be.
Activity
aduh95 commentedon Sep 22, 2023
Currently, you can only have "one absolute URL <=> one module"; it's a limitation we inherit from the HTML spec IIRC. You need to make changes in your resolve loader so it produces a different URL every time you want a different module to be loaded – otherwise it will return the one that's already on the LoadCache.
iambumblehead commentedon Sep 22, 2023
The loader is doing this:
resolved.url = resolved.url + matchlinkThe json file alone is always returned from LoadCache regardless of url or query params
The reproduction repo seeks to demonstrate 1) the url is modified by the loader 2) json returned to the main thread is cached. What other corrections to the demo loader would return un-cached json to the unit-test?
My own numerous attempts mutating the url at the loader failed to break the cache
aduh95 commentedon Sep 22, 2023
I created a minimal repro:
/cc @nodejs/loaders
GeoffreyBooth commentedon Sep 24, 2023
Is there a minimal repro that doesn’t involve customization hooks? In particular, I want some code that can be run either in Node or in browsers, to see how browsers behave (so that we can copy their behavior).
iambumblehead commentedon Sep 25, 2023
Respectfully, the issue reported here is a "loader issue" and reproducing the issue requires a loader. Browsers do not provide loader hooks to reproduce this issue, how can they be related to this issue?
GeoffreyBooth commentedon Sep 25, 2023
Sorry, I thought it was a bug regardless and loaders were just providing the way to reproduce it. So you’re saying that the default, no hooks registered scenario is fine and matches browsers? It’s only when hooks are registered that the issue appears, and it’s unrelated to the content of the hooks themselves (as in, the hooks in question aren’t caching when they shouldn’t)?
isaacs commentedon Sep 25, 2023
I think this is actually a bug regardless, even when loaders are not used.
Minimal repro:
It seems to be caching based on the filename, not the import url. When loading JavaScript (or anything that doesn't use
assert, I guess?) it caches based on the URL, so the js test passes.iambumblehead commentedon Sep 25, 2023
Yes, the default, no hooks registered scenario is fine and matches browsers afaik.
When a hook returns a "json" modified source it is not returned to the importing module and instead a cached source is returned.
This is different from "module" and "commonjs" sources. When a hook returns "module" or "commonjs" modified sources, those modified sources are returned to the importing module.
aduh95 commentedon Sep 26, 2023
No it doesn't:
/// package.json {}Node.js will print
falsewhen browsers (Safari and Chromium, andnode --experimental-network-imports) printtrue.The reason is that the Node.js ESM loader, when loading a JSON file from a file: URL, checks the CJS cache first; the reason for this choice was that in case CJS and ESM code try to load the same JSON file, they get the same data. Maybe we should restrict that behavior to when there's no query string in the loaded URL.
isaacs commentedon Sep 26, 2023
(Or hash.)
I think that would be the most expected outcome.
./package.jsonand./package.json?x#yare completely different things from a require() point of view. They ought to be from an import point of view as well (and indeed, they are, except in this instance).Another (more complicated, probably worse, maybe more performant) approach would be to only look to the require cache if the file is loaded from disk, and cache the stat (which I believe it does already), and only return the cached value if the mtime, dev, and ino all match what it saw the last time it loaded. Would save a file read and prevent the surprise, but the reason I'm guessing it's "probably worse" is that it might be quite a bit more complexity for not much performance benefit.
iambumblehead commentedon Sep 26, 2023
Sorry everyone, I submitted my comment just after @isaacs' comment and probably should have deleted or edited that. At that time, I wasn't aware of the general issue and believed the problem was loader-specific. I agree with things @aduh95 and @isaacs are messaging and have no additional input now.
esm: fix cache collision on JSON files using file: URL
esm: fix cache collision on JSON files using file: URL
esm: fix cache collision on JSON files using file: URL
esm: fix cache collision on JSON files using file: URL
esm: fix cache collision on JSON files using file: URL
esm: fix cache collision on JSON files using file: URL
esm: fix cache collision on JSON files using file: URL