Skip to content

Commit

Permalink
esm: fix cache collision on JSON files using file: URL
Browse files Browse the repository at this point in the history
PR-URL: nodejs#49887
Fixes: nodejs#49724
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: LiviaMedeiros <livia@cirno.name>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
  • Loading branch information
aduh95 committed Oct 14, 2023
1 parent 39f107f commit 9577a00
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 10 deletions.
10 changes: 7 additions & 3 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
SafeArrayIterator,
SafeMap,
SafeSet,
StringPrototypeIncludes,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
Expand Down Expand Up @@ -440,9 +441,12 @@ translators.set('json', function jsonStrategy(url, source) {
debug(`Loading JSONModule ${url}`);
const pathname = StringPrototypeStartsWith(url, 'file:') ?
fileURLToPath(url) : null;
const shouldCheckAndPopulateCJSModuleCache =
// We want to involve the CJS loader cache only for `file:` URL with no search query and no hash.
pathname && !StringPrototypeIncludes(url, '?') && !StringPrototypeIncludes(url, '#');
let modulePath;
let module;
if (pathname) {
if (shouldCheckAndPopulateCJSModuleCache) {
modulePath = isWindows ?
StringPrototypeReplaceAll(pathname, '/', '\\') : pathname;
module = CJSModule._cache[modulePath];
Expand All @@ -454,7 +458,7 @@ translators.set('json', function jsonStrategy(url, source) {
}
}
source = stringify(source);
if (pathname) {
if (shouldCheckAndPopulateCJSModuleCache) {
// A require call could have been called on the same file during loading and
// that resolves synchronously. To make sure we always return the identical
// export, we have to check again if the module already exists or not.
Expand All @@ -481,7 +485,7 @@ translators.set('json', function jsonStrategy(url, source) {
err.message = errPath(url) + ': ' + err.message;
throw err;
}
if (pathname) {
if (shouldCheckAndPopulateCJSModuleCache) {
CJSModule._cache[modulePath] = module;
}
cjsCache.set(url, module);
Expand Down
62 changes: 61 additions & 1 deletion test/es-module/test-esm-json.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
import { describe, it, test } from 'node:test';

import { mkdir, rm, writeFile } from 'node:fs/promises';
import * as tmpdir from '../common/tmpdir.js';

import secret from '../fixtures/experimental.json' assert { type: 'json' };

Expand All @@ -21,4 +24,61 @@ describe('ESM: importing JSON', () => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

test('should load different modules when the URL is different', async (t) => {
const root = tmpdir.fileURL(`./test-esm-json-${Math.random()}/`);
try {
await mkdir(root, { recursive: true });

await t.test('json', async () => {
let i = 0;
const url = new URL('./foo.json', root);
await writeFile(url, JSON.stringify({ id: i++ }));
const absoluteURL = await import(`${url}`, {
assert: { type: 'json' },
});
await writeFile(url, JSON.stringify({ id: i++ }));
const queryString = await import(`${url}?a=2`, {
assert: { type: 'json' },
});
await writeFile(url, JSON.stringify({ id: i++ }));
const hash = await import(`${url}#a=2`, {
assert: { type: 'json' },
});
await writeFile(url, JSON.stringify({ id: i++ }));
const queryStringAndHash = await import(`${url}?a=2#a=2`, {
assert: { type: 'json' },
});

assert.notDeepStrictEqual(absoluteURL, queryString);
assert.notDeepStrictEqual(absoluteURL, hash);
assert.notDeepStrictEqual(queryString, hash);
assert.notDeepStrictEqual(absoluteURL, queryStringAndHash);
assert.notDeepStrictEqual(queryString, queryStringAndHash);
assert.notDeepStrictEqual(hash, queryStringAndHash);
});

await t.test('js', async () => {
let i = 0;
const url = new URL('./foo.mjs', root);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const absoluteURL = await import(`${url}`);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const queryString = await import(`${url}?a=1`);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const hash = await import(`${url}#a=1`);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const queryStringAndHash = await import(`${url}?a=1#a=1`);

assert.notDeepStrictEqual(absoluteURL, queryString);
assert.notDeepStrictEqual(absoluteURL, hash);
assert.notDeepStrictEqual(queryString, hash);
assert.notDeepStrictEqual(absoluteURL, queryStringAndHash);
assert.notDeepStrictEqual(queryString, queryStringAndHash);
assert.notDeepStrictEqual(hash, queryStringAndHash);
});
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
30 changes: 30 additions & 0 deletions test/es-module/test-esm-virtual-json.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { register } from 'node:module';
import assert from 'node:assert';

async function resolve(referrer, context, next) {
const result = await next(referrer, context);
const url = new URL(result.url);
url.searchParams.set('randomSeed', Math.random());
result.url = url.href;
return result;
}

function load(url, context, next) {
if (context.importAssertions.type === 'json') {
return {
shortCircuit: true,
format: 'json',
source: JSON.stringify({ data: Math.random() }),
};
}
return next(url, context);
}

register(`data:text/javascript,export ${encodeURIComponent(resolve)};export ${encodeURIComponent(load)}`);

assert.notDeepStrictEqual(
await import(fixtures.fileURL('empty.json'), { assert: { type: 'json' } }),
await import(fixtures.fileURL('empty.json'), { assert: { type: 'json' } }),
);
10 changes: 5 additions & 5 deletions test/fixtures/errors/force_colors.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ throw new Error('Should include grayed stack trace')

Error: Should include grayed stack trace
at Object.<anonymous> (/test*force_colors.js:1:7)
 at Module._compile (node:internal*modules*cjs*loader:1241:14)
 at Module._extensions..js (node:internal*modules*cjs*loader:1295:10)
 at Module.load (node:internal*modules*cjs*loader:1091:32)
 at Module._load (node:internal*modules*cjs*loader:938:12)
 at Function.executeUserEntryPoint [as runMain] (node:internal*modules*run_main:83:12)
 at Module._compile (node:internal*modules*cjs*loader:1406:14)
 at Module._extensions..js (node:internal*modules*cjs*loader:1464:10)
 at Module.load (node:internal*modules*cjs*loader:1239:32)
 at Module._load (node:internal*modules*cjs*loader:1061:12)
 at Function.executeUserEntryPoint [as runMain] (node:internal*modules*run_main:102:12)
 at node:internal*main*run_main_module:23:47

Node.js *
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Error: an exception.
at Object.<anonymous> (*typescript-sourcemapping_url_string.ts:3:7)
at Module._compile (node:internal*modules*cjs*loader:1241:14)
at Module._compile (node:internal*modules*cjs*loader:1406:14)

0 comments on commit 9577a00

Please sign in to comment.