Skip to content

Commit c6ce9fd

Browse files
committed
esm: experimental addon modules
1 parent be5a500 commit c6ce9fd

File tree

15 files changed

+252
-18
lines changed

15 files changed

+252
-18
lines changed

doc/api/cli.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ Otherwise, the file is loaded using the CommonJS module loader. See
4848
When loading, the [ES module loader][Modules loaders] loads the program
4949
entry point, the `node` command will accept as input only files with `.js`,
5050
`.mjs`, or `.cjs` extensions; with `.wasm` extensions when
51-
[`--experimental-wasm-modules`][] is enabled; and with no extension when
51+
[`--experimental-wasm-modules`][] is enabled; with `.node` extensions when
52+
[`--experimental-addon-modules`][] is enabled; and with no extension when
5253
[`--experimental-default-type=module`][] is passed.
5354

5455
## Options
@@ -903,6 +904,14 @@ and `"` are usable.
903904
It is possible to run code containing inline types by passing
904905
[`--experimental-strip-types`][].
905906

907+
### `--experimental-addon-modules`
908+
909+
<!-- YAML
910+
added: REPLACEME
911+
-->
912+
913+
Enable experimental addon modules with extension `.node` support.
914+
906915
### `--experimental-default-type=type`
907916

908917
<!-- YAML
@@ -3031,6 +3040,7 @@ one is included in the list below.
30313040
* `--enable-source-maps`
30323041
* `--entry-url`
30333042
* `--experimental-abortcontroller`
3043+
* `--experimental-addon-modules`
30343044
* `--experimental-default-type`
30353045
* `--experimental-detect-module`
30363046
* `--experimental-eventsource`
@@ -3589,6 +3599,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
35893599
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
35903600
[`--env-file-if-exists`]: #--env-file-if-existsconfig
35913601
[`--env-file`]: #--env-fileconfig
3602+
[`--experimental-addon-modules`]: #--experimental-addon-modules
35923603
[`--experimental-default-type=module`]: #--experimental-default-typetype
35933604
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
35943605
[`--experimental-strip-types`]: #--experimental-strip-types

doc/api/esm.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,18 +1027,21 @@ _isImports_, _conditions_)
10271027
> 5. If `--experimental-wasm-modules` is enabled and _url_ ends in
10281028
> _".wasm"_, then
10291029
> 1. Return _"wasm"_.
1030-
> 6. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
1031-
> 7. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1032-
> 8. Let _packageType_ be **null**.
1033-
> 9. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
1034-
> 1. Set _packageType_ to _pjson.type_.
1035-
> 10. If _url_ ends in _".js"_, then
1030+
> 6. If `--experimental-addon-modules` is enabled and _url_ ends in
1031+
> _".node"_, then
1032+
> 1. Return _"addon"_.
1033+
> 7. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
1034+
> 8. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1035+
> 9. Let _packageType_ be **null**.
1036+
> 10. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
1037+
> 1. Set _packageType_ to _pjson.type_.
1038+
> 11. If _url_ ends in _".js"_, then
10361039
> 1. If _packageType_ is not **null**, then
10371040
> 1. Return _packageType_.
10381041
> 2. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
10391042
> 1. Return _"module"_.
10401043
> 3. Return _"commonjs"_.
1041-
> 11. If _url_ does not have any extension, then
1044+
> 12. If _url_ does not have any extension, then
10421045
> 1. If _packageType_ is _"module"_ and `--experimental-wasm-modules` is
10431046
> enabled and the file at _url_ contains the header for a WebAssembly
10441047
> module, then
@@ -1048,7 +1051,7 @@ _isImports_, _conditions_)
10481051
> 3. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
10491052
> 1. Return _"module"_.
10501053
> 4. Return _"commonjs"_.
1051-
> 12. Return **undefined** (will throw during load phase).
1054+
> 13. Return **undefined** (will throw during load phase).
10521055
10531056
**LOOKUP\_PACKAGE\_SCOPE**(_url_)
10541057

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const fsBindings = internalBinding('fs');
1010
const { fs: fsConstants } = internalBinding('constants');
1111

1212
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
13+
const experimentalAddonModules = getOptionValue('--experimental-addon-modules');
1314

