Skip to content

Commit d302cb3

Browse files
legendecasrichardlau
authored andcommitted
esm: add experimental support for addon modules
PR-URL: #55844 Backport-PR-URL: #59961Fixes: #40541Fixes: #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

21 files changed

Lines changed: 330 additions & 25 deletions

‎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
.ItFl-entry-url
164164
Interpret the entry point as a URL.
165165
.
166+
.ItFl-experimental-addon-modules
167+
Enable experimental addon module support.
168+
.
166169
.ItFl-experimental-default-typeNs=NsArtype
167170
Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
168171
.jsorextensionlessfileswithnosiblingorparentpackage.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+
}elseif(format==='addon'){
88+
// Skip loading addon file content. It must be loaded with dlopen from file system.
89+
source=null;
8790
}elseif(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(newURL(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
constnamesWithDefault=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+
functioncreateCJSNoSourceModuleWrap(url,isMain){
237+
debug(`Translating CJSModule without source ${url}`);
238+
239+
constfilename=urlToFilename(url);
240+
241+
constmodule=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+
constexportNames=['default','module.exports'];
250+
returnnewModuleWrap(url,undefined,exportNames,functionevaluationCallback(){
251+
debug(`Loading CJSModule ${url}`);
252+
253+
if(!module.loaded){
254+
wrapModuleLoad(filename,null,isMain);
255+
}
256+
257+
/** @type {import('./loader').ModuleExports} */
258+
letexports;
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',functionrequireCommonJS(url,source,isMain){
231272
initCJSParseSync();
232273

@@ -277,26 +318,38 @@ translators.set('commonjs', function commonjsStrategy(url, source, isMain) {
277318
returncreateCJSModuleWrap(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+
functioncjsEmplaceModuleCacheEntry(filename,exportNames){
327+
// TODO: Do we want to keep hitting the user mutable CJS loader here?
328+
letcjsMod=CJSModule._cache[filename];
329+
if(cjsMod){
330+
returncjsMod;
331+
}
332+
333+
cjsMod=newCJSModule(filename);
334+
cjsMod.filename=filename;
335+
cjsMod.paths=CJSModule._nodeModulePaths(cjsMod.path);
336+
cjsMod[kIsCachedByESMLoader]=true;
337+
CJSModule._cache[filename]=cjsMod;
338+
339+
returncjsMod;
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-
functioncjsPreparseModuleExports(filename,source,isMain,format){
288-
letmodule=CJSModule._cache[filename];
289-
if(module&&module[kModuleExportNames]!==undefined){
348+
functioncjsPreparseModuleExports(filename,source,format){
349+
constmodule=cjsEmplaceModuleCacheEntry(filename);
350+
if(module[kModuleExportNames]!==undefined){
290351
return{ module,exportNames: module[kModuleExportNames]};
291352
}
292-
constloaded=Boolean(module);
293-
if(!loaded){
294-
module=newCJSModule(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(constnameofreexportNames){
342395
exportNames.add(name);
343396
}
@@ -519,6 +572,25 @@ translators.set('wasm', function(url, source) {
519572
returnmodule;
520573
});
521574

575+
// Strategy for loading a addon
576+
translators.set('addon',functiontranslateAddon(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+
thrownewERR_INVALID_RETURN_PROPERTY_VALUE(
583+
'null',
584+
'load',
585+
'source',
586+
source);
587+
}
588+
589+
debug(`Translating addon ${url}`);
590+
591+
returncreateCJSNoSourceModuleWrap(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)