-
Notifications
You must be signed in to change notification settings - Fork 29.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
esm: add experimental support for addon modules #55844
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -52,6 +52,7 @@ const { | |||||
ERR_UNKNOWN_BUILTIN_MODULE, | ||||||
} = require('internal/errors').codes; | ||||||
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); | ||||||
const { validateNull } = require('internal/validators'); | ||||||
const moduleWrap = internalBinding('module_wrap'); | ||||||
const { ModuleWrap } = moduleWrap; | ||||||
|
||||||
|
@@ -225,6 +226,48 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) { | |||||
}, 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 { exportNames, module } = cjsEmplaceModuleCacheEntry(filename); | ||||||
if (exportNames === undefined) { | ||||||
// Addon export names are not known until the addon is loaded. | ||||||
module[kModuleExportNames] = ['default', 'module.exports']; | ||||||
} | ||||||
cjsCache.set(url, module); | ||||||
|
||||||
if (isMain) { | ||||||
setOwnProperty(process, 'mainModule', module); | ||||||
} | ||||||
|
||||||
const wrapperNames = module[kModuleExportNames]; | ||||||
return new ModuleWrap(url, undefined, wrapperNames, function() { | ||||||
debug(`Loading CJSModule ${url}`); | ||||||
|
||||||
if (!module.loaded) { | ||||||
wrapModuleLoad(filename, null, isMain); | ||||||
} | ||||||
|
||||||
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(); | ||||||
|
||||||
|
@@ -276,12 +319,7 @@ translators.set('commonjs', function commonjsStrategy(url, source, isMain) { | |||||
return createCJSModuleWrap(url, source, isMain, cjsLoader); | ||||||
}); | ||||||
|
||||||
/** | ||||||
* 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. | ||||||
*/ | ||||||
function cjsPreparseModuleExports(filename, source) { | ||||||
function cjsEmplaceModuleCacheEntry(filename) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this please get a JSDoc? |
||||||
// TODO: Do we want to keep hitting the user mutable CJS loader here? | ||||||
let module = CJSModule._cache[filename]; | ||||||
if (module && module[kModuleExportNames] !== undefined) { | ||||||
|
@@ -293,10 +331,23 @@ function cjsPreparseModuleExports(filename, source) { | |||||
module.filename = filename; | ||||||
module.paths = CJSModule._nodeModulePaths(module.path); | ||||||
module[kIsCachedByESMLoader] = true; | ||||||
module[kModuleSource] = source; | ||||||
CJSModule._cache[filename] = module; | ||||||
} | ||||||
|
||||||
return { module }; | ||||||
} | ||||||
|
||||||
/** | ||||||
* 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. | ||||||
*/ | ||||||
function cjsPreparseModuleExports(filename, source) { | ||||||
const { module, exportNames: cachedExportNames } = cjsEmplaceModuleCacheEntry(filename); | ||||||
if (cachedExportNames !== undefined) { | ||||||
return { module, exportNames: cachedExportNames }; | ||||||
} | ||||||
|
||||||
let exports, reexports; | ||||||
try { | ||||||
({ exports, reexports } = cjsParse(source || '')); | ||||||
|
@@ -308,11 +359,10 @@ function cjsPreparseModuleExports(filename, source) { | |||||
const exportNames = new SafeSet(new SafeArrayIterator(exports)); | ||||||
|
||||||
// Set first for cycles. | ||||||
module[kModuleSource] = source; | ||||||
module[kModuleExportNames] = exportNames; | ||||||
|
||||||
if (reexports.length) { | ||||||
module.filename = filename; | ||||||
module.paths = CJSModule._nodeModulePaths(module.path); | ||||||
for (let i = 0; i < reexports.length; i++) { | ||||||
const reexport = reexports[i]; | ||||||
let resolved; | ||||||
|
@@ -459,6 +509,19 @@ translators.set('wasm', async function(url, source) { | |||||
}).module; | ||||||
}); | ||||||
|
||||||
// Strategy for loading a addon | ||||||
translators.set('addon', async function(url, source, isMain) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: This function doesn’t contain
Suggested change
|
||||||
emitExperimentalWarning('Importing addons'); | ||||||
|
||||||
// The addon must be loaded from file system with dlopen. Assert | ||||||
// the source is null. | ||||||
validateNull(source, 'source'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not an argument, it wouldn't make sense to throw There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO asserting it to be null could prevent loader from unexpectedly reading the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In any case, let's not throw |
||||||
|
||||||
debug(`Translating addon ${url}`); | ||||||
|
||||||
return createCJSNoSourceModuleWrap(url, isMain); | ||||||
}); | ||||||
|
||||||
// Strategy for loading a commonjs TypeScript module | ||||||
translators.set('commonjs-typescript', function(url, source) { | ||||||
emitExperimentalWarning('Type Stripping'); | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -487,6 +487,19 @@ const validateUndefined = hideStackFrames((value, name) => { | |||||
throw new ERR_INVALID_ARG_TYPE(name, 'undefined', value); | ||||||
}); | ||||||
|
||||||
/** | ||||||
* @callback validateNull | ||||||
* @param {*} value | ||||||
* @param {string} name | ||||||
* @returns {asserts value is null} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Nit: The return description isn’t a value, it’s an explanation, so it doesn’t go in braces (I think?). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
*/ | ||||||
|
||||||
/** @type {validateNull} */ | ||||||
const validateNull = hideStackFrames((value, name) => { | ||||||
if (value !== null) | ||||||
throw new ERR_INVALID_ARG_TYPE(name, 'null', value); | ||||||
}); | ||||||
|
||||||
/** | ||||||
* @template T | ||||||
* @param {T} value | ||||||
|
@@ -623,6 +636,7 @@ module.exports = { | |||||
validateFunction, | ||||||
validateInt32, | ||||||
validateInteger, | ||||||
validateNull, | ||||||
validateNumber, | ||||||
validateObject, | ||||||
kValidateObjectNone, | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -411,6 +411,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | |||||
"Treat the entrypoint as a URL", | ||||||
&EnvironmentOptions::entry_is_url, | ||||||
kAllowedInEnvvar); | ||||||
AddOption("--experimental-addon-modules", | ||||||
"experimental ES Module support for addons", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Technically we’re adding support to |
||||||
&EnvironmentOptions::experimental_addon_modules, | ||||||
kAllowedInEnvvar); | ||||||
AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar); | ||||||
AddOption("--experimental-eventsource", | ||||||
"experimental EventSource API", | ||||||
|
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) |
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) |
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
'targets': [ | ||
{ | ||
'target_name': 'binding', | ||
'sources': [ 'binding.cc' ], | ||
'includes': ['../common.gypi'], | ||
}, | ||
{ | ||
'target_name': 'binding-export-default', | ||
'sources': [ 'binding-export-default.cc' ], | ||
'includes': ['../common.gypi'], | ||
}, | ||
{ | ||
'target_name': 'binding-export-primitive', | ||
'sources': [ 'binding-export-primitive.cc' ], | ||
'includes': ['../common.gypi'], | ||
} | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.