diff --git a/doc/api/vm.md b/doc/api/vm.md index c729ef59991cc8..ed7be817768603 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -167,9 +167,49 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); in stack traces produced by this Module. * `columnOffset` {integer} Spcifies the column number offset that is displayed in stack traces produced by this Module. + * `initalizeImportMeta` {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.Module` object. Creates a new ES `Module` object. +*Note*: Properties assigned to the `import.meta` object that are objects may +allow the Module to access information outside the specified `context`, if the +object is created in the top level context. Use `vm.runInContext()` to create +objects in a specific context. + +```js +const vm = require('vm'); + +const contextifiedSandbox = vm.createContext({ secret: 42 }); + +(async () => { + const module = new vm.Module( + 'Object.getPrototypeOf(import.meta.prop).secret = secret;', + { + initializeImportMeta(meta) { + // Note: this object is created in the top context. As such, + // Object.getPrototypeOf(import.meta.prop) points to the + // Object.prototype in the top context rather than that in + // the sandbox. + meta.prop = {}; + } + }); + // Since module has no dependencies, the linker function will never be called. + await module.link(() => {}); + module.initialize(); + await module.evaluate(); + + // Now, Object.prototype.secret will be equal to 42. + // + // To fix this problem, replace + // meta.prop = {}; + // above with + // meta.prop = vm.runInContext('{}', contextifiedSandbox); +})(); +``` + ### module.dependencySpecifiers * {string[]} diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 01efe6f32900d4..99bf00a6d6e4ac 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -108,10 +108,13 @@ 'DeprecationWarning', 'DEP0062', startup, true); } - if (process.binding('config').experimentalModules) { - process.emitWarning( - 'The ESM module loader is experimental.', - 'ExperimentalWarning', undefined); + if (process.binding('config').experimentalModules || + process.binding('config').experimentalVMModules) { + if (process.binding('config').experimentalModules) { + process.emitWarning( + 'The ESM module loader is experimental.', + 'ExperimentalWarning', undefined); + } NativeModule.require('internal/process/esm_loader').setup(); } diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index bcb6501af69eb6..db28ca04b16cdc 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -10,6 +10,10 @@ const { getURLFromFilePath } = require('internal/url'); const Loader = require('internal/modules/esm/loader'); const path = require('path'); const { URL } = require('url'); +const { + initImportMetaMap, + wrapToModuleMap +} = require('internal/vm/module'); function normalizeReferrerURL(referrer) { if (typeof referrer === 'string' && path.isAbsolute(referrer)) { @@ -19,7 +23,18 @@ function normalizeReferrerURL(referrer) { } function initializeImportMetaObject(wrap, meta) { - meta.url = wrap.url; + 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.Module, initializer callback was + // provided. + initializeImportMeta(meta, vmModule); + } + } } let loaderResolve; diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js index 48d591f3bf8ad1..9af071ce28ceca 100644 --- a/lib/internal/vm/module.js +++ b/lib/internal/vm/module.js @@ -43,6 +43,10 @@ const perContextModuleId = new WeakMap(); const wrapMap = new WeakMap(); const dependencyCacheMap = new WeakMap(); const linkingStatusMap = new WeakMap(); +// vm.Module -> function +const initImportMetaMap = new WeakMap(); +// ModuleWrap -> vm.Module +const wrapToModuleMap = new WeakMap(); class Module { constructor(src, options = {}) { @@ -80,6 +84,16 @@ class Module { perContextModuleId.set(context, 1); } + if (options.initializeImportMeta !== undefined) { + if (typeof options.initializeImportMeta === 'function') { + initImportMetaMap.set(this, options.initializeImportMeta); + } else { + throw new ERR_INVALID_ARG_TYPE( + 'options.initializeImportMeta', 'function', + options.initializeImportMeta); + } + } + const wrap = new ModuleWrap(src, url, { [kParsingContext]: context, lineOffset: options.lineOffset, @@ -88,6 +102,7 @@ class Module { wrapMap.set(this, wrap); linkingStatusMap.set(this, 'unlinked'); + wrapToModuleMap.set(wrap, this); Object.defineProperties(this, { url: { value: url, enumerable: true }, @@ -206,5 +221,7 @@ class Module { } module.exports = { - Module + Module, + initImportMetaMap, + wrapToModuleMap }; diff --git a/test/parallel/test-vm-module-import-meta.js b/test/parallel/test-vm-module-import-meta.js new file mode 100644 index 00000000000000..835ef5b6eb5de0 --- /dev/null +++ b/test/parallel/test-vm-module-import-meta.js @@ -0,0 +1,45 @@ +'use strict'; + +// Flags: --experimental-vm-modules --harmony-import-meta + +const common = require('../common'); +const assert = require('assert'); +const { Module } = require('vm'); + +common.crashOnUnhandledRejection(); + +async function testBasic() { + const m = new Module('import.meta;', { + initializeImportMeta: common.mustCall((meta, module) => { + assert.strictEqual(module, m); + meta.prop = 42; + }) + }); + await m.link(common.mustNotCall()); + m.instantiate(); + const { result } = await m.evaluate(); + assert.strictEqual(typeof result, 'object'); + assert.strictEqual(Object.getPrototypeOf(result), null); + assert.strictEqual(result.prop, 42); + assert.deepStrictEqual(Reflect.ownKeys(result), ['prop']); +} + +async function testInvalid() { + for (const invalidValue of [ + null, {}, 0, Symbol.iterator, [], 'string', false + ]) { + common.expectsError(() => { + new Module('', { + initializeImportMeta: invalidValue + }); + }, { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + }); + } +} + +(async () => { + await testBasic(); + await testInvalid(); +})();