Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d302cb3

Browse filesBrowse files
legendecasrichardlau
authored andcommitted
esm: add experimental support for addon modules
PR-URL: #55844 Backport-PR-URL: #59961 Fixes: #40541 Fixes: #55821 Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 937e9bb commit d302cb3
Copy full SHA for d302cb3
Expand file treeCollapse file tree

21 files changed

+330
-25
lines changed
Open diff view settings
Collapse file

‎doc/api/cli.md‎

Copy file name to clipboardExpand all lines: doc/api/cli.md
+16-1Lines changed: 16 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ Otherwise, the file is loaded using the CommonJS module loader. See
4949
When loading, the [ES module loader][Modules loaders] loads the program
5050
entry point, the `node` command will accept as input only files with `.js`,
5151
`.mjs`, `.cjs` or `.wasm` extensions; and with no extension when
52-
[`--experimental-default-type=module`][] is passed.
52+
[`--experimental-default-type=module`][] is passed. With the following flags,
53+
additional file extensions are enabled:
54+
55+
* [`--experimental-addon-modules`][] for files with `.node` extension.
5356

5457
## Options
5558

@@ -895,6 +898,16 @@ and `"` are usable.
895898
It is possible to run code containing inline types unless the
896899
[`--no-experimental-strip-types`][] flag is provided.
897900

901+
### `--experimental-addon-modules`
902+
903+
<!-- YAML
904+
added: REPLACEME
905+
-->
906+
907+
> Stability: 1.0 - Early development
908+
909+
Enable experimental import support for `.node` addons.
910+
898911
### `--experimental-async-context-frame`
899912

900913
<!-- YAML
@@ -3330,6 +3343,7 @@ one is included in the list below.
33303343
* `--enable-source-maps`
33313344
* `--entry-url`
33323345
* `--experimental-abortcontroller`
3346+
* `--experimental-addon-modules`
33333347
* `--experimental-async-context-frame`
33343348
* `--experimental-default-type`
33353349
* `--experimental-detect-module`
@@ -3926,6 +3940,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
39263940
[`--disable-sigusr1`]: #--disable-sigusr1
39273941
[`--env-file-if-exists`]: #--env-file-if-existsfile
39283942
[`--env-file`]: #--env-filefile
3943+
[`--experimental-addon-modules`]: #--experimental-addon-modules
39293944
[`--experimental-default-type=module`]: #--experimental-default-typetype
39303945
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
39313946
[`--heap-prof-dir`]: #--heap-prof-dir
Collapse file

‎doc/api/esm.md‎

Copy file name to clipboardExpand all lines: doc/api/esm.md
+11-8Lines changed: 11 additions & 8 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1129,18 +1129,21 @@ _isImports_, _conditions_)
11291129
> 5. If _url_ ends in
11301130
> _".wasm"_, then
11311131
> 1. Return _"wasm"_.
1132-
> 6. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
1133-
> 7. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1134-
> 8. Let _packageType_ be **null**.
1135-
> 9. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
1136-
> 1. Set _packageType_ to _pjson.type_.
1137-
> 10. If _url_ ends in _".js"_, then
1132+
> 6. If `--experimental-addon-modules` is enabled and _url_ ends in
1133+
> _".node"_, then
1134+
> 1. Return _"addon"_.
1135+
> 7. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
1136+
> 8. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1137+
> 9. Let _packageType_ be **null**.
1138+
> 10. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
1139+
> 1. Set _packageType_ to _pjson.type_.
1140+
> 11. If _url_ ends in _".js"_, then
11381141
> 1. If _packageType_ is not **null**, then
11391142
> 1. Return _packageType_.
11401143
> 2. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
11411144
> 1. Return _"module"_.
11421145
> 3. Return _"commonjs"_.
1143-
> 11. If _url_ does not have any extension, then
1146+
> 12. If _url_ does not have any extension, then
11441147
> 1. If _packageType_ is _"module"_ and the file at _url_ contains the
11451148
> header for a WebAssembly module, then
11461149
> 1. Return _"wasm"_.
@@ -1149,7 +1152,7 @@ _isImports_, _conditions_)
11491152
> 3. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
11501153
> 1. Return _"module"_.
11511154
> 4. Return _"commonjs"_.
1152-
> 12. Return **undefined** (will throw during load phase).
1155+
> 13. Return **undefined** (will throw during load phase).
11531156
11541157
**LOOKUP\_PACKAGE\_SCOPE**(_url_)
11551158
Collapse file

‎doc/api/module.md‎

