diff --git a/test/es-module/test-esm-dynamic-import.js b/test/es-module/test-esm-dynamic-import.js index 9a6cfad18e1..47995d50d9c 100644 --- a/test/es-module/test-esm-dynamic-import.js +++ b/test/es-module/test-esm-dynamic-import.js @@ -35,15 +35,15 @@ function expectFsNamespace(result) { } // For direct use of import expressions inside of CJS or ES modules, including -// via eval, all kinds of specifiers should work without issue. +// via direct/indirect eval, all kinds of specifiers should work without issue. (function testScriptOrModuleImport() { - // Importing another file, both direct & via eval + // Importing another file, both direct & via direct eval // expectOkNamespace(import(relativePath)); expectOkNamespace(eval(`import("${relativePath}")`)); expectOkNamespace(eval(`import("${relativePath}")`)); expectOkNamespace(eval(`import(${JSON.stringify(targetURL)})`)); - // Importing a built-in, both direct & via eval + // Importing a built-in, both direct & via direct eval expectFsNamespace(import('fs')); expectFsNamespace(eval('import("fs")')); expectFsNamespace(eval('import("fs")')); @@ -70,6 +70,8 @@ function expectFsNamespace(result) { // be treated as a file: URL. expectOkNamespace(import(targetURL.pathname)); + // Import with an indirect eval. In this case, the referrer is null and + // defaults to the realm record. // If the referrer is a realm record, there is no way to resolve the // specifier. // TODO(legendecas): https://github.com/tc39/ecma262/pull/3195 diff --git a/test/es-module/test-vm-main-context-default-loader-eval.js b/test/es-module/test-vm-main-context-default-loader-eval.js new file mode 100644 index 00000000000..3226c3512f4 --- /dev/null +++ b/test/es-module/test-vm-main-context-default-loader-eval.js @@ -0,0 +1,73 @@ +'use strict'; +/** + * This test verifies that dynamic import in an indirect eval without JS stacks. + * In this case, the referrer for the dynamic import will be null and the main + * context default loader in createContext() resolves to cwd. + * + * Caveat: this test can be unstable if the loader internals are changed and performs + * microtasks with a JS stack (e.g. with CallbackScope). In this case, the + * referrer will be resolved to the top JS stack frame `node:internal/process/task_queues.js`. + * This is due to the implementation detail of how V8 finds the referrer for a dynamic import + * call. + */ + +const common = require('../common'); + +// Can't process.chdir() in worker. +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); +const fs = require('node:fs'); +const { + Script, + createContext, + constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER }, +} = require('node:vm'); +const assert = require('node:assert'); + +common.expectWarning('ExperimentalWarning', + 'vm.USE_MAIN_CONTEXT_DEFAULT_LOADER is an experimental feature and might change at any time'); +assert( + !process.execArgv.includes('--experimental-vm-modules'), + 'This test must be run without --experimental-vm-modules'); +assert.strictEqual(typeof USE_MAIN_CONTEXT_DEFAULT_LOADER, 'symbol'); + +async function main() { + tmpdir.refresh(); + process.chdir(tmpdir.path); + + // + { + const options = { + importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER, + }; + const ctx = createContext({}, options); + const s = new Script('Promise.resolve("import(\'./message.mjs\')").then(eval)', { + importModuleDynamically: common.mustNotCall(), + }); + await assert.rejects(s.runInContext(ctx), { code: 'ERR_MODULE_NOT_FOUND' }); + } + + const moduleUrl = fixtures.fileURL('es-modules', 'message.mjs'); + fs.copyFileSync(moduleUrl, tmpdir.resolve('message.mjs')); + { + const options = { + importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER, + }; + const ctx = createContext({}, options); + const moduleUrl = fixtures.fileURL('es-modules', 'message.mjs'); + const namespace = await import(moduleUrl.href); + const script = new Script('Promise.resolve("import(\'./message.mjs\')").then(eval)', { + importModuleDynamically: common.mustNotCall(), + }); + const result = await script.runInContext(ctx); + assert.deepStrictEqual(result, namespace); + } +} + +main().catch(common.mustNotCall()); diff --git a/test/es-module/test-vm-main-context-default-loader.js b/test/es-module/test-vm-main-context-default-loader.js index bda954be6eb..9e45bbd9a12 100644 --- a/test/es-module/test-vm-main-context-default-loader.js +++ b/test/es-module/test-vm-main-context-default-loader.js @@ -16,7 +16,6 @@ const fs = require('fs'); const { compileFunction, Script, - createContext, constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER }, } = require('vm'); const assert = require('assert'); @@ -112,34 +111,8 @@ async function main() { await testNotFoundErrors(undefinedOptions); await testNotFoundErrors(nonPathOptions); - // createContext() with null referrer also resolves to cwd. - { - const options = { - importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER, - }; - const ctx = createContext({}, options); - const s = new Script('Promise.resolve("import(\'./message.mjs\')").then(eval)', { - importModuleDynamically: common.mustNotCall(), - }); - await assert.rejects(s.runInContext(ctx), { code: 'ERR_MODULE_NOT_FOUND' }); - } - await testLoader(undefinedOptions); await testLoader(nonPathOptions); - - { - const options = { - importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER, - }; - const ctx = createContext({}, options); - const moduleUrl = fixtures.fileURL('es-modules', 'message.mjs'); - const namespace = await import(moduleUrl.href); - const script = new Script('Promise.resolve("import(\'./message.mjs\')").then(eval)', { - importModuleDynamically: common.mustNotCall(), - }); - const result = await script.runInContext(ctx); - assert.deepStrictEqual(result, namespace); - } } } diff --git a/test/parallel/test-vm-module-referrer-realm.mjs b/test/parallel/test-vm-module-referrer-realm.mjs index 3957f147d8e..0b60f7b8531 100644 --- a/test/parallel/test-vm-module-referrer-realm.mjs +++ b/test/parallel/test-vm-module-referrer-realm.mjs @@ -3,6 +3,18 @@ import * as common from '../common/index.mjs'; import assert from 'node:assert'; import { Script, SourceTextModule, createContext } from 'node:vm'; +/** + * This test verifies that dynamic import in an indirect eval without JS stacks. + * In this case, the referrer for the dynamic import will be null and the + * per-context importModuleDynamically callback will be invoked. + * + * Caveat: this test can be unstable if the loader internals are changed and performs + * microtasks with a JS stack (e.g. with CallbackScope). In this case, the + * referrer will be resolved to the top JS stack frame `node:internal/process/task_queues.js`. + * This is due to the implementation detail of how V8 finds the referrer for a dynamic import + * call. + */ + async function test() { const foo = new SourceTextModule('export const a = 1;'); await foo.link(common.mustNotCall());