node/test/es-module/test-vm-main-context-default-loader.js
Joyee Cheung ad0bcb9c02
vm: support using the default loader to handle dynamic import()
This patch adds support for using
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER` as
`importModuleDynamically` in all APIs that take the option
except `vm.SourceTextModule`. This allows users to have a shortcut
to support dynamic import() in the compiled code without missing
the compilation cache if they don't need customization of the
loading process. We emit an experimental warning when the
`import()` is actually handled by the default loader through
this option instead of requiring `--experimental-vm-modules`.

In addition this refactors the documentation for
`importModuleDynamically` and adds a dedicated section for it
with examples.

`vm.SourceTextModule` is not supported in this patch because
it needs additional refactoring to handle `initializeImportMeta`,
which can be done in a follow-up.

PR-URL: https://github.com/nodejs/node/pull/51244
Fixes: https://github.com/nodejs/node/issues/51154
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2024-02-01 11:45:42 +00:00

143 lines
4.5 KiB
JavaScript

'use strict';
const common = require('../common');
// Can't process.chdir() in worker.
common.skipIfWorker();
const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
const url = require('url');
const fs = require('fs');
const {
compileFunction,
Script,
createContext,
constants: { USE_MAIN_CONTEXT_DEFAULT_LOADER },
} = require('vm');
const assert = require('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 testNotFoundErrors(options) {
// Import user modules.
const script = new Script('import("./message.mjs")', options);
// Use try-catch for better async stack traces in the logs.
await assert.rejects(script.runInThisContext(), { code: 'ERR_MODULE_NOT_FOUND' });
const imported = compileFunction('return import("./message.mjs")', [], options)();
// Use try-catch for better async stack traces in the logs.
await assert.rejects(imported, { code: 'ERR_MODULE_NOT_FOUND' });
}
async function testLoader(options) {
{
// Import built-in modules
const script = new Script('import("fs")', options);
let result = await script.runInThisContext();
assert.strictEqual(result.constants.F_OK, fs.constants.F_OK);
const imported = compileFunction('return import("fs")', [], options)();
result = await imported;
assert.strictEqual(result.constants.F_OK, fs.constants.F_OK);
}
const moduleUrl = fixtures.fileURL('es-modules', 'message.mjs');
fs.copyFileSync(moduleUrl, tmpdir.resolve('message.mjs'));
{
const namespace = await import(moduleUrl);
const script = new Script('import("./message.mjs")', options);
const result = await script.runInThisContext();
assert.deepStrictEqual(result, namespace);
}
{
const namespace = await import(moduleUrl);
const imported = compileFunction('return import("./message.mjs")', [], options)();
const result = await imported;
assert.deepStrictEqual(result, namespace);
}
}
async function main() {
{
// Importing with absolute path as filename.
tmpdir.refresh();
const filename = tmpdir.resolve('index.js');
const options = {
filename,
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER
};
await testNotFoundErrors(options);
await testLoader(options);
}
{
// Importing with file:// URL as filename.
tmpdir.refresh();
// We use a search parameter to bypass caching.
const filename = url.pathToFileURL(tmpdir.resolve('index.js')).href + '?t=1';
const options = {
filename,
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER
};
await testNotFoundErrors(options);
await testLoader(options);
}
{
// For undefined or non-path/URL filenames, import() should resolve to the cwd.
tmpdir.refresh();
process.chdir(tmpdir.path);
const undefinedOptions = {
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER
};
const nonPathOptions = {
filename: 'non-path',
importModuleDynamically: USE_MAIN_CONTEXT_DEFAULT_LOADER
};
// Run the error tests first to avoid caching.
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);
}
}
}
main().catch(common.mustNotCall());