Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ Otherwise, the file is loaded using the CommonJS module loader. See
When loading, the [ES module loader][Modules loaders] loads the program
entry point, the `node` command will accept as input only files with `.js`,
`.mjs`, `.cjs` or `.wasm` extensions; and with no extension when
[`--experimental-default-type=module`][] is passed.
[`--experimental-default-type=module`][] is passed. With the following flags,
additional file extensions are enabled:

* [`--experimental-addon-modules`][] for files with `.node` extension.

## Options

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

### `--experimental-addon-modules`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

Enable experimental import support for `.node` addons.

### `--experimental-async-context-frame`

<!-- YAML
Expand Down Expand Up @@ -3330,6 +3343,7 @@ one is included in the list below.
* `--enable-source-maps`
* `--entry-url`
* `--experimental-abortcontroller`
* `--experimental-addon-modules`
* `--experimental-async-context-frame`
* `--experimental-default-type`
* `--experimental-detect-module`
Expand Down Expand Up @@ -3926,6 +3940,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--disable-sigusr1`]: #--disable-sigusr1
[`--env-file-if-exists`]: #--env-file-if-existsfile
[`--env-file`]: #--env-filefile
[`--experimental-addon-modules`]: #--experimental-addon-modules
[`--experimental-default-type=module`]: #--experimental-default-typetype
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
[`--heap-prof-dir`]: #--heap-prof-dir
Expand Down
19 changes: 11 additions & 8 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1129,18 +1129,21 @@ _isImports_, _conditions_)
> 5. If _url_ ends in
> _".wasm"_, then
> 1. Return _"wasm"_.
> 6. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
> 7. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
> 8. Let _packageType_ be **null**.
> 9. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
> 1. Set _packageType_ to _pjson.type_.
> 10. If _url_ ends in _".js"_, then
> 6. If `--experimental-addon-modules` is enabled and _url_ ends in
> _".node"_, then
> 1. Return _"addon"_.
> 7. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
> 8. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
> 9. Let _packageType_ be **null**.
> 10. If _pjson?.type_ is _"module"_ or _"commonjs"_, then
> 1. Set _packageType_ to _pjson.type_.
> 11. If _url_ ends in _".js"_, then
> 1. If _packageType_ is not **null**, then
> 1. Return _packageType_.
> 2. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
> 1. Return _"module"_.
> 3. Return _"commonjs"_.
> 11. If _url_ does not have any extension, then
> 12. If _url_ does not have any extension, then
> 1. If _packageType_ is _"module"_ and the file at _url_ contains the
> header for a WebAssembly module, then
> 1. Return _"wasm"_.
Expand All @@ -1149,7 +1152,7 @@ _isImports_, _conditions_)
> 3. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then
> 1. Return _"module"_.
> 4. Return _"commonjs"_.
> 12. Return **undefined** (will throw during load phase).
> 13. Return **undefined** (will throw during load phase).

**LOOKUP\_PACKAGE\_SCOPE**(_url_)

Expand Down
1 change: 1 addition & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,7 @@ The final value of `format` must be one of the following:

| `format` | Description | Acceptable types for `source` returned by `load` |
| ----------------------- | ----------------------------------------------------- | -------------------------------------------------- |
| `'addon'` | Load a Node.js addon | {null} |
| `'builtin'` | Load a Node.js builtin module | {null} |
| `'commonjs-typescript'` | Load a Node.js CommonJS module with TypeScript syntax | {string\|ArrayBuffer\|TypedArray\|null\|undefined} |
| `'commonjs'` | Load a Node.js CommonJS module | {string\|ArrayBuffer\|TypedArray\|null\|undefined} |
Expand Down
3 changes: 3 additions & 0 deletions doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@
"entry-url": {
"type": "boolean"
},
"experimental-addon-modules": {
"type": "boolean"
},
"experimental-async-context-frame": {
"type": "boolean"
},
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ Enable Source Map V3 support for stack traces.
.It Fl -entry-url
Interpret the entry point as a URL.
.
.It Fl -experimental-addon-modules
Enable experimental addon module support.
.
.It Fl -experimental-default-type Ns = Ns Ar type
Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
.js or extensionless files with no sibling or parent package.json;
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/modules/esm/formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const extensionFormatMap = {
'.wasm': 'wasm',
};

if (getOptionValue('--experimental-addon-modules')) {
extensionFormatMap['.node'] = 'addon';
}
if (getOptionValue('--experimental-strip-types')) {
extensionFormatMap['.ts'] = 'module-typescript';
extensionFormatMap['.mts'] = 'module-typescript';
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/modules/esm/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ function defaultLoad(url, context = kEmptyObject) {
if (urlInstance.protocol === 'node:') {
source = null;
format ??= 'builtin';
} else if (format === 'addon') {
// Skip loading addon file content. It must be loaded with dlopen from file system.
source = null;
} else if (format !== 'commonjs' || defaultType === 'module') {
if (source == null) {
({ responseURL, source } = getSourceSync(urlInstance, context));
Expand Down
104 changes: 88 additions & 16 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const {
ArrayPrototypePush,
Boolean,
FunctionPrototypeCall,
JSONParse,
ObjectAssign,
Expand Down Expand Up @@ -52,6 +51,7 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
});
const { emitExperimentalWarning, kEmptyObject, setOwnProperty, isWindows } = require('internal/util');
const {
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_UNKNOWN_BUILTIN_MODULE,
} = require('internal/errors').codes;
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
Expand Down Expand Up @@ -186,7 +186,7 @@ function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModul
// In case the source was not provided by the `load` step, we need fetch it now.
source = stringify(source ?? getSource(new URL(url)).source);

const { exportNames, module } = cjsPreparseModuleExports(filename, source, isMain, format);
const { exportNames, module } = cjsPreparseModuleExports(filename, source, format);
cjsCache.set(url, module);
const namesWithDefault = exportNames.has('default') ?
[...exportNames] : ['default', ...exportNames];
Expand Down Expand Up @@ -227,6 +227,47 @@ function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModul
}, module);
}

/**
* Creates a ModuleWrap object for a CommonJS module without source texts.
* @param {string} url - The URL of the module.
* @param {boolean} isMain - Whether the module is the main module.
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
*/
function createCJSNoSourceModuleWrap(url, isMain) {
debug(`Translating CJSModule without source ${url}`);

const filename = urlToFilename(url);

const module = cjsEmplaceModuleCacheEntry(filename);
cjsCache.set(url, module);

if (isMain) {
setOwnProperty(process, 'mainModule', module);
}

// Addon export names are not known until the addon is loaded.
const exportNames = ['default', 'module.exports'];
return new ModuleWrap(url, undefined, exportNames, function evaluationCallback() {
debug(`Loading CJSModule ${url}`);

if (!module.loaded) {
wrapModuleLoad(filename, null, isMain);
}

/** @type {import('./loader').ModuleExports} */
let exports;
if (module[kModuleExport] !== undefined) {
exports = module[kModuleExport];
module[kModuleExport] = undefined;
} else {
({ exports } = module);
}

this.setExport('default', exports);
this.setExport('module.exports', exports);
}, module);
}

translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) {
initCJSParseSync();

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

/**
* Get or create an entry in the CJS module cache for the given filename.
* @param {string} filename CJS module filename
* @returns {CJSModule} the cached CJS module entry
*/
function cjsEmplaceModuleCacheEntry(filename, exportNames) {
// TODO: Do we want to keep hitting the user mutable CJS loader here?
let cjsMod = CJSModule._cache[filename];
if (cjsMod) {
return cjsMod;
}

cjsMod = new CJSModule(filename);
cjsMod.filename = filename;
cjsMod.paths = CJSModule._nodeModulePaths(cjsMod.path);
cjsMod[kIsCachedByESMLoader] = true;
CJSModule._cache[filename] = cjsMod;

return cjsMod;
}

/**
* Pre-parses a CommonJS module's exports and re-exports.
* @param {string} filename - The filename of the module.
* @param {string} [source] - The source code of the module.
* @param {boolean} isMain - Whether it is pre-parsing for the entry point.
* @param {string} format
* @param {string} [format]
*/
function cjsPreparseModuleExports(filename, source, isMain, format) {
let module = CJSModule._cache[filename];
if (module && module[kModuleExportNames] !== undefined) {
function cjsPreparseModuleExports(filename, source, format) {
const module = cjsEmplaceModuleCacheEntry(filename);
if (module[kModuleExportNames] !== undefined) {
return { module, exportNames: module[kModuleExportNames] };
}
const loaded = Boolean(module);
if (!loaded) {
module = new CJSModule(filename);
module.filename = filename;
module.paths = CJSModule._nodeModulePaths(module.path);
module[kIsCachedByESMLoader] = true;
CJSModule._cache[filename] = module;
}

if (source === undefined) {
({ source } = loadSourceForCJSWithHooks(module, filename, format));
Expand Down Expand Up @@ -337,7 +390,7 @@ function cjsPreparseModuleExports(filename, source, isMain, format) {

if (format === 'commonjs' ||
(!BuiltinModule.normalizeRequirableId(resolved) && findLongestRegisteredExtension(resolved) === '.js')) {
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, false, format);
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, format);
for (const name of reexportNames) {
exportNames.add(name);
}
Expand Down Expand Up @@ -519,6 +572,25 @@ translators.set('wasm', function(url, source) {
return module;
});

// Strategy for loading a addon
translators.set('addon', function translateAddon(url, source, isMain) {
emitExperimentalWarning('Importing addons');

// The addon must be loaded from file system with dlopen. Assert
// the source is null.
if (source !== null) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'null',
'load',
'source',
source);
}

debug(`Translating addon ${url}`);

return createCJSNoSourceModuleWrap(url, isMain);
});

// Strategy for loading a commonjs TypeScript module
translators.set('commonjs-typescript', function(url, source, isMain) {
assertBufferSource(source, true, 'load');
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"Treat the entrypoint as a URL",
&EnvironmentOptions::entry_is_url,
kAllowedInEnvvar);
AddOption("--experimental-addon-modules",
"experimental import support for addons",
&EnvironmentOptions::experimental_addon_modules,
kAllowedInEnvvar);
AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar);
AddOption("--experimental-eventsource",
"experimental EventSource API",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class EnvironmentOptions : public Options {
bool require_module = true;
std::string dns_result_order;
bool enable_source_maps = false;
bool experimental_addon_modules = false;
bool experimental_eventsource = false;
bool experimental_fetch = true;
bool experimental_websocket = true;
Expand Down
1 change: 1 addition & 0 deletions test/addons/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Makefile
*.mk
gyp-mac-tool
/*/build
/esm/node_modules/*/build
17 changes: 17 additions & 0 deletions test/addons/esm/binding-export-default.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <node.h>
#include <uv.h>
#include <v8.h>

static void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(
v8::String::NewFromUtf8(isolate, "hello world").ToLocalChecked());
}

static void InitModule(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context) {
NODE_SET_METHOD(exports, "default", Method);
}

NODE_MODULE_CONTEXT_AWARE(Binding, InitModule)
17 changes: 17 additions & 0 deletions test/addons/esm/binding-export-primitive.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <node.h>
#include <uv.h>
#include <v8.h>

static void InitModule(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module_val,
v8::Local<v8::Context> context) {
v8::Isolate* isolate = context->GetIsolate();
v8::Local<v8::Object> module = module_val.As<v8::Object>();
module
->Set(context,
v8::String::NewFromUtf8(isolate, "exports").ToLocalChecked(),
v8::String::NewFromUtf8(isolate, "hello world").ToLocalChecked())
.FromJust();
}

NODE_MODULE_CONTEXT_AWARE(Binding, InitModule)
17 changes: 17 additions & 0 deletions test/addons/esm/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include <node.h>
#include <uv.h>
#include <v8.h>

static void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(
v8::String::NewFromUtf8(isolate, "world").ToLocalChecked());
}

static void InitModule(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context) {
NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE_CONTEXT_AWARE(Binding, InitModule)
Loading
Loading