GeezerVison is showing github.com — vm: add dynamic import support · nodejs/node@134d1e9
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/errors.md +5Lines changed: 5 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1795,6 +1795,11 @@ The V8 `BreakIterator` API was used but the full ICU data set is not installed. | 1795 | 1795 | While using the Performance Timing API (`perf_hooks`), no valid performance | 1796 | 1796 | entry types were found. | 1797 | 1797 |
| | 1798 | +<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a> | | 1799 | +### ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING | | 1800 | + | | 1801 | +A dynamic import callback was not specified. | | 1802 | + | 1798 | 1803 | <a id="ERR_VM_MODULE_ALREADY_LINKED"></a> | 1799 | 1804 | ### ERR_VM_MODULE_ALREADY_LINKED | 1800 | 1805 |
| |
Collapse file Copy file name to clipboardExpand all lines: doc/api/vm.md +21-1Lines changed: 21 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -167,10 +167,19 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); | 167 | 167 | in stack traces produced by this `Module`. | 168 | 168 | * `columnOffset` {integer} Specifies the column number offset that is | 169 | 169 | displayed in stack traces produced by this `Module`. | 170 | | - * `initalizeImportMeta` {Function} Called during evaluation of this `Module` | | 170 | + * `initializeImportMeta` {Function} Called during evaluation of this `Module` | 171 | 171 | to initialize the `import.meta`. This function has the signature `(meta, | 172 | 172 | module)`, where `meta` is the `import.meta` object in the `Module`, and | 173 | 173 | `module` is this `vm.SourceTextModule` object. | | 174 | + * `importModuleDynamically` {Function} Called during evaluation of this | | 175 | + module when `import()` is called. This function has the signature | | 176 | + `(specifier, module)` where `specifier` is the specifier passed to | | 177 | + `import()` and `module` is this `vm.SourceTextModule`. If this option is | | 178 | + not specified, calls to `import()` will reject with | | 179 | + [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a | | 180 | + [Module Namespace Object][], but returning a `vm.SourceTextModule` is | | 181 | + recommended in order to take advantage of error tracking, and to avoid | | 182 | + issues with namespaces that contain `then` function exports. | 174 | 183 |
| 175 | 184 | Creates a new ES `Module` object. | 176 | 185 |
| @@ -436,6 +445,15 @@ changes: | 436 | 445 | The `cachedDataProduced` value will be set to either `true` or `false` | 437 | 446 | depending on whether code cache data is produced successfully. | 438 | 447 | This option is deprecated in favor of `script.createCachedData()`. | | 448 | + * `importModuleDynamically` {Function} Called during evaluation of this | | 449 | + module when `import()` is called. This function has the signature | | 450 | + `(specifier, module)` where `specifier` is the specifier passed to | | 451 | + `import()` and `module` is this `vm.SourceTextModule`. If this option is | | 452 | + not specified, calls to `import()` will reject with | | 453 | + [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a | | 454 | + [Module Namespace Object][], but returning a `vm.SourceTextModule` is | | 455 | + recommended in order to take advantage of error tracking, and to avoid | | 456 | + issues with namespaces that contain `then` function exports. | 439 | 457 |
| 440 | 458 | Creating a new `vm.Script` object compiles `code` but does not run it. The | 441 | 459 | compiled `vm.Script` can be run later multiple times. The `code` is not bound to | @@ -977,6 +995,7 @@ This issue occurs because all contexts share the same microtask and nextTick | 977 | 995 | queues. | 978 | 996 |
| 979 | 997 | [`Error`]: errors.html#errors_class_error | | 998 | +[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING | 980 | 999 | [`URL`]: url.html#url_class_url | 981 | 1000 | [`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval | 982 | 1001 | [`script.runInContext()`]: #vm_script_runincontext_contextifiedsandbox_options | @@ -985,6 +1004,7 @@ queues. | 985 | 1004 | [`vm.createContext()`]: #vm_vm_createcontext_sandbox_options | 986 | 1005 | [`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options | 987 | 1006 | [`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options | | 1007 | +[Module Namespace Object]: https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects | 988 | 1008 | [ECMAScript Module Loader]: esm.html#esm_ecmascript_modules | 989 | 1009 | [Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation | 990 | 1010 | [GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/bootstrap/loaders.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 115 | 115 | }; | 116 | 116 | } | 117 | 117 |
| | 118 | + // Create this WeakMap in js-land because V8 has no C++ API for WeakMap | | 119 | + internalBinding('module_wrap').callbackMap = new WeakMap(); | 118 | 120 | const { ContextifyScript } = process.binding('contextify'); | 119 | 121 |
| 120 | 122 | // Set up NativeModule | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -920,6 +920,8 @@ E('ERR_V8BREAKITERATOR', | 920 | 920 | // This should probably be a `TypeError`. | 921 | 921 | E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', | 922 | 922 | 'At least one valid performance entry type is required', Error); | | 923 | +E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', | | 924 | + 'A dynamic import callback was not specified.', TypeError); | 923 | 925 | E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error); | 924 | 926 | E('ERR_VM_MODULE_DIFFERENT_CONTEXT', | 925 | 927 | 'Linked modules must use the same context', Error); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/cjs/loader.js +14-1Lines changed: 14 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -29,6 +29,7 @@ const assert = require('assert').ok; | 29 | 29 | const fs = require('fs'); | 30 | 30 | const internalFS = require('internal/fs/utils'); | 31 | 31 | const path = require('path'); | | 32 | +const { URL } = require('url'); | 32 | 33 | const { | 33 | 34 | internalModuleReadJSON, | 34 | 35 | internalModuleStat | @@ -642,6 +643,13 @@ Module.prototype.require = function(id) { | 642 | 643 | // (needed for setting breakpoint when called with --inspect-brk) | 643 | 644 | var resolvedArgv; | 644 | 645 |
| | 646 | +function normalizeReferrerURL(referrer) { | | 647 | + if (typeof referrer === 'string' && path.isAbsolute(referrer)) { | | 648 | + return pathToFileURL(referrer).href; | | 649 | + } | | 650 | + return new URL(referrer).href; | | 651 | +} | | 652 | + | 645 | 653 |
| 646 | 654 | // Run the file contents in the correct scope or sandbox. Expose | 647 | 655 | // the correct helper variables (require, module, exports) to | @@ -657,7 +665,12 @@ Module.prototype._compile = function(content, filename) { | 657 | 665 | var compiledWrapper = vm.runInThisContext(wrapper, { | 658 | 666 | filename: filename, | 659 | 667 | lineOffset: 0, | 660 | | - displayErrors: true | | 668 | + displayErrors: true, | | 669 | + importModuleDynamically: experimentalModules ? async (specifier) => { | | 670 | + if (asyncESM === undefined) lazyLoadESM(); | | 671 | + const loader = await asyncESM.loaderPromise; | | 672 | + return loader.import(specifier, normalizeReferrerURL(filename)); | | 673 | + } : undefined, | 661 | 674 | }); | 662 | 675 |
| 663 | 676 | var inspectorWrapper = null; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js +19-3Lines changed: 19 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 |
| 3 | 3 | const { NativeModule } = require('internal/bootstrap/loaders'); | 4 | | -const { ModuleWrap } = internalBinding('module_wrap'); | | 4 | +const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); | 5 | 5 | const { | 6 | 6 | stripShebang, | 7 | 7 | stripBOM | @@ -15,6 +15,8 @@ const { _makeLong } = require('path'); | 15 | 15 | const { SafeMap } = require('internal/safe_globals'); | 16 | 16 | const { URL } = require('url'); | 17 | 17 | const { debuglog, promisify } = require('util'); | | 18 | +const esmLoader = require('internal/process/esm_loader'); | | 19 | + | 18 | 20 | const readFileAsync = promisify(fs.readFile); | 19 | 21 | const readFileSync = fs.readFileSync; | 20 | 22 | const StringReplace = Function.call.bind(String.prototype.replace); | @@ -25,13 +27,27 @@ const debug = debuglog('esm'); | 25 | 27 | const translators = new SafeMap(); | 26 | 28 | module.exports = translators; | 27 | 29 |
| | 30 | +function initializeImportMeta(meta, { url }) { | | 31 | + meta.url = url; | | 32 | +} | | 33 | + | | 34 | +async function importModuleDynamically(specifier, { url }) { | | 35 | + const loader = await esmLoader.loaderPromise; | | 36 | + return loader.import(specifier, url); | | 37 | +} | | 38 | + | 28 | 39 | // Strategy for loading a standard JavaScript module | 29 | 40 | translators.set('esm', async (url) => { | 30 | 41 | const source = `${await readFileAsync(new URL(url))}`; | 31 | 42 | debug(`Translating StandardModule ${url}`); | | 43 | + const module = new ModuleWrap(stripShebang(source), url); | | 44 | + callbackMap.set(module, { | | 45 | + initializeImportMeta, | | 46 | + importModuleDynamically, | | 47 | + }); | 32 | 48 | return { | 33 | | - module: new ModuleWrap(stripShebang(source), url), | 34 | | - reflect: undefined | | 49 | + module, | | 50 | + reflect: undefined, | 35 | 51 | }; | 36 | 52 | }); | 37 | 53 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/process/esm_loader.js +22-27Lines changed: 22 additions & 27 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | const { | 4 | 4 | setImportModuleDynamicallyCallback, | 5 | | - setInitializeImportMetaObjectCallback | | 5 | + setInitializeImportMetaObjectCallback, | | 6 | + callbackMap, | 6 | 7 | } = internalBinding('module_wrap'); | 7 | 8 |
| 8 | 9 | const { pathToFileURL } = require('internal/url'); | 9 | 10 | const Loader = require('internal/modules/esm/loader'); | 10 | | -const path = require('path'); | 11 | | -const { URL } = require('url'); | 12 | 11 | const { | 13 | | - initImportMetaMap, | 14 | | - wrapToModuleMap | | 12 | + wrapToModuleMap, | 15 | 13 | } = require('internal/vm/source_text_module'); | | 14 | +const { | | 15 | + ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, | | 16 | +} = require('internal/errors').codes; | 16 | 17 |
| 17 | | -function normalizeReferrerURL(referrer) { | 18 | | - if (typeof referrer === 'string' && path.isAbsolute(referrer)) { | 19 | | - return pathToFileURL(referrer).href; | | 18 | +function initializeImportMetaObject(wrap, meta) { | | 19 | + if (callbackMap.has(wrap)) { | | 20 | + const { initializeImportMeta } = callbackMap.get(wrap); | | 21 | + if (initializeImportMeta !== undefined) { | | 22 | + initializeImportMeta(meta, wrapToModuleMap.get(wrap) || wrap); | | 23 | + } | 20 | 24 | } | 21 | | - return new URL(referrer).href; | 22 | 25 | } | 23 | 26 |
| 24 | | -function initializeImportMetaObject(wrap, meta) { | 25 | | - const vmModule = wrapToModuleMap.get(wrap); | 26 | | - if (vmModule === undefined) { | 27 | | - // This ModuleWrap belongs to the Loader. | 28 | | - meta.url = wrap.url; | 29 | | - } else { | 30 | | - const initializeImportMeta = initImportMetaMap.get(vmModule); | 31 | | - if (initializeImportMeta !== undefined) { | 32 | | - // This ModuleWrap belongs to vm.SourceTextModule, | 33 | | - // initializer callback was provided. | 34 | | - initializeImportMeta(meta, vmModule); | | 27 | +async function importModuleDynamicallyCallback(wrap, specifier) { | | 28 | + if (callbackMap.has(wrap)) { | | 29 | + const { importModuleDynamically } = callbackMap.get(wrap); | | 30 | + if (importModuleDynamically !== undefined) { | | 31 | + return importModuleDynamically( | | 32 | + specifier, wrapToModuleMap.get(wrap) || wrap); | 35 | 33 | } | 36 | 34 | } | | 35 | + throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); | 37 | 36 | } | 38 | 37 |
| | 38 | +setInitializeImportMetaObjectCallback(initializeImportMetaObject); | | 39 | +setImportModuleDynamicallyCallback(importModuleDynamicallyCallback); | | 40 | + | 39 | 41 | let loaderResolve; | 40 | 42 | exports.loaderPromise = new Promise((resolve, reject) => { | 41 | 43 | loaderResolve = resolve; | @@ -44,8 +46,6 @@ exports.loaderPromise = new Promise((resolve, reject) => { | 44 | 46 | exports.ESMLoader = undefined; | 45 | 47 |
| 46 | 48 | exports.setup = function() { | 47 | | - setInitializeImportMetaObjectCallback(initializeImportMetaObject); | 48 | | - | 49 | 49 | let ESMLoader = new Loader(); | 50 | 50 | const loaderPromise = (async () => { | 51 | 51 | const userLoader = require('internal/options').getOptionValue('--loader'); | @@ -60,10 +60,5 @@ exports.setup = function() { | 60 | 60 | })(); | 61 | 61 | loaderResolve(loaderPromise); | 62 | 62 |
| 63 | | - setImportModuleDynamicallyCallback(async (referrer, specifier) => { | 64 | | - const loader = await loaderPromise; | 65 | | - return loader.import(specifier, normalizeReferrerURL(referrer)); | 66 | | - }); | 67 | | - | 68 | 63 | exports.ESMLoader = ESMLoader; | 69 | 64 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/vm/source_text_module.js +34-13Lines changed: 34 additions & 13 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 |
| | 3 | +const { isModuleNamespaceObject } = require('util').types; | 3 | 4 | const { URL } = require('internal/url'); | 4 | 5 | const { isContext } = process.binding('contextify'); | 5 | 6 | const { | @@ -10,7 +11,7 @@ const { | 10 | 11 | ERR_VM_MODULE_LINKING_ERRORED, | 11 | 12 | ERR_VM_MODULE_NOT_LINKED, | 12 | 13 | ERR_VM_MODULE_NOT_MODULE, | 13 | | - ERR_VM_MODULE_STATUS | | 14 | + ERR_VM_MODULE_STATUS, | 14 | 15 | } = require('internal/errors').codes; | 15 | 16 | const { | 16 | 17 | getConstructorOf, | @@ -21,6 +22,7 @@ const { SafePromise } = require('internal/safe_globals'); | 21 | 22 |
| 22 | 23 | const { | 23 | 24 | ModuleWrap, | | 25 | + callbackMap, | 24 | 26 | kUninstantiated, | 25 | 27 | kInstantiating, | 26 | 28 | kInstantiated, | @@ -43,8 +45,6 @@ const perContextModuleId = new WeakMap(); | 43 | 45 | const wrapMap = new WeakMap(); | 44 | 46 | const dependencyCacheMap = new WeakMap(); | 45 | 47 | const linkingStatusMap = new WeakMap(); | 46 | | -// vm.SourceTextModule -> function | 47 | | -const initImportMetaMap = new WeakMap(); | 48 | 48 | // ModuleWrap -> vm.SourceTextModule | 49 | 49 | const wrapToModuleMap = new WeakMap(); | 50 | 50 | const defaultModuleName = 'vm:module'; | @@ -63,7 +63,8 @@ class SourceTextModule { | 63 | 63 | context, | 64 | 64 | lineOffset = 0, | 65 | 65 | columnOffset = 0, | 66 | | - initializeImportMeta | | 66 | + initializeImportMeta, | | 67 | + importModuleDynamically, | 67 | 68 | } = options; | 68 | 69 |
| 69 | 70 | if (context !== undefined) { | @@ -96,20 +97,39 @@ class SourceTextModule { | 96 | 97 | validateInteger(lineOffset, 'options.lineOffset'); | 97 | 98 | validateInteger(columnOffset, 'options.columnOffset'); | 98 | 99 |
| 99 | | - if (initializeImportMeta !== undefined) { | 100 | | - if (typeof initializeImportMeta === 'function') { | 101 | | - initImportMetaMap.set(this, initializeImportMeta); | 102 | | - } else { | 103 | | - throw new ERR_INVALID_ARG_TYPE( | 104 | | - 'options.initializeImportMeta', 'function', initializeImportMeta); | 105 | | - } | | 100 | + if (initializeImportMeta !== undefined && | | 101 | + typeof initializeImportMeta !== 'function') { | | 102 | + throw new ERR_INVALID_ARG_TYPE( | | 103 | + 'options.initializeImportMeta', 'function', initializeImportMeta); | | 104 | + } | | 105 | + | | 106 | + if (importModuleDynamically !== undefined && | | 107 | + typeof importModuleDynamically !== 'function') { | | 108 | + throw new ERR_INVALID_ARG_TYPE( | | 109 | + 'options.importModuleDynamically', 'function', importModuleDynamically); | 106 | 110 | } | 107 | 111 |
| 108 | 112 | const wrap = new ModuleWrap(src, url, context, lineOffset, columnOffset); | 109 | 113 | wrapMap.set(this, wrap); | 110 | 114 | linkingStatusMap.set(this, 'unlinked'); | 111 | 115 | wrapToModuleMap.set(wrap, this); | 112 | 116 |
| | 117 | + callbackMap.set(wrap, { | | 118 | + initializeImportMeta, | | 119 | + importModuleDynamically: importModuleDynamically ? async (...args) => { | | 120 | + const m = await importModuleDynamically(...args); | | 121 | + if (isModuleNamespaceObject(m)) { | | 122 | + return m; | | 123 | + } | | 124 | + if (!m || !wrapMap.has(m)) | | 125 | + throw new ERR_VM_MODULE_NOT_MODULE(); | | 126 | + const childLinkingStatus = linkingStatusMap.get(m); | | 127 | + if (childLinkingStatus === 'errored') | | 128 | + throw m.error; | | 129 | + return m.namespace; | | 130 | + } : undefined, | | 131 | + }); | | 132 | + | 113 | 133 | Object.defineProperties(this, { | 114 | 134 | url: { value: url, enumerable: true }, | 115 | 135 | context: { value: context, enumerable: true }, | @@ -255,6 +275,7 @@ function validateInteger(prop, propName) { | 255 | 275 |
| 256 | 276 | module.exports = { | 257 | 277 | SourceTextModule, | 258 | | - initImportMetaMap, | 259 | | - wrapToModuleMap | | 278 | + wrapToModuleMap, | | 279 | + wrapMap, | | 280 | + linkingStatusMap, | 260 | 281 | }; |
|