Copy file name to clipboardExpand all lines: doc/api/module.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1326,6 +1326,7 @@ The final value of `format` must be one of the following:
13261326
13271327
| `format` | Description | Acceptable types for `source` returned by `load` |
13281328
| ----------------------- | ----------------------------------------------------- | -------------------------------------------------- |
1329+
| `'addon'` | Load a Node.js addon | {null} |
13291330
| `'builtin'` | Load a Node.js builtin module | {null} |
13301331
| `'commonjs-typescript'` | Load a Node.js CommonJS module with TypeScript syntax | {string\|ArrayBuffer\|TypedArray\|null\|undefined} |
13311332
| `'commonjs'` | Load a Node.js CommonJS module | {string\|ArrayBuffer\|TypedArray\|null\|undefined} |
Collapse file

‎doc/node-config-schema.json‎

Copy file name to clipboardExpand all lines: doc/node-config-schema.json
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@
121121
"entry-url": {
122122
"type": "boolean"
123123
},
124+
"experimental-addon-modules": {
125+
"type": "boolean"
126+
},
124127
"experimental-async-context-frame": {
125128
"type": "boolean"
126129
},
Collapse file

‎doc/node.1‎

Copy file name to clipboardExpand all lines: doc/node.1
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ Enable Source Map V3 support for stack traces.
163163
.It Fl -entry-url
164164
Interpret the entry point as a URL.
165165
.
166+
.It Fl -experimental-addon-modules
167+
Enable experimental addon module support.
168+
.
166169
.It Fl -experimental-default-type Ns = Ns Ar type
167170
Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
168171
.js or extensionless files with no sibling or parent package.json;
Collapse file

‎lib/internal/modules/esm/formats.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/formats.js
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const extensionFormatMap = {
1818
'.wasm': 'wasm',
1919
};
2020

21+
if (getOptionValue('--experimental-addon-modules')) {
22+
extensionFormatMap['.node'] = 'addon';
23+
}
2124
if (getOptionValue('--experimental-strip-types')) {
2225
extensionFormatMap['.ts'] = 'module-typescript';
2326
extensionFormatMap['.mts'] = 'module-typescript';
Collapse file

‎lib/internal/modules/esm/load.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/load.js
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ function defaultLoad(url, context = kEmptyObject) {
8484
if (urlInstance.protocol === 'node:') {
8585
source = null;
8686
format ??= 'builtin';
87+
} else if (format === 'addon') {
88+
// Skip loading addon file content. It must be loaded with dlopen from file system.
89+
source = null;
8790
} else if (format !== 'commonjs' || defaultType === 'module') {
8891
if (source == null) {
8992
({ responseURL, source } = getSourceSync(urlInstance, context));
Collapse file

‎lib/internal/modules/esm/translators.js‎

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js
+88-16Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
const {
44
ArrayPrototypePush,
5-
Boolean,
65
FunctionPrototypeCall,
76
JSONParse,
87
ObjectAssign,
@@ -52,6 +51,7 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
5251
});
5352
const { emitExperimentalWarning, kEmptyObject, setOwnProperty, isWindows } = require('internal/util');
5453
const {
54+
ERR_INVALID_RETURN_PROPERTY_VALUE,
5555
ERR_UNKNOWN_BUILTIN_MODULE,
5656
} = require('internal/errors').codes;
5757
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
@@ -186,7 +186,7 @@ function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModul
186186
// In case the source was not provided by the `load` step, we need fetch it now.
187187
source = stringify(source ?? getSource(new URL(url)).source);
188188

189-
const { exportNames, module } = cjsPreparseModuleExports(filename, source, isMain, format);
189+
const { exportNames, module } = cjsPreparseModuleExports(filename, source, format);
190190
cjsCache.set(url, module);
191191
const namesWithDefault = exportNames.has('default') ?
192192
[...exportNames] : ['default', ...exportNames];
@@ -227,6 +227,47 @@ function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModul
227227
}, module);
228228
}
229229

