From ec421263fc2485248e995cf6092b8370107124d6 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 23 Mar 2026 16:26:13 -0700 Subject: [PATCH 1/2] esm: fix source phase identity bug in loadCache eviction PR-URL: https://github.com/nodejs/node/pull/62415 Reviewed-By: Yagiz Nizipli Reviewed-By: Benjamin Gruenbaum --- lib/internal/modules/esm/module_job.js | 15 +++++++++++---- .../test-esm-wasm-source-phase-identity.mjs | 11 +++++++++++ .../test-wasm-source-phase-identity-parent.mjs | 6 ++++++ .../test-wasm-source-phase-identity.mjs | 14 ++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 test/es-module/test-esm-wasm-source-phase-identity.mjs create mode 100644 test/fixtures/es-modules/test-wasm-source-phase-identity-parent.mjs create mode 100644 test/fixtures/es-modules/test-wasm-source-phase-identity.mjs diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 929577c0da6d08..22032f79e90d44 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -145,7 +145,9 @@ class ModuleJobBase { */ syncLink(requestType) { // Store itself into the cache first before linking in case there are circular - // references in the linking. + // references in the linking. Track whether we're overwriting an existing entry + // so we know whether to remove the temporary entry in the finally block. + const hadPreviousEntry = this.loader.loadCache.get(this.url, this.type) !== undefined; this.loader.loadCache.set(this.url, this.type, this); const moduleRequests = this.module.getModuleRequests(); // Modules should be aligned with the moduleRequests array in order. @@ -169,9 +171,14 @@ class ModuleJobBase { } this.module.link(modules); } finally { - // Restore it - if it succeeds, we'll reset in the caller; Otherwise it's - // not cached and if the error is caught, subsequent attempt would still fail. - this.loader.loadCache.delete(this.url, this.type); + if (!hadPreviousEntry) { + // Remove the temporary entry. On failure this ensures subsequent attempts + // don't return a broken job. On success the caller + // (#getOrCreateModuleJobAfterResolve) will re-insert under the correct key. + this.loader.loadCache.delete(this.url, this.type); + } + // If there was a previous entry (ensurePhase() path), leave this in cache - + // it is the upgraded job and the caller will not re-insert. } return evaluationDepJobs; diff --git a/test/es-module/test-esm-wasm-source-phase-identity.mjs b/test/es-module/test-esm-wasm-source-phase-identity.mjs new file mode 100644 index 00000000000000..2d742dfcff61ab --- /dev/null +++ b/test/es-module/test-esm-wasm-source-phase-identity.mjs @@ -0,0 +1,11 @@ +// Regression test for source phase import identity with mixed eval/source +// phase imports of the same module in one parent. +import '../common/index.mjs'; +import { spawnSyncAndAssert } from '../common/child_process.js'; +import * as fixtures from '../common/fixtures.mjs'; + +spawnSyncAndAssert( + process.execPath, + ['--no-warnings', fixtures.path('es-modules/test-wasm-source-phase-identity.mjs')], + { stdout: '', stderr: '', trim: true } +); diff --git a/test/fixtures/es-modules/test-wasm-source-phase-identity-parent.mjs b/test/fixtures/es-modules/test-wasm-source-phase-identity-parent.mjs new file mode 100644 index 00000000000000..36d5765c17f0ad --- /dev/null +++ b/test/fixtures/es-modules/test-wasm-source-phase-identity-parent.mjs @@ -0,0 +1,6 @@ +import * as mod1 from './simple.wasm'; +import * as mod2 from './simple.wasm'; +import source mod3 from './simple.wasm'; +import source mod4 from './simple.wasm'; + +export { mod1, mod2, mod3, mod4 }; diff --git a/test/fixtures/es-modules/test-wasm-source-phase-identity.mjs b/test/fixtures/es-modules/test-wasm-source-phase-identity.mjs new file mode 100644 index 00000000000000..84cf6261139038 --- /dev/null +++ b/test/fixtures/es-modules/test-wasm-source-phase-identity.mjs @@ -0,0 +1,14 @@ +import { strictEqual } from 'node:assert'; + +// Pre-load simple.wasm at kSourcePhase to prime the loadCache. +const preloaded = await import.source('./simple.wasm'); +strictEqual(preloaded instanceof WebAssembly.Module, true); + +// Import a parent that has both eval-phase and source-phase imports of the +// same wasm file, which triggers ensurePhase(kEvaluationPhase) on the cached +// job and exposes the loadCache eviction bug. +const { mod1, mod2, mod3, mod4 } = + await import('./test-wasm-source-phase-identity-parent.mjs'); + +strictEqual(mod1, mod2, 'two eval-phase imports of the same wasm must be identical'); +strictEqual(mod3, mod4, 'two source-phase imports of the same wasm must be identical'); From 54ccef046840d6b2df5164d3dc4896acf1512a96 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 10 Mar 2026 22:12:44 -0400 Subject: [PATCH 2/2] wasm: support js string constant esm import Extends the Wasm ESM Integration for importing WebAssembly modules in either the source phase or instance phase to support importing static JS string constants from the special import name `wasm:js/string-constants`. PR-URL: https://github.com/nodejs/node/pull/62198 Reviewed-By: Yagiz Nizipli Reviewed-By: Colin Ihrig --- doc/api/esm.md | 9 +++++++++ lib/internal/modules/esm/translators.js | 1 + .../fixtures/es-modules/js-string-builtins.wasm | Bin 325 -> 401 bytes test/fixtures/es-modules/js-string-builtins.wat | 8 ++++++++ .../es-modules/test-wasm-js-string-builtins.mjs | 1 + 5 files changed, 19 insertions(+) diff --git a/doc/api/esm.md b/doc/api/esm.md index 838e0810012ad5..17164959ca6205 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -800,6 +800,15 @@ imports and they cannot be inspected via `WebAssembly.Module.imports(mod)` or virtualized unless recompiling the module using the direct `WebAssembly.compile` API with string builtins disabled. +String constants may also be imported from the `wasm:js/string-constants` builtin +import URL, allowing static JS string globals to be defined: + +```text +(module + (import "wasm:js/string-constants" "hello" (global $hello externref)) +) +``` + Importing a module in the source phase before it has been instantiated will also use the compile-time builtins automatically: diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index d6c96996a900da..4643300041a638 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -523,6 +523,7 @@ translators.set('wasm', function(url, translateContext) { try { compiled = new WebAssembly.Module(source, { builtins: ['js-string'], + importedStringConstants: 'wasm:js/string-constants', }); } catch (err) { err.message = errPath(url) + ': ' + err.message; diff --git a/test/fixtures/es-modules/js-string-builtins.wasm b/test/fixtures/es-modules/js-string-builtins.wasm index b4c08587dd08e715fa2a79fead8fc46143d949d2..fe520bab1fbbf5af043b905c4729d2e2ce2746fa 100644 GIT binary patch delta 157 zcmX@gG?7__A+b1@k%57MQJf`#F`uzMfhj+qF(n^N)h948<}+opOjPjDkthc$w8|>h zFD@y{%uClz&d)0@Nz5xLX3a>=$;oHVXJBS!VPIrpX18RSSg6a#k)B%O0g_>0XWM*MzU}R#~W1cue tSDj0WnT