Skip to content
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

[10.x] vm: add dynamic import support #25421

Closed
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
5 changes: 5 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,11 @@ The V8 `BreakIterator` API was used but the full ICU data set is not installed.
While using the Performance Timing API (`perf_hooks`), no valid performance
entry types were found.

<a id="ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING"></a>
### ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING

A dynamic import callback was not specified.

<a id="ERR_VM_MODULE_ALREADY_LINKED"></a>
### ERR_VM_MODULE_ALREADY_LINKED

Expand Down
22 changes: 21 additions & 1 deletion doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,19 @@ const contextifiedSandbox = vm.createContext({ secret: 42 });
in stack traces produced by this `Module`.
* `columnOffset` {integer} Specifies the column number offset that is
displayed in stack traces produced by this `Module`.
* `initalizeImportMeta` {Function} Called during evaluation of this `Module`
* `initializeImportMeta` {Function} Called during evaluation of this `Module`
to initialize the `import.meta`. This function has the signature `(meta,
module)`, where `meta` is the `import.meta` object in the `Module`, and
`module` is this `vm.SourceTextModule` object.
* `importModuleDynamically` {Function} Called during evaluation of this
module when `import()` is called. This function has the signature
`(specifier, module)` where `specifier` is the specifier passed to
`import()` and `module` is this `vm.SourceTextModule`. If this option is
not specified, calls to `import()` will reject with
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a
[Module Namespace Object][], but returning a `vm.SourceTextModule` is
recommended in order to take advantage of error tracking, and to avoid
issues with namespaces that contain `then` function exports.

Creates a new ES `Module` object.

Expand Down Expand Up @@ -436,6 +445,15 @@ changes:
The `cachedDataProduced` value will be set to either `true` or `false`
depending on whether code cache data is produced successfully.
This option is deprecated in favor of `script.createCachedData()`.
* `importModuleDynamically` {Function} Called during evaluation of this
module when `import()` is called. This function has the signature
`(specifier, module)` where `specifier` is the specifier passed to
`import()` and `module` is this `vm.SourceTextModule`. If this option is
not specified, calls to `import()` will reject with
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. This method can return a
[Module Namespace Object][], but returning a `vm.SourceTextModule` is
recommended in order to take advantage of error tracking, and to avoid
issues with namespaces that contain `then` function exports.

Creating a new `vm.Script` object compiles `code` but does not run it. The
compiled `vm.Script` can be run later multiple times. The `code` is not bound to
Expand Down Expand Up @@ -977,6 +995,7 @@ This issue occurs because all contexts share the same microtask and nextTick
queues.

[`Error`]: errors.html#errors_class_error
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.html#ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING
[`URL`]: url.html#url_class_url
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
[`script.runInContext()`]: #vm_script_runincontext_contextifiedsandbox_options
Expand All @@ -985,6 +1004,7 @@ queues.
[`vm.createContext()`]: #vm_vm_createcontext_sandbox_options
[`vm.runInContext()`]: #vm_vm_runincontext_code_contextifiedsandbox_options
[`vm.runInThisContext()`]: #vm_vm_runinthiscontext_code_options
[Module Namespace Object]: https://tc39.github.io/ecma262/#sec-module-namespace-exotic-objects
[ECMAScript Module Loader]: esm.html#esm_ecmascript_modules
[Evaluate() concrete method]: https://tc39.github.io/ecma262/#sec-moduleevaluation
[GetModuleNamespace]: https://tc39.github.io/ecma262/#sec-getmodulenamespace
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/bootstrap/loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
};
}

// Create this WeakMap in js-land because V8 has no C++ API for WeakMap
internalBinding('module_wrap').callbackMap = new WeakMap();
const { ContextifyScript } = process.binding('contextify');

