Skip to content

Commit 2ea455d

Browse files
committed
esm: add experimental support for addon modules
1 parent 5bdf1c4 commit 2ea455d

File tree

15 files changed

+260
-24
lines changed

15 files changed

+260
-24
lines changed

doc/api/cli.md

Lines changed: 14 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
@@ -896,6 +897,16 @@ and `"` are usable.
896897
It is possible to run code containing inline types by passing
897898
[`--experimental-strip-types`][].
898899

900+
### `--experimental-addon-modules`
901+
902+
<!-- YAML
903+
added: REPLACEME
904+
-->
905+
906+
> Stability: 1.0 - Early development
907+
908+
Enable experimental import support for `.node` addons.
909+
899910
### `--experimental-default-type=type`
900911

901912
<!-- YAML
@@ -3057,6 +3068,7 @@ one is included in the list below.
30573068
* `--enable-source-maps`
30583069
* `--entry-url`
30593070
* `--experimental-abortcontroller`
3071+
* `--experimental-addon-modules`
30603072
* `--experimental-default-type`
30613073
* `--experimental-detect-module`
30623074
* `--experimental-eventsource`
@@ -3620,6 +3632,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
36203632
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
36213633
[`--env-file-if-exists`]: #--env-file-if-existsconfig
36223634
[`--env-file`]: #--env-fileconfig
3635+
[`--experimental-addon-modules`]: #--experimental-addon-modules
36233636
[`--experimental-default-type=module`]: #--experimental-default-typetype
36243637
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
36253638
[`--experimental-strip-types`]: #--experimental-strip-types

doc/api/esm.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,18 +1044,21 @@ _isImports_, _conditions_)
10441044
> 5. If `--experimental-wasm-modules` is enabled and _url_ ends in
10451045
> _".wasm"_, then
10461046
> 1. Return _"wasm"_.
1047-
> 6. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
1048-
> 7. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1049-
> 8. Let _packageType_ be **null**.
1050-
> 9. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
1051-
> 1. Set _packageType_ to _pjson.type_.
1052-
> 10. If _url_ ends in _".js"_, then
1047+
> 6. If `--experimental-addon-modules` is enabled and _url_ ends in
1048+
> _".node"_, then
1049+
> 1. Return _"addon"_.
1050+
> 7. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
1051+
> 8. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
1052+
> 9. Let _packageType_ be **null**.
1053+
> 10. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
1054+
> 1. Set _packageType_ to _pjson.type_.
1055+
> 11. If _url_ ends in _".js"_, then
10531056
> 1. If _packageType_ is not **null**, then
10541057
> 1. Return _packageType_.
10551058
> 2. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
10561059
> 1. Return _"module"_.
10571060
> 3. Return _"commonjs"_.
1058-
> 11. If _url_ does not have any extension, then
1061+
> 12. If _url_ does not have any extension, then
10591062
> 1. If _packageType_ is _"module"_ and `--experimental-wasm-modules` is
10601063
> enabled and the file at _url_ contains the header for a WebAssembly
10611064
> module, then
@@ -1065,7 +1068,7 @@ _isImports_, _conditions_)
10651068
> 3. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
10661069
> 1. Return _"module"_.
10671070
> 4. Return _"commonjs"_.
1068-
> 12. Return **undefined** (will throw during load phase).
1071+
> 13. Return **undefined** (will throw during load phase).
10691072
10701073
**LOOKUP\_PACKAGE\_SCOPE**(_url_)
10711074

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 from file system.
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: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const {
44
ArrayPrototypeMap,
55
ArrayPrototypePush,
6-
Boolean,
76
FunctionPrototypeCall,
87
JSONParse,
98
ObjectKeys,
@@ -49,6 +48,7 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
4948
});
5049
const { emitExperimentalWarning, kEmptyObject, setOwnProperty, isWindows } = require('internal/util');
5150
const {
51+
ERR_INVALID_RETURN_PROPERTY_VALUE,
5252
ERR_UNKNOWN_BUILTIN_MODULE,
5353
} = require('internal/errors').codes;
5454
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
@@ -225,6 +225,46 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
225225
}, module);
226226
}
227227

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

@@ -276,26 +316,37 @@ translators.set('commonjs', function commonjsStrategy(url, source, isMain) {
276316
return createCJSModuleWrap(url, source, isMain, cjsLoader);
277317
});
278318

319+
/**
320+
* Get or create an entry in the CJS module cache for the given filename.
321+
* @param {string} filename CJS module filename
322+
* @returns {CJSModule} the cached CJS module entry
323+
*/
324+
function cjsEmplaceModuleCacheEntry(filename, exportNames) {
325+
// TODO: Do we want to keep hitting the user mutable CJS loader here?
326+
let module = CJSModule._cache[filename];
327+
if (module) {
328+
return module;
329+
}
330+
331+
module = new CJSModule(filename);
332+
module.filename = filename;
333+
module.paths = CJSModule._nodeModulePaths(module.path);
334+
module[kIsCachedByESMLoader] = true;
335+
CJSModule._cache[filename] = module;
336+
337+
return module;
338+
}
339+
279340
/**
280341
* Pre-parses a CommonJS module's exports and re-exports.
281342
* @param {string} filename - The filename of the module.
282343
* @param {string} [source] - The source code of the module.
283344
*/
284345
function cjsPreparseModuleExports(filename, source) {
285-
// TODO: Do we want to keep hitting the user mutable CJS loader here?
286-
let module = CJSModule._cache[filename];
287-
if (module && module[kModuleExportNames] !== undefined) {
346+
const module = cjsEmplaceModuleCacheEntry(filename);
347+
if (module[kModuleExportNames] !== undefined) {
288348
return { module, exportNames: module[kModuleExportNames] };
289349
}
290-
const loaded = Boolean(module);
291-
if (!loaded) {
292-
module = new CJSModule(filename);
293-
module.filename = filename;
294-
module.paths = CJSModule._nodeModulePaths(module.path);
295-
module[kIsCachedByESMLoader] = true;
296-
module[kModuleSource] = source;
297-
CJSModule._cache[filename] = module;
298-
}
299350

300351
let exports, reexports;
301352
try {
@@ -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,25 @@ translators.set('wasm', async function(url, source) {
459509
}).module;
460510
});
461511

512+
// Strategy for loading a addon
513+
translators.set('addon', 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+
if (source !== null) {
519+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
520+
'null',
521+
'load',
522+
'source',
523+
source);
524+
}
525+
526+
debug(`Translating addon ${url}`);
527+
528+
return createCJSNoSourceModuleWrap(url, isMain);
529+
});
530+
462531
// Strategy for loading a commonjs TypeScript module
463532
translators.set('commonjs-typescript', function(url, source) {
464533
emitExperimentalWarning('Type Stripping');

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 import 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)
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 InitModule(v8::Local<v8::Object> exports,
6+
v8::Local<v8::Value> module_val,
7+
v8::Local<v8::Context> context) {
8+
v8::Isolate* isolate = context->GetIsolate();
9+
v8::Local<v8::Object> module = module_val.As<v8::Object>();
10+
module
11+
->Set(context,
12+
v8::String::NewFromUtf8(isolate, "exports").ToLocalChecked(),
13+
v8::String::NewFromUtf8(isolate, "hello world").ToLocalChecked())
14+
.FromJust();
15+
}
16+
17+
NODE_MODULE_CONTEXT_AWARE(Binding, InitModule)

0 commit comments

Comments
 (0)