Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: Source Phase Imports for WebAssembly #56919

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

guybedford
Copy link
Contributor

@guybedford guybedford commented Feb 5, 2025

This implements the Stage 3 Source Phase Imports proposal for Node.js (with latest specification at tc39/ecma262#3492), based on the new V8 parser and module loading APIs for this feature.

Phases in the module pipeline represent the ability to load uninstantiated modules (and in future with import defer, unexecuted modules as well).

While we are still waiting for progression on https://github.com/tc39/proposal-esm-phase-imports for JS Source Phase objects, this implements only the WebAssembly source phase per the Wasm ESM Integration Phase 3 proposal (https://github.com/webassembly/esm-integration).

This PR adds support both for dynamic and static source phase syntax:

import source mod1 from './mod.wasm';
const mod2 = await import.source('./mod.wasm');

This allows obtaining a Wasm module through the ESM integration, without it being instantiated through the Node.js resolver, to permit custom instantiations:

import source mod from './mod.wasm';

const instance = await WebAssembly.instantiate(mod, imports);

The dynamic source phase support is a one-line TODO, which is pending #56842. When this lands the skipped tests have been tested against that branch to fully pass.

When a source phase representation for a module is not available, a new ERR_SOURCE_PHASE_NOT_DEFINED SyntaxError is thrown as per the standard.

The VM API is updated to take a string phase argument as its last argument, although the ability to define source phase representations for virtual modules is not currently supported, and left as a future integration point. Perhaps this will simplify with ESM Phase Imports in future as well allowing handles to modules outside of the VM graph.

Resolves #53178.

@nodejs/loaders

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/startup
  • @nodejs/vm

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Feb 5, 2025
@guybedford guybedford force-pushed the source-phase-imports branch from aa33f53 to c485531 Compare February 5, 2025 02:27
@guybedford guybedford force-pushed the source-phase-imports branch from c485531 to 8b3ce05 Compare February 5, 2025 02:31
@guybedford guybedford changed the title esm: Support Source Phase Imports for WebAssembly esm: Source Phase Imports for WebAssembly Feb 5, 2025
Copy link

codecov bot commented Feb 5, 2025

Codecov Report

Attention: Patch coverage is 82.15488% with 53 lines in your changes missing coverage. Please review.

Project coverage is 89.15%. Comparing base (316ac8c) to head (301b067).
Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/module_wrap.cc 62.50% 23 Missing and 13 partials ⚠️
lib/internal/modules/esm/module_job.js 89.77% 9 Missing ⚠️
lib/internal/modules/esm/loader.js 87.50% 3 Missing and 1 partial ⚠️
lib/internal/vm/module.js 85.71% 3 Missing ⚠️
lib/repl.js 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #56919      +/-   ##
==========================================
- Coverage   89.18%   89.15%   -0.04%     
==========================================
  Files         665      665              
  Lines      192543   192771     +228     
  Branches    37045    37113      +68     
==========================================
+ Hits       171726   171865     +139     
- Misses      13636    13687      +51     
- Partials     7181     7219      +38     
Files with missing lines Coverage Δ
lib/internal/modules/cjs/loader.js 98.26% <100.00%> (ø)
lib/internal/modules/esm/translators.js 91.51% <100.00%> (+0.06%) ⬆️
lib/internal/modules/esm/utils.js 98.90% <100.00%> (+0.01%) ⬆️
lib/internal/modules/run_main.js 97.60% <100.00%> (ø)
lib/internal/process/execution.js 94.79% <100.00%> (-0.16%) ⬇️
lib/internal/vm.js 100.00% <100.00%> (ø)
src/module_wrap.h 0.00% <ø> (ø)
src/node.cc 73.61% <100.00%> (+0.11%) ⬆️
src/node_errors.h 88.46% <100.00%> (+3.46%) ⬆️
lib/repl.js 94.90% <75.00%> (-0.05%) ⬇️
... and 4 more

... and 36 files with indirect coverage changes

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Feb 5, 2025
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Feb 5, 2025
@nodejs-github-bot
Copy link
Collaborator

@marco-ippolito marco-ippolito added esm Issues and PRs related to the ECMAScript Modules implementation. wasm Issues and PRs related to WebAssembly. labels Feb 5, 2025
@targos targos added dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. dont-land-on-v20.x PRs that should not land on the v20.x-staging branch and should not be released in v20.x. dont-land-on-v22.x PRs that should not land on the v22.x-staging branch and should not be released in v22.x. dont-land-on-v23.x PRs that should not land on the v23.x-staging branch and should not be released in v23.x. semver-minor PRs that contain new features and should be released in the next minor version. labels Feb 5, 2025
src/module_wrap.cc Outdated Show resolved Hide resolved
@legendecas legendecas linked an issue Feb 5, 2025 that may be closed by this pull request
src/module_wrap.cc Outdated Show resolved Hide resolved
src/module_wrap.cc Outdated Show resolved Hide resolved
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
* @param {Record<string, string>} attributes
* @param {string} phase
* @returns { Promise<void> }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this return value type correct? I think it should be Promise<ModuleNamespace|vm.Module>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not - but this is a move of an existing definition, so this is as it's currently written on main.

For example, to create multiple instances of a module, or to pass custom imports
into a new instance of `library.wasm`:
<!-- eslint-skip -->
Copy link
Contributor

@aduh95 aduh95 Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we instead add the required plugin? Can happen in a follow-up PR

diff --git a/eslint.config.mjs b/eslint.config.mjs
index ef195d0c60..912d326557 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -17,6 +17,7 @@ import nodeCore from './tools/eslint/eslint-plugin-node-core.js';
 const js = requireEslintTool('@eslint/js');
 const babelEslintParser = requireEslintTool('@babel/eslint-parser');
 const babelPluginSyntaxImportAttributes = resolveEslintTool('@babel/plugin-syntax-import-attributes');
+const babelPluginSyntaxImportSource = resolveEslintTool('@babel/plugin-syntax-import-source');
 const jsdoc = requireEslintTool('eslint-plugin-jsdoc');
 const markdown = requireEslintTool('eslint-plugin-markdown');
 const stylisticJs = requireEslintTool('@stylistic/eslint-plugin-js');
@@ -74,6 +75,7 @@ export default [
         babelOptions: {
           plugins: [
             babelPluginSyntaxImportAttributes,
+            babelPluginSyntaxImportSource,
           ],
         },
         requireConfigFile: false,
diff --git a/tools/eslint/package.json b/tools/eslint/package.json
index 68bedee0cb..0b6244654e 100644
--- a/tools/eslint/package.json
+++ b/tools/eslint/package.json
@@ -6,6 +6,7 @@
     "@babel/core": "^7.26.0",
     "@babel/eslint-parser": "^7.25.9",
     "@babel/plugin-syntax-import-attributes": "^7.26.0",
+    "@babel/plugin-syntax-import-source": "^7.25.9",
     "@stylistic/eslint-plugin-js": "^2.12.1",
     "eslint": "^9.17.0",
     "eslint-formatter-tap": "^8.40.0",

doc/api/esm.md Outdated Show resolved Hide resolved
Co-authored-by: Antoine du Hamel <[email protected]>
Co-authored-by: Michaël Zasso <[email protected]>
'await m1.evaluate();',
].join('\n'),
]);
ok(stderr.includes('Source phase import object is not defined for module'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get better error output

Suggested change
ok(stderr.includes('Source phase import object is not defined for module'));
match(stderr, /Source phase import object is not defined for module/);

'--eval',
`import source nosource from ${JSON.stringify(fileUrl)};`,
]);
ok(stderr.includes('Source phase import object is not defined for module'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ok(stderr.includes('Source phase import object is not defined for module'));
match(stderr, /Source phase import object is not defined for module/);

'await m1.evaluate();',
].join('\n'),
]);
ok(stderr.includes('Source phase import object is not defined for module'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ok(stderr.includes('Source phase import object is not defined for module'));
match(stderr, /Source phase import object is not defined for module/);

Comment on lines +179 to +190
[
'import { ok, strictEqual } from "node:assert";',
'try {',
` await import.source(${JSON.stringify(fileUrl)});`,
' ok(false);',
'}',
'catch (e) {',
' strictEqual(e instanceof SyntaxError, true);',
' strictEqual(e.message.includes("Source phase import object is not defined for module"), true);',
` strictEqual(e.message.includes(${JSON.stringify(fileUrl)}), true);`,
'}',
].join('\n'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[
'import { ok, strictEqual } from "node:assert";',
'try {',
` await import.source(${JSON.stringify(fileUrl)});`,
' ok(false);',
'}',
'catch (e) {',
' strictEqual(e instanceof SyntaxError, true);',
' strictEqual(e.message.includes("Source phase import object is not defined for module"), true);',
` strictEqual(e.message.includes(${JSON.stringify(fileUrl)}), true);`,
'}',
].join('\n'),
`await assert.rejects(import.source(${JSON.stringify(fileUrl)}), (e) => {
assert.match(e.message, /Source phase import object is not defined for module/);
assert.ok(e.message.includes(${JSON.stringify(fileUrl)}));
assert.ok(e instanceof SyntaxError);
});`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, I'll aim to incorporate the changes soon. For this rewriting, I was just following the pattern already set in this test - I think if we want to change this we should consider doing it for the whole file, which would be a larger rewriting?

Copy link
Contributor

@aduh95 aduh95 Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which test? I'd be up to do that rewriting, but I can't seem to find where we use that pattern

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dont-land-on-v18.x PRs that should not land on the v18.x-staging branch and should not be released in v18.x. dont-land-on-v20.x PRs that should not land on the v20.x-staging branch and should not be released in v20.x. dont-land-on-v22.x PRs that should not land on the v22.x-staging branch and should not be released in v22.x. dont-land-on-v23.x PRs that should not land on the v23.x-staging branch and should not be released in v23.x. esm Issues and PRs related to the ECMAScript Modules implementation. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version. wasm Issues and PRs related to WebAssembly.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

WebAssembly source phase imports Tracking issue: latest ESM Integration support
8 participants