// Set up NativeModule
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,8 @@ E('ERR_V8BREAKITERATOR',
// This should probably be a `TypeError`.
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE',
'At least one valid performance entry type is required', Error);
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING',
'A dynamic import callback was not specified.', TypeError);
E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error);
E('ERR_VM_MODULE_DIFFERENT_CONTEXT',
'Linked modules must use the same context', Error);
Expand Down
15 changes: 14 additions & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const assert = require('assert').ok;
const fs = require('fs');
const internalFS = require('internal/fs/utils');
const path = require('path');
const { URL } = require('url');
const {
internalModuleReadJSON,
internalModuleStat
Expand Down Expand Up @@ -642,6 +643,13 @@ Module.prototype.require = function(id) {
// (needed for setting breakpoint when called with --inspect-brk)
var resolvedArgv;

function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return pathToFileURL(referrer).href;
}
return new URL(referrer).href;
}


// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
Expand All @@ -657,7 +665,12 @@ Module.prototype._compile = function(content, filename) {
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
displayErrors: true,
importModuleDynamically: experimentalModules ? async (specifier) => {
if (asyncESM === undefined) lazyLoadESM();
const loader = await asyncESM.loaderPromise;
return loader.import(specifier, normalizeReferrerURL(filename));
} : undefined,
});

var inspectorWrapper = null;
Expand Down
22 changes: 19 additions & 3 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const { NativeModule } = require('internal/bootstrap/loaders');
const { ModuleWrap } = internalBinding('module_wrap');
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const {
stripShebang,
stripBOM
Expand All @@ -15,6 +15,8 @@ const { _makeLong } = require('path');
const { SafeMap } = require('internal/safe_globals');
const { URL } = require('url');
const { debuglog, promisify } = require('util');
const esmLoader = require('internal/process/esm_loader');

const readFileAsync = promisify(fs.readFile);
const readFileSync = fs.readFileSync;
const StringReplace = Function.call.bind(String.prototype.replace);
Expand All @@ -25,13 +27,27 @@ const debug = debuglog('esm');
const translators = new SafeMap();
module.exports = translators;

function initializeImportMeta(meta, { url }) {
meta.url = url;
}

async function importModuleDynamically(specifier, { url }) {
const loader = await esmLoader.loaderPromise;
return loader.import(specifier, url);
}

// Strategy for loading a standard JavaScript module
translators.set('esm', async (url) => {
const source = `${await readFileAsync(new URL(url))}`;
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(stripShebang(source), url);
callbackMap.set(module, {
initializeImportMeta,
importModuleDynamically,
});
return {
module: new ModuleWrap(stripShebang(source), url),
reflect: undefined
module,
reflect: undefined,
};
});

Expand Down
49 changes: 22 additions & 27 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,42 @@

const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback
setInitializeImportMetaObjectCallback,
callbackMap,
} = internalBinding('module_wrap');

const { pathToFileURL } = require('internal/url');
const Loader = require('internal/modules/esm/loader');
const path = require('path');
const { URL } = require('url');
const {
initImportMetaMap,
wrapToModuleMap
wrapToModuleMap,
} = require('internal/vm/source_text_module');
const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
} = require('internal/errors').codes;

function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return pathToFileURL(referrer).href;
function initializeImportMetaObject(wrap, meta) {
if (callbackMap.has(wrap)) {
const { initializeImportMeta } = callbackMap.get(wrap);
if (initializeImportMeta !== undefined) {
initializeImportMeta(meta, wrapToModuleMap.get(wrap) || wrap);
}
}
return new URL(referrer).href;
}