1415
const extensionFormatMap = {
1516
'__proto__': null,
@@ -23,6 +24,10 @@ if (experimentalWasmModules) {
2324
extensionFormatMap['.wasm'] = 'wasm';
2425
}
2526

27+
if (experimentalAddonModules) {
28+
extensionFormatMap['.node'] = 'addon';
29+
}
30+
2631
if (getOptionValue('--experimental-strip-types')) {
2732
extensionFormatMap['.ts'] = 'module-typescript';
2833
extensionFormatMap['.mts'] = 'module-typescript';

lib/internal/modules/esm/load.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ async function defaultLoad(url, context = kEmptyObject) {
109109
if (urlInstance.protocol === 'node:') {
110110
source = null;
111111
format ??= 'builtin';
112+
} else if (format === 'addon') {
113+
// Skip loading addon file content. It must be loaded with dlopen.
114+
source = null;
112115
} else if (format !== 'commonjs' || defaultType === 'module') {
113116
if (source == null) {
114117
({ responseURL, source } = await getSource(urlInstance, context));

lib/internal/modules/esm/translators.js

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const {
5252
ERR_UNKNOWN_BUILTIN_MODULE,
5353
} = require('internal/errors').codes;
5454
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
55+
const { validateNull } = require('internal/validators');
5556
const moduleWrap = internalBinding('module_wrap');
5657
const { ModuleWrap } = moduleWrap;
5758

@@ -225,6 +226,48 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
225226
}, module);
226227
}
227228

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

@@ -276,12 +319,7 @@ translators.set('commonjs', function commonjsStrategy(url, source, isMain) {
276319
return createCJSModuleWrap(url, source, isMain, cjsLoader);
277320
});
278321

279-
/**
280-
* Pre-parses a CommonJS module's exports and re-exports.
281-
* @param {string} filename - The filename of the module.
282-
* @param {string} [source] - The source code of the module.
283-
*/
284-
function cjsPreparseModuleExports(filename, source) {
322+
function cjsEmplaceModuleCacheEntry(filename) {
285323
// TODO: Do we want to keep hitting the user mutable CJS loader here?
286324
let module = CJSModule._cache[filename];
287325
if (module && module[kModuleExportNames] !== undefined) {
@@ -293,10 +331,23 @@ function cjsPreparseModuleExports(filename, source) {
293331
module.filename = filename;
294332
module.paths = CJSModule._nodeModulePaths(module.path);
295333
module[kIsCachedByESMLoader] = true;
296-
module[kModuleSource] = source;
297334
CJSModule._cache[filename] = module;
298335
}
299336

337+
return { module };
338+
}
339+
340+
/**
341+
* Pre-parses a CommonJS module's exports and re-exports.
342+
* @param {string} filename - The filename of the module.
343+
* @param {string} [source] - The source code of the module.
344+
*/
345+
function cjsPreparseModuleExports(filename, source) {
346+
const { module, exportNames: cachedExportNames } = cjsEmplaceModuleCacheEntry(filename);
347+
if (cachedExportNames !== undefined) {
348+
return { module, exportNames: cachedExportNames };
349+
}
350+
300351
let exports, reexports;
301352
try {
302353
({ exports, reexports } = cjsParse(source || ''));
@@ -308,11 +359,10 @@ function cjsPreparseModuleExports(filename, source) {
308359
const exportNames = new SafeSet(new SafeArrayIterator(exports));
309360

310361
// Set first for cycles.
362+
module[kModuleSource] = source;
311363
module[kModuleExportNames] = exportNames;
312364

313365
if (reexports.length) {
314-
module.filename = filename;
315-
module.paths = CJSModule._nodeModulePaths(module.path);
316366
for (let i = 0; i < reexports.length; i++) {
317367
const reexport = reexports[i];
318368
let resolved;
@@ -459,6 +509,19 @@ translators.set('wasm', async function(url, source) {
459509
}).module;
460510
});
461511

512+
// Strategy for loading a addon
513+
translators.set('addon', async function(url, source, isMain) {
514+
emitExperimentalWarning('Importing addons');
515+
516+
// The addon must be loaded from file system with dlopen. Assert
517+
// the source is null.
518+
validateNull(source, 'source');
519+
520+
debug(`Translating addon ${url}`);
521+
522+
return createCJSNoSourceModuleWrap(url, isMain);
523+
});
524+
462525
// Strategy for loading a commonjs TypeScript module
463526
translators.set('commonjs-typescript', function(url, source) {
464527
emitExperimentalWarning('Type Stripping');

lib/internal/validators.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,19 @@ const validateUndefined = hideStackFrames((value, name) => {
487487
throw new ERR_INVALID_ARG_TYPE(name, 'undefined', value);
488488
});
489489

490+
/**
491+
* @callback validateNull
492+
* @param {*} value
493+
* @param {string} name
494+
* @returns {asserts value is null}
495+
*/
496+
497+
/** @type {validateNull} */
498+
const validateNull = hideStackFrames((value, name) => {
499+
if (value !== null)
500+
throw new ERR_INVALID_ARG_TYPE(name, 'null', value);
501+
});
502+
490503
/**
491504
* @template T
492505
* @param {T} value
@@ -623,6 +636,7 @@ module.exports = {
623636
validateFunction,
624637
validateInt32,
625638
validateInteger,
639+
validateNull,
626640
validateNumber,
627641
validateObject,
628642
kValidateObjectNone,

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
411411
"Treat the entrypoint as a URL",
412412
&EnvironmentOptions::entry_is_url,
413413
kAllowedInEnvvar);
414+
AddOption("--experimental-addon-modules",
415+
"experimental ES Module support for addons",
416+
&EnvironmentOptions::experimental_addon_modules,
417+
kAllowedInEnvvar);
414418
AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar);
415419
AddOption("--experimental-eventsource",
416420
"experimental EventSource API",

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class EnvironmentOptions : public Options {
120120
bool require_module = true;
121121
std::string dns_result_order;
122122
bool enable_source_maps = false;
123+
bool experimental_addon_modules = false;
123124
bool experimental_eventsource = false;
124125
bool experimental_fetch = true;
125126
bool experimental_websocket = true;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#include <node.h>
2+
#include <uv.h>
3+
#include <v8.h>
4+
5+
static void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
6+
v8::Isolate* isolate = args.GetIsolate();
7+
args.GetReturnValue().Set(
8+
v8::String::NewFromUtf8(isolate, "hello world").ToLocalChecked());
9+
}
10+
11+
static void InitModule(v8::Local<v8::Object> exports,
12+
v8::Local<v8::Value> module,
13+
v8::Local<v8::Context> context) {
14+
NODE_SET_METHOD(exports, "default", Method);
15+
}
16+
17+
NODE_MODULE_CONTEXT_AWARE(Binding, InitModule)

0 commit comments

Comments
 (0)