GeezerVison is showing github.com — esm: add experimental support for addon modules · nodejs/node@cdb631e
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +17-2Lines changed: 17 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -45,8 +45,11 @@ Otherwise, the file is loaded using the CommonJS module loader. See | 45 | 45 |
| 46 | 46 | When loading, the [ES module loader][Modules loaders] loads the program | 47 | 47 | entry point, the `node` command will accept as input only files with `.js`, | 48 | | -`.mjs`, or `.cjs` extensions; and with `.wasm` extensions when | 49 | | -[`--experimental-wasm-modules`][] is enabled. | | 48 | +`.mjs`, or `.cjs` extensions. With the following flags, additional file | | 49 | +extensions are enabled: | | 50 | + | | 51 | +* [`--experimental-wasm-modules`][] for files with `.wasm` extension. | | 52 | +* [`--experimental-addon-modules`][] for files with `.node` extension. | 50 | 53 |
| 51 | 54 | ## Options | 52 | 55 |
| @@ -879,6 +882,16 @@ and `"` are usable. | 879 | 882 | It is possible to run code containing inline types by passing | 880 | 883 | [`--experimental-strip-types`][]. | 881 | 884 |
| | 885 | +### `--experimental-addon-modules` | | 886 | + | | 887 | +<!-- YAML | | 888 | +added: REPLACEME | | 889 | +--> | | 890 | + | | 891 | +> Stability: 1.0 - Early development | | 892 | +
| | 893 | +Enable experimental import support for `.node` addons. | | 894 | + | 882 | 895 | ### `--experimental-async-context-frame` | 883 | 896 |
| 884 | 897 | <!-- YAML | @@ -3046,6 +3059,7 @@ one is included in the list below. | 3046 | 3059 | * `--enable-source-maps` | 3047 | 3060 | * `--entry-url` | 3048 | 3061 | * `--experimental-abortcontroller` | | 3062 | +* `--experimental-addon-modules` | 3049 | 3063 | * `--experimental-async-context-frame` | 3050 | 3064 | * `--experimental-detect-module` | 3051 | 3065 | * `--experimental-eventsource` | @@ -3617,6 +3631,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12 | 3617 | 3631 | [`--diagnostic-dir`]: #--diagnostic-dirdirectory | 3618 | 3632 | [`--env-file-if-exists`]: #--env-file-if-existsconfig | 3619 | 3633 | [`--env-file`]: #--env-fileconfig | | 3634 | +[`--experimental-addon-modules`]: #--experimental-addon-modules | 3620 | 3635 | [`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs | 3621 | 3636 | [`--experimental-strip-types`]: #--experimental-strip-types | 3622 | 3637 | [`--experimental-wasm-modules`]: #--experimental-wasm-modules | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/esm.md +11-8Lines changed: 11 additions & 8 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1039,18 +1039,21 @@ _isImports_, _conditions_) | 1039 | 1039 | > 5. If `--experimental-wasm-modules` is enabled and _url_ ends in | 1040 | 1040 | > _".wasm"_, then | 1041 | 1041 | > 1. Return _"wasm"_. | 1042 | | -> 6. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_). | 1043 | | -> 7. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_). | 1044 | | -> 8. Let _packageType_ be **null**. | 1045 | | -> 9. If _pjson?.type_ is _"module"_ or _"commonjs"_, then | 1046 | | -> 1. Set _packageType_ to _pjson.type_. | 1047 | | -> 10. If _url_ ends in _".js"_, then | | 1042 | +> 6. If `--experimental-addon-modules` is enabled and _url_ ends in | | 1043 | +> _".node"_, then | | 1044 | +> 1. Return _"addon"_. | | 1045 | +> 7. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_). | | 1046 | +> 8. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_). | | 1047 | +> 9. Let _packageType_ be **null**. | | 1048 | +> 10. If _pjson?.type_ is _"module"_ or _"commonjs"_, then | | 1049 | +> 1. Set _packageType_ to _pjson.type_. | | 1050 | +> 11. If _url_ ends in _".js"_, then | 1048 | 1051 | > 1. If _packageType_ is not **null**, then | 1049 | 1052 | > 1. Return _packageType_. | 1050 | 1053 | > 2. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then | 1051 | 1054 | > 1. Return _"module"_. | 1052 | 1055 | > 3. Return _"commonjs"_. | 1053 | | -> 11. If _url_ does not have any extension, then | | 1056 | +> 12. If _url_ does not have any extension, then | 1054 | 1057 | > 1. If _packageType_ is _"module"_ and `--experimental-wasm-modules` is | 1055 | 1058 | > enabled and the file at _url_ contains the header for a WebAssembly | 1056 | 1059 | > module, then | @@ -1060,7 +1063,7 @@ _isImports_, _conditions_) | 1060 | 1063 | > 3. If the result of **DETECT\_MODULE\_SYNTAX**(_source_) is true, then | 1061 | 1064 | > 1. Return _"module"_. | 1062 | 1065 | > 4. Return _"commonjs"_. | 1063 | | -> 12. Return **undefined** (will throw during load phase). | | 1066 | +> 13. Return **undefined** (will throw during load phase). | 1064 | 1067 |
| 1065 | 1068 | **LOOKUP\_PACKAGE\_SCOPE**(_url_) | 1066 | 1069 |
| |
Collapse file Copy file name to clipboardExpand all lines: doc/api/module.md +1Lines changed: 1 addition & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1160,6 +1160,7 @@ The final value of `format` must be one of the following: | 1160 | 1160 |
| 1161 | 1161 | | `format` | Description | Acceptable types for `source` returned by `load` | | 1162 | 1162 | | ------------ | ------------------------------ | -------------------------------------------------- | | | 1163 | +| `'addon'` | Load a Node.js addon | {null} | | 1163 | 1164 | | `'builtin'` | Load a Node.js builtin module | {null} | | 1164 | 1165 | | `'commonjs'` | Load a Node.js CommonJS module | {string\|ArrayBuffer\|TypedArray\|null\|undefined} | | 1165 | 1166 | | `'json'` | Load a JSON file | {string\|ArrayBuffer\|TypedArray} | | |
Collapse file Copy file name to clipboardExpand all lines: doc/node.1 +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -163,6 +163,9 @@ Enable Source Map V3 support for stack traces. | 163 | 163 | .It Fl -entry-url | 164 | 164 | Interpret the entry point as a URL. | 165 | 165 | . | | 166 | +.It Fl -experimental-addon-modules | | 167 | +Enable experimental addon module support. | | 168 | +. | 166 | 169 | .It Fl -experimental-import-meta-resolve | 167 | 170 | Enable experimental ES modules support for import.meta.resolve(). | 168 | 171 | . | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/formats.js +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,6 +10,7 @@ const fsBindings = internalBinding('fs'); | 10 | 10 | const { fs: fsConstants } = internalBinding('constants'); | 11 | 11 |
| 12 | 12 | const experimentalWasmModules = getOptionValue('--experimental-wasm-modules'); | | 13 | +const experimentalAddonModules = getOptionValue('--experimental-addon-modules'); | 13 | 14 |
| 14 | 15 | const extensionFormatMap = { | 15 | 16 | '__proto__': null, | @@ -23,6 +24,10 @@ if (experimentalWasmModules) { | 23 | 24 | extensionFormatMap['.wasm'] = 'wasm'; | 24 | 25 | } | 25 | 26 |
| | 27 | +if (experimentalAddonModules) { | | 28 | + extensionFormatMap['.node'] = 'addon'; | | 29 | +} | | 30 | + | 26 | 31 | if (getOptionValue('--experimental-strip-types')) { | 27 | 32 | extensionFormatMap['.ts'] = 'module-typescript'; | 28 | 33 | extensionFormatMap['.mts'] = 'module-typescript'; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/load.js +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -105,6 +105,9 @@ async function defaultLoad(url, context = kEmptyObject) { | 105 | 105 | if (urlInstance.protocol === 'node:') { | 106 | 106 | source = null; | 107 | 107 | format ??= 'builtin'; | | 108 | + } else if (format === 'addon') { | | 109 | + // Skip loading addon file content. It must be loaded with dlopen from file system. | | 110 | + source = null; | 108 | 111 | } else if (format !== 'commonjs') { | 109 | 112 | if (source == null) { | 110 | 113 | ({ responseURL, source } = await getSource(urlInstance, context)); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js +88-16Lines changed: 88 additions & 16 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | const { | 4 | 4 | ArrayPrototypeMap, | 5 | 5 | ArrayPrototypePush, | 6 | | - Boolean, | 7 | 6 | FunctionPrototypeCall, | 8 | 7 | JSONParse, | 9 | 8 | ObjectKeys, | @@ -52,6 +51,7 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { | 52 | 51 | }); | 53 | 52 | const { emitExperimentalWarning, kEmptyObject, setOwnProperty, isWindows } = require('internal/util'); | 54 | 53 | const { | | 54 | + ERR_INVALID_RETURN_PROPERTY_VALUE, | 55 | 55 | ERR_UNKNOWN_BUILTIN_MODULE, | 56 | 56 | } = require('internal/errors').codes; | 57 | 57 | const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); | @@ -185,7 +185,7 @@ function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModul | 185 | 185 | // In case the source was not provided by the `load` step, we need fetch it now. | 186 | 186 | source = stringify(source ?? getSource(new URL(url)).source); | 187 | 187 |
| 188 | | - const { exportNames, module } = cjsPreparseModuleExports(filename, source, isMain, format); | | 188 | + const { exportNames, module } = cjsPreparseModuleExports(filename, source, format); | 189 | 189 | cjsCache.set(url, module); | 190 | 190 |
| 191 | 191 | const wrapperNames = [...exportNames, 'module.exports']; | @@ -229,6 +229,47 @@ function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModul | 229 | 229 | }, module); | 230 | 230 | } | 231 | 231 |
| | 232 | +/** | | 233 | + * Creates a ModuleWrap object for a CommonJS module without source texts. | | 234 | + * @param {string} url - The URL of the module. | | 235 | + * @param {boolean} isMain - Whether the module is the main module. | | 236 | + * @returns {ModuleWrap} The ModuleWrap object for the CommonJS module. | | 237 | + */ | | 238 | +function createCJSNoSourceModuleWrap(url, isMain) { | | 239 | + debug(`Translating CJSModule without source ${url}`); | | 240 | + | | 241 | + const filename = urlToFilename(url); | | 242 | + | | 243 | + const module = cjsEmplaceModuleCacheEntry(filename); | | 244 | + cjsCache.set(url, module); | | 245 | + | | 246 | + if (isMain) { | | 247 | + setOwnProperty(process, 'mainModule', module); | | 248 | + } | | 249 | + | | 250 | + // Addon export names are not known until the addon is loaded. | | 251 | + const exportNames = ['default', 'module.exports']; | | 252 | + return new ModuleWrap(url, undefined, exportNames, function evaluationCallback() { | | 253 | + debug(`Loading CJSModule ${url}`); | | 254 | + | | 255 | + if (!module.loaded) { | | 256 | + wrapModuleLoad(filename, null, isMain); | | 257 | + } | | 258 | + | | 259 | + /** @type {import('./loader').ModuleExports} */ | | 260 | + let exports; | | 261 | + if (module[kModuleExport] !== undefined) { | | 262 | + exports = module[kModuleExport]; | | 263 | + module[kModuleExport] = undefined; | | 264 | + } else { | | 265 | + ({ exports } = module); | | 266 | + } | | 267 | + | | 268 | + this.setExport('default', exports); | | 269 | + this.setExport('module.exports', exports); | | 270 | + }, module); | | 271 | +} | | 272 | + | 232 | 273 | translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) { | 233 | 274 | initCJSParseSync(); | 234 | 275 |
| @@ -280,26 +321,38 @@ translators.set('commonjs', function commonjsStrategy(url, source, isMain) { | 280 | 321 | return createCJSModuleWrap(url, source, isMain, 'commonjs', cjsLoader); | 281 | 322 | }); | 282 | 323 |
| | 324 | +/** | | 325 | + * Get or create an entry in the CJS module cache for the given filename. | | 326 | + * @param {string} filename CJS module filename | | 327 | + * @returns {CJSModule} the cached CJS module entry | | 328 | + */ | | 329 | +function cjsEmplaceModuleCacheEntry(filename, exportNames) { | | 330 | + // TODO: Do we want to keep hitting the user mutable CJS loader here? | | 331 | + let cjsMod = CJSModule._cache[filename]; | | 332 | + if (cjsMod) { | | 333 | + return cjsMod; | | 334 | + } | | 335 | + | | 336 | + cjsMod = new CJSModule(filename); | | 337 | + cjsMod.filename = filename; | | 338 | + cjsMod.paths = CJSModule._nodeModulePaths(cjsMod.path); | | 339 | + cjsMod[kIsCachedByESMLoader] = true; | | 340 | + CJSModule._cache[filename] = cjsMod; | | 341 | + | | 342 | + return cjsMod; | | 343 | +} | | 344 | + | 283 | 345 | /** | 284 | 346 | * Pre-parses a CommonJS module's exports and re-exports. | 285 | 347 | * @param {string} filename - The filename of the module. | 286 | 348 | * @param {string} [source] - The source code of the module. | 287 | | - * @param {boolean} isMain - Whether it is pre-parsing for the entry point. | 288 | | - * @param {string} format | | 349 | + * @param {string} [format] | 289 | 350 | */ | 290 | | -function cjsPreparseModuleExports(filename, source, isMain, format) { | 291 | | - let module = CJSModule._cache[filename]; | 292 | | - if (module && module[kModuleExportNames] !== undefined) { | | 351 | +function cjsPreparseModuleExports(filename, source, format) { | | 352 | + const module = cjsEmplaceModuleCacheEntry(filename); | | 353 | + if (module[kModuleExportNames] !== undefined) { | 293 | 354 | return { module, exportNames: module[kModuleExportNames] }; | 294 | 355 | } | 295 | | - const loaded = Boolean(module); | 296 | | - if (!loaded) { | 297 | | - module = new CJSModule(filename); | 298 | | - module.filename = filename; | 299 | | - module.paths = CJSModule._nodeModulePaths(module.path); | 300 | | - module[kIsCachedByESMLoader] = true; | 301 | | - CJSModule._cache[filename] = module; | 302 | | - } | 303 | 356 |
| 304 | 357 | if (source === undefined) { | 305 | 358 | ({ source } = loadSourceForCJSWithHooks(module, filename, format)); | @@ -340,7 +393,7 @@ function cjsPreparseModuleExports(filename, source, isMain, format) { | 340 | 393 |
| 341 | 394 | if (format === 'commonjs' || | 342 | 395 | (!BuiltinModule.normalizeRequirableId(resolved) && findLongestRegisteredExtension(resolved) === '.js')) { | 343 | | - const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, false, format); | | 396 | + const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, format); | 344 | 397 | for (const name of reexportNames) { | 345 | 398 | exportNames.add(name); | 346 | 399 | } | @@ -462,6 +515,25 @@ translators.set('wasm', async function(url, source) { | 462 | 515 | }).module; | 463 | 516 | }); | 464 | 517 |
| | 518 | +// Strategy for loading a addon | | 519 | +translators.set('addon', function translateAddon(url, source, isMain) { | | 520 | + emitExperimentalWarning('Importing addons'); | | 521 | + | | 522 | + // The addon must be loaded from file system with dlopen. Assert | | 523 | + // the source is null. | | 524 | + if (source !== null) { | | 525 | + throw new ERR_INVALID_RETURN_PROPERTY_VALUE( | | 526 | + 'null', | | 527 | + 'load', | | 528 | + 'source', | | 529 | + source); | | 530 | + } | | 531 | + | | 532 | + debug(`Translating addon ${url}`); | | 533 | + | | 534 | + return createCJSNoSourceModuleWrap(url, isMain); | | 535 | +}); | | 536 | + | 465 | 537 | // Strategy for loading a commonjs TypeScript module | 466 | 538 | translators.set('commonjs-typescript', function(url, source) { | 467 | 539 | emitExperimentalWarning('Type Stripping'); | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -409,6 +409,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 409 | 409 | "Treat the entrypoint as a URL", | 410 | 410 | &EnvironmentOptions::entry_is_url, | 411 | 411 | kAllowedInEnvvar); | | 412 | + AddOption("--experimental-addon-modules", | | 413 | + "experimental import support for addons", | | 414 | + &EnvironmentOptions::experimental_addon_modules, | | 415 | + kAllowedInEnvvar); | 412 | 416 | AddOption("--experimental-abortcontroller", "", NoOp{}, kAllowedInEnvvar); | 413 | 417 | AddOption("--experimental-eventsource", | 414 | 418 | "experimental EventSource API", | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -120,6 +120,7 @@ class EnvironmentOptions : public Options { | 120 | 120 | bool require_module = true; | 121 | 121 | std::string dns_result_order; | 122 | 122 | bool enable_source_maps = false; | | 123 | + bool experimental_addon_modules = false; | 123 | 124 | bool experimental_eventsource = false; | 124 | 125 | bool experimental_fetch = true; | 125 | 126 | bool experimental_websocket = true; | |
Collapse file Copy file name to clipboardExpand all lines: test/addons/.gitignore +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 5 | 5 | *.mk | 6 | 6 | gyp-mac-tool | 7 | 7 | /*/build | | 8 | +/esm/node_modules/*/build |
|