function initializeImportMetaObject(wrap, meta) {
const vmModule = wrapToModuleMap.get(wrap);
if (vmModule === undefined) {
// This ModuleWrap belongs to the Loader.
meta.url = wrap.url;
} else {
const initializeImportMeta = initImportMetaMap.get(vmModule);
if (initializeImportMeta !== undefined) {
// This ModuleWrap belongs to vm.SourceTextModule,
// initializer callback was provided.
initializeImportMeta(meta, vmModule);
async function importModuleDynamicallyCallback(wrap, specifier) {
if (callbackMap.has(wrap)) {
const { importModuleDynamically } = callbackMap.get(wrap);
if (importModuleDynamically !== undefined) {
return importModuleDynamically(
specifier, wrapToModuleMap.get(wrap) || wrap);
}
}
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
}

setInitializeImportMetaObjectCallback(initializeImportMetaObject);
setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);

let loaderResolve;
exports.loaderPromise = new Promise((resolve, reject) => {
loaderResolve = resolve;
Expand All @@ -44,8 +46,6 @@ exports.loaderPromise = new Promise((resolve, reject) => {
exports.ESMLoader = undefined;

exports.setup = function() {
setInitializeImportMetaObjectCallback(initializeImportMetaObject);

let ESMLoader = new Loader();
const loaderPromise = (async () => {
const userLoader = require('internal/options').getOptionValue('--loader');
Expand All @@ -60,10 +60,5 @@ exports.setup = function() {
})();
loaderResolve(loaderPromise);

setImportModuleDynamicallyCallback(async (referrer, specifier) => {
const loader = await loaderPromise;
return loader.import(specifier, normalizeReferrerURL(referrer));
});

exports.ESMLoader = ESMLoader;
};
47 changes: 34 additions & 13 deletions lib/internal/vm/source_text_module.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const { isModuleNamespaceObject } = require('util').types;
const { URL } = require('internal/url');
const { isContext } = process.binding('contextify');
const {
Expand All @@ -10,7 +11,7 @@ const {
ERR_VM_MODULE_LINKING_ERRORED,
ERR_VM_MODULE_NOT_LINKED,
ERR_VM_MODULE_NOT_MODULE,
ERR_VM_MODULE_STATUS
ERR_VM_MODULE_STATUS,
} = require('internal/errors').codes;
const {
getConstructorOf,
Expand All @@ -21,6 +22,7 @@ const { SafePromise } = require('internal/safe_globals');

const {
ModuleWrap,
callbackMap,
kUninstantiated,
kInstantiating,
kInstantiated,
Expand All @@ -43,8 +45,6 @@ const perContextModuleId = new WeakMap();
const wrapMap = new WeakMap();
const dependencyCacheMap = new WeakMap();
const linkingStatusMap = new WeakMap();
// vm.SourceTextModule -> function
const initImportMetaMap = new WeakMap();
// ModuleWrap -> vm.SourceTextModule
const wrapToModuleMap = new WeakMap();
const defaultModuleName = 'vm:module';
Expand All @@ -63,7 +63,8 @@ class SourceTextModule {
context,
lineOffset = 0,
columnOffset = 0,
initializeImportMeta
initializeImportMeta,
importModuleDynamically,
} = options;

if (context !== undefined) {
Expand Down Expand Up @@ -96,20 +97,39 @@ class SourceTextModule {
validateInteger(lineOffset, 'options.lineOffset');
validateInteger(columnOffset, 'options.columnOffset');

if (initializeImportMeta !== undefined) {
if (typeof initializeImportMeta === 'function') {
initImportMetaMap.set(this, initializeImportMeta);
} else {
throw new ERR_INVALID_ARG_TYPE(
'options.initializeImportMeta', 'function', initializeImportMeta);
}
if (initializeImportMeta !== undefined &&
typeof initializeImportMeta !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.initializeImportMeta', 'function', initializeImportMeta);
}

if (importModuleDynamically !== undefined &&
typeof importModuleDynamically !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'options.importModuleDynamically', 'function', importModuleDynamically);
}

const wrap = new ModuleWrap(src, url, context, lineOffset, columnOffset);
wrapMap.set(this, wrap);
linkingStatusMap.set(this, 'unlinked');
wrapToModuleMap.set(wrap, this);

callbackMap.set(wrap, {
initializeImportMeta,
importModuleDynamically: importModuleDynamically ? async (...args) => {
const m = await importModuleDynamically(...args);
if (isModuleNamespaceObject(m)) {
return m;
}
if (!m || !wrapMap.has(m))
throw new ERR_VM_MODULE_NOT_MODULE();
const childLinkingStatus = linkingStatusMap.get(m);
if (childLinkingStatus === 'errored')
throw m.error;
return m.namespace;
} : undefined,
});

Object.defineProperties(this, {
url: { value: url, enumerable: true },
context: { value: context, enumerable: true },
Expand Down Expand Up @@ -255,6 +275,7 @@ function validateInteger(prop, propName) {

module.exports = {
SourceTextModule,
initImportMetaMap,
wrapToModuleMap
wrapToModuleMap,
wrapMap,
linkingStatusMap,
};
Loading