Skip to content

Commit d302cb3

Browse 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 <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 937e9bb commit d302cb3

21 files changed

+330
-25
lines changed

doc/api/cli.md

Lines changed: 16 additions & 1 deletion
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

doc/api/esm.md

Lines changed: 11 additions & 8 deletions
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

doc/api/module.md

Lines changed: 1 addition & 0 deletions
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} |

doc/node-config-schema.json

Lines 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
},

doc/node.1

Lines 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;

lib/internal/modules/esm/formats.js

Lines 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';

lib/internal/modules/esm/load.js

Lines 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));

lib/internal/modules/esm/translators.js

Lines 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');

src/node_options.cc

Lines 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",

src/node_options.h

Lines 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)