230+
/**
231+
* Creates a ModuleWrap object for a CommonJS module without source texts.
232+
* @param {string} url - The URL of the module.
233+
* @param {boolean} isMain - Whether the module is the main module.
234+
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
235+
*/
236+
function createCJSNoSourceModuleWrap(url, isMain) {
237+
debug(`Translating CJSModule without source ${url}`);
238+
239+
const filename = urlToFilename(url);
240+
241+
const module = cjsEmplaceModuleCacheEntry(filename);
242+
cjsCache.set(url, module);
243+
244+
if (isMain) {
245+
setOwnProperty(process, 'mainModule', module);
246+
}
247+
248+
// Addon export names are not known until the addon is loaded.
249+
const exportNames = ['default', 'module.exports'];
250+
return new ModuleWrap(url, undefined, exportNames, function evaluationCallback() {
251+
debug(`Loading CJSModule ${url}`);
252+
253+
if (!module.loaded) {
254+
wrapModuleLoad(filename, null, isMain);
255+
}
256+
257+
/** @type {import('./loader').ModuleExports} */
258+
let exports;
259+
if (module[kModuleExport] !== undefined) {
260+
exports = module[kModuleExport];
261+
module[kModuleExport] = undefined;
262+
} else {
263+
({ exports } = module);
264+
}
265+
266+
this.setExport('default', exports);
267+
this.setExport('module.exports', exports);
268+
}, module);
269+
}
270+
230271
translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) {
231272
initCJSParseSync();
232273

@@ -277,26 +318,38 @@ translators.set('commonjs', function commonjsStrategy(url, source, isMain) {
277318
return createCJSModuleWrap(url, source, isMain, 'commonjs', cjsLoader);
278319
});
279320

321+
/**
322+
* Get or create an entry in the CJS module cache for the given filename.
323+
* @param {string} filename CJS module filename
324+
* @returns {CJSModule} the cached CJS module entry
325+
*/
326+
function cjsEmplaceModuleCacheEntry(filename, exportNames) {
327+
// TODO: Do we want to keep hitting the user mutable CJS loader here?
328+
let cjsMod = CJSModule._cache[filename];
329+
if (cjsMod) {
330+
return cjsMod;
331+
}
332+
333+
cjsMod = new CJSModule(filename);
334+
cjsMod.filename = filename;
335+
cjsMod.paths = CJSModule._nodeModulePaths(cjsMod.path);
336+
cjsMod[kIsCachedByESMLoader] = true;
337+
CJSModule._cache[filename] = cjsMod;
338+
339+
return cjsMod;
340+
}
341+
280342
/**
281343
* Pre-parses a CommonJS module's exports and re-exports.
282344
* @param {string} filename - The filename of the module.
283345
* @param {string} [source] - The source code of the module.
284-
* @param {boolean} isMain - Whether it is pre-parsing for the entry point.
285-
* @param {string} format
346+
* @param {string} [format]
286347
*/
287-
function cjsPreparseModuleExports(filename, source, isMain, format) {
288-
let module = CJSModule._cache[filename];
289-
if (module && module[kModuleExportNames] !== undefined) {
348+
function cjsPreparseModuleExports(filename, source, format) {
349+
const module = cjsEmplaceModuleCacheEntry(filename);
350+
if (module[kModuleExportNames] !== undefined) {
290351
return { module, exportNames: module[kModuleExportNames] };
291352
}
292-
const loaded = Boolean(module);
293-
if (!loaded) {
294-
module = new CJSModule(filename);
295-
module.filename = filename;
296-
module.paths = CJSModule._nodeModulePaths(module.path);
297-
module[kIsCachedByESMLoader] = true;
298-
CJSModule._cache[filename] = module;
299-
}
300353

301354
if (source === undefined) {
302355
({ source } = loadSourceForCJSWithHooks(module, filename, format));
@@ -337,7 +390,7 @@ function cjsPreparseModuleExports(filename, source, isMain, format) {
337390

338391
if (format === 'commonjs' ||
339392
(!BuiltinModule.normalizeRequirableId(resolved) && findLongestRegisteredExtension(resolved) === '.js')) {
340-
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, false, format);
393+
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, format);
341394
for (const name of reexportNames) {
342395
exportNames.add(name);
343396
}
@@ -519,6 +572,25 @@ translators.set('wasm', function(url, source) {
519572
return module;
520573
});
521574

575+
// Strategy for loading a addon
576+
translators.set('addon', function translateAddon(url, source, isMain) {
577+
emitExperimentalWarning('Importing addons');
578+
579+
// The addon must be loaded from file system with dlopen. Assert
580+
// the source is null.
581+
if (source !== null) {
582+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
583+
'null',
584+
'load',
585+
'source',
586+
source);
587+
}
588+
589+
debug(`Translating addon ${url}`);
590+
591+
return createCJSNoSourceModuleWrap(url, isMain);
592+
});
593+
522594
// Strategy for loading a commonjs TypeScript module
523595
translators.set('commonjs-typescript', function(url, source, isMain) {
524596
assertBufferSource(source, true, 'load');
Collapse file

‎src/node_options.cc‎

Copy file name to clipboardExpand all lines: src/node_options.cc
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
424424
"Treat the entrypoint as a URL",
425425
&EnvironmentOptions::entry_is_url,
426426
kAllowedInEnvvar);
427+
AddOption("--experimental-addon-modules",
428+
"experimental import support for addons",
429+
&EnvironmentOptions::experimental_addon_modules,
430+
kAllowedInEnvvar);
427431
AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar);
428432
AddOption("--experimental-eventsource",
429433
"experimental EventSource API",
Collapse file

‎src/node_options.h‎

Copy file name to clipboardExpand all lines: src/node_options.h
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class EnvironmentOptions : public Options {
121121
bool require_module = true;
122122
std::string dns_result_order;
123123
bool enable_source_maps = false;
124+
bool experimental_addon_modules = false;
124125
bool experimental_eventsource = false;
125126
bool experimental_fetch = true;
126127
bool experimental_websocket = true;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.