mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
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>
This commit is contained in:
parent
dd4767f99f
commit
ad0bcb9c02
412
doc/api/vm.md
412
doc/api/vm.md
@ -58,6 +58,11 @@ executed in specific contexts.
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51244
|
||||
description: Added support for
|
||||
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- version:
|
||||
- v17.0.0
|
||||
- v16.12.0
|
||||
@ -94,21 +99,13 @@ changes:
|
||||
depending on whether code cache data is produced successfully.
|
||||
This option is **deprecated** in favor of `script.createCachedData()`.
|
||||
**Default:** `false`.
|
||||
* `importModuleDynamically` {Function} Called during evaluation of this module
|
||||
when `import()` is called. If this option is not specified, calls to
|
||||
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
This option is part of the experimental modules API. We do not recommend
|
||||
using it in a production environment. If `--experimental-vm-modules` isn't
|
||||
set, this callback will be ignored and calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `script` {vm.Script}
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `importModuleDynamically`
|
||||
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
|
||||
Used to specify how the modules should be loaded during the evaluation
|
||||
of this script when `import()` is called. This option is part of the
|
||||
experimental modules API. We do not recommend using it in a production
|
||||
environment. For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
|
||||
If `options` is a string, then it specifies the filename.
|
||||
|
||||
@ -769,20 +766,12 @@ changes:
|
||||
to initialize the `import.meta`.
|
||||
* `meta` {import.meta}
|
||||
* `module` {vm.SourceTextModule}
|
||||
* `importModuleDynamically` {Function} Called during evaluation of this module
|
||||
when `import()` is called. If this option is not specified, calls to
|
||||
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
If `--experimental-vm-modules` isn't set, this callback will be ignored
|
||||
and calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `module` {vm.Module}
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `importModuleDynamically` {Function} Used to specify the
|
||||
how the modules should be loaded during the evaluation of this module
|
||||
when `import()` is called. This option is part of the experimental
|
||||
modules API. We do not recommend using it in a production environment.
|
||||
For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
|
||||
Creates a new `SourceTextModule` instance.
|
||||
|
||||
@ -981,6 +970,11 @@ const vm = require('node:vm');
|
||||
<!-- YAML
|
||||
added: v10.10.0
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51244
|
||||
description: Added support for
|
||||
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- version:
|
||||
- v19.6.0
|
||||
- v18.15.0
|
||||
@ -1029,32 +1023,54 @@ changes:
|
||||
* `contextExtensions` {Object\[]} An array containing a collection of context
|
||||
extensions (objects wrapping the current scope) to be applied while
|
||||
compiling. **Default:** `[]`.
|
||||
* `importModuleDynamically` {Function} Called during evaluation of this module
|
||||
when `import()` is called. If this option is not specified, calls to
|
||||
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
This option is part of the experimental modules API, and should not be
|
||||
considered stable. If `--experimental-vm-modules` isn't
|
||||
set, this callback will be ignored and calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `function` {Function}
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `importModuleDynamically`
|
||||
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
|
||||
Used to specify the how the modules should be loaded during the evaluation of
|
||||
this function when `import()` is called. This option is part of the
|
||||
experimental modules API. We do not recommend using it in a production
|
||||
environment. For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
* Returns: {Function}
|
||||
|
||||
Compiles the given code into the provided context (if no context is
|
||||
supplied, the current context is used), and returns it wrapped inside a
|
||||
function with the given `params`.
|
||||
|
||||
## `vm.constants`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* {Object}
|
||||
|
||||
Returns an object containing commonly used constants for VM operations.
|
||||
|
||||
### `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.1 - Active development
|
||||
|
||||
A constant that can be used as the `importModuleDynamically` option to
|
||||
`vm.Script` and `vm.compileFunction()` so that Node.js uses the default
|
||||
ESM loader from the main context to load the requested module.
|
||||
|
||||
For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
|
||||
## `vm.createContext([contextObject[, options]])`
|
||||
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51244
|
||||
description: Added support for
|
||||
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- version:
|
||||
- v21.2.0
|
||||
- v20.11.0
|
||||
@ -1092,21 +1108,13 @@ changes:
|
||||
scheduled through `Promise`s and `async function`s) will be run immediately
|
||||
after a script has run through [`script.runInContext()`][].
|
||||
They are included in the `timeout` and `breakOnSigint` scopes in that case.
|
||||
* `importModuleDynamically` {Function} Called when `import()` is called in
|
||||
this context without a referrer script or module. If this option is not
|
||||
specified, calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][]. If
|
||||
`--experimental-vm-modules` isn't set, this callback will be ignored and
|
||||
calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `contextObject` {Object} contextified object
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `importModuleDynamically`
|
||||
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
|
||||
Used to specify the how the modules should be loaded when `import()` is
|
||||
called in this context without a referrer script or module. This option is
|
||||
part of the experimental modules API. We do not recommend using it in a
|
||||
production environment. For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
* Returns: {Object} contextified object.
|
||||
|
||||
If given a `contextObject`, the `vm.createContext()` method will [prepare
|
||||
@ -1240,6 +1248,11 @@ vm.measureMemory({ mode: 'detailed', execution: 'eager' })
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51244
|
||||
description: Added support for
|
||||
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- version:
|
||||
- v17.0.0
|
||||
- v16.12.0
|
||||
@ -1275,22 +1288,13 @@ changes:
|
||||
* `cachedData` {Buffer|TypedArray|DataView} Provides an optional `Buffer` or
|
||||
`TypedArray`, or `DataView` with V8's code cache data for the supplied
|
||||
source.
|
||||
* `importModuleDynamically` {Function} Called during evaluation of this module
|
||||
when `import()` is called. If this option is not specified, calls to
|
||||
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
This option is part of the experimental modules API. We do not recommend
|
||||
using it in a production environment. If `--experimental-vm-modules` isn't
|
||||
set, this callback will be ignored and calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `script` {vm.Script}
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* Returns: {any} the result of the very last statement executed in the script.
|
||||
* `importModuleDynamically`
|
||||
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
|
||||
Used to specify the how the modules should be loaded during the evaluation
|
||||
of this script when `import()` is called. This option is part of the
|
||||
experimental modules API. We do not recommend using it in a production
|
||||
environment. For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
|
||||
The `vm.runInContext()` method compiles `code`, runs it within the context of
|
||||
the `contextifiedObject`, then returns the result. Running code does not have
|
||||
@ -1320,6 +1324,11 @@ console.log(contextObject);
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51244
|
||||
description: Added support for
|
||||
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- version:
|
||||
- v17.0.0
|
||||
- v16.12.0
|
||||
@ -1376,21 +1385,13 @@ changes:
|
||||
* `cachedData` {Buffer|TypedArray|DataView} Provides an optional `Buffer` or
|
||||
`TypedArray`, or `DataView` with V8's code cache data for the supplied
|
||||
source.
|
||||
* `importModuleDynamically` {Function} Called during evaluation of this module
|
||||
when `import()` is called. If this option is not specified, calls to
|
||||
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
This option is part of the experimental modules API. We do not recommend
|
||||
using it in a production environment. If `--experimental-vm-modules` isn't
|
||||
set, this callback will be ignored and calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `script` {vm.Script}
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `importModuleDynamically`
|
||||
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
|
||||
Used to specify the how the modules should be loaded during the evaluation
|
||||
of this script when `import()` is called. This option is part of the
|
||||
experimental modules API. We do not recommend using it in a production
|
||||
environment. For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
* `microtaskMode` {string} If set to `afterEvaluate`, microtasks (tasks
|
||||
scheduled through `Promise`s and `async function`s) will be run immediately
|
||||
after the script has run. They are included in the `timeout` and
|
||||
@ -1425,6 +1426,11 @@ console.log(contextObject);
|
||||
<!-- YAML
|
||||
added: v0.3.1
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51244
|
||||
description: Added support for
|
||||
`vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`.
|
||||
- version:
|
||||
- v17.0.0
|
||||
- v16.12.0
|
||||
@ -1458,21 +1464,13 @@ changes:
|
||||
* `cachedData` {Buffer|TypedArray|DataView} Provides an optional `Buffer` or
|
||||
`TypedArray`, or `DataView` with V8's code cache data for the supplied
|
||||
source.
|
||||
* `importModuleDynamically` {Function} Called during evaluation of this module
|
||||
when `import()` is called. If this option is not specified, calls to
|
||||
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
This option is part of the experimental modules API. We do not recommend
|
||||
using it in a production environment. If `--experimental-vm-modules` isn't
|
||||
set, this callback will be ignored and calls to `import()` will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `script` {vm.Script}
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value
|
||||
was provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid
|
||||
issues with namespaces that contain `then` function exports.
|
||||
* `importModuleDynamically`
|
||||
{Function|vm.constants.USE\_MAIN\_CONTEXT\_DEFAULT\_LOADER}
|
||||
Used to specify the how the modules should be loaded during the evaluation
|
||||
of this script when `import()` is called. This option is part of the
|
||||
experimental modules API. We do not recommend using it in a production
|
||||
environment. For detailed information, see
|
||||
[Support of dynamic `import()` in compilation APIs][].
|
||||
* Returns: {any} the result of the very last statement executed in the script.
|
||||
|
||||
`vm.runInThisContext()` compiles `code`, runs it within the context of the
|
||||
@ -1618,6 +1616,209 @@ inside a `vm.Context`, functions passed to them will be added to global queues,
|
||||
which are shared by all contexts. Therefore, callbacks passed to those functions
|
||||
are not controllable through the timeout either.
|
||||
|
||||
## Support of dynamic `import()` in compilation APIs
|
||||
|
||||
The following APIs support an `importModuleDynamically` option to enable dynamic
|
||||
`import()` in code compiled by the vm module.
|
||||
|
||||
* `new vm.Script`
|
||||
* `vm.compileFunction()`
|
||||
* `new vm.SourceTextModule`
|
||||
* `vm.runInThisContext()`
|
||||
* `vm.runInContext()`
|
||||
* `vm.runInNewContext()`
|
||||
* `vm.createContext()`
|
||||
|
||||
This option is still part of the experimental modules API. We do not recommend
|
||||
using it in a production environment.
|
||||
|
||||
### When the `importModuleDynamically` option is not specified or undefined
|
||||
|
||||
If this option is not specified, or if it's `undefined`, code containing
|
||||
`import()` can still be compiled by the vm APIs, but when the compiled code is
|
||||
executed and it actually calls `import()`, the result will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
|
||||
|
||||
### When `importModuleDynamically` is `vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER`
|
||||
|
||||
This option is currently not supported for `vm.SourceTextModule`.
|
||||
|
||||
With this option, when an `import()` is initiated in the compiled code, Node.js
|
||||
would use the default ESM loader from the main context to load the requested
|
||||
module and return it to the code being executed.
|
||||
|
||||
This gives access to Node.js built-in modules such as `fs` or `http`
|
||||
to the code being compiled. If the code is executed in a different context,
|
||||
be aware that the objects created by modules loaded from the main context
|
||||
are still from the main context and not `instanceof` built-in classes in the
|
||||
new context.
|
||||
|
||||
```cjs
|
||||
const { Script, constants } = require('node:vm');
|
||||
const script = new Script(
|
||||
'import("node:fs").then(({readFile}) => readFile instanceof Function)',
|
||||
{ importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER });
|
||||
|
||||
// false: URL loaded from the main context is not an instance of the Function
|
||||
// class in the new context.
|
||||
script.runInNewContext().then(console.log);
|
||||
```
|
||||
|
||||
```mjs
|
||||
import { Script, constants } from 'node:vm';
|
||||
|
||||
const script = new Script(
|
||||
'import("node:fs").then(({readFile}) => readFile instanceof Function)',
|
||||
{ importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER });
|
||||
|
||||
// false: URL loaded from the main context is not an instance of the Function
|
||||
// class in the new context.
|
||||
script.runInNewContext().then(console.log);
|
||||
```
|
||||
|
||||
This option also allows the script or function to load user modules:
|
||||
|
||||
```mjs
|
||||
import { Script, constants } from 'node:vm';
|
||||
import { resolve } from 'node:path';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
// Write test.js and test.txt to the directory where the current script
|
||||
// being run is located.
|
||||
writeFileSync(resolve(import.meta.dirname, 'test.mjs'),
|
||||
'export const filename = "./test.json";');
|
||||
writeFileSync(resolve(import.meta.dirname, 'test.json'),
|
||||
'{"hello": "world"}');
|
||||
|
||||
// Compile a script that loads test.mjs and then test.json
|
||||
// as if the script is placed in the same directory.
|
||||
const script = new Script(
|
||||
`(async function() {
|
||||
const { filename } = await import('./test.mjs');
|
||||
return import(filename, { with: { type: 'json' } })
|
||||
})();`,
|
||||
{
|
||||
filename: resolve(import.meta.dirname, 'test-with-default.js'),
|
||||
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||
});
|
||||
|
||||
// { default: { hello: 'world' } }
|
||||
script.runInThisContext().then(console.log);
|
||||
```
|
||||
|
||||
```cjs
|
||||
const { Script, constants } = require('node:vm');
|
||||
const { resolve } = require('node:path');
|
||||
const { writeFileSync } = require('node:fs');
|
||||
|
||||
// Write test.js and test.txt to the directory where the current script
|
||||
// being run is located.
|
||||
writeFileSync(resolve(__dirname, 'test.mjs'),
|
||||
'export const filename = "./test.json";');
|
||||
writeFileSync(resolve(__dirname, 'test.json'),
|
||||
'{"hello": "world"}');
|
||||
|
||||
// Compile a script that loads test.mjs and then test.json
|
||||
// as if the script is placed in the same directory.
|
||||
const script = new Script(
|
||||
`(async function() {
|
||||
const { filename } = await import('./test.mjs');
|
||||
return import(filename, { with: { type: 'json' } })
|
||||
})();`,
|
||||
{
|
||||
filename: resolve(__dirname, 'test-with-default.js'),
|
||||
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||
});
|
||||
|
||||
// { default: { hello: 'world' } }
|
||||
script.runInThisContext().then(console.log);
|
||||
```
|
||||
|
||||
There are a few caveats with loading user modules using the default loader
|
||||
from the main context:
|
||||
|
||||
1. The module being resolved would be relative to the `filename` option passed
|
||||
to `vm.Script` or `vm.compileFunction()`. The resolution can work with a
|
||||
`filename` that's either an absolute path or a URL string. If `filename` is
|
||||
a string that's neither an absolute path or a URL, or if it's undefined,
|
||||
the resolution will be relative to the current working directory
|
||||
of the process. In the case of `vm.createContext()`, the resolution is always
|
||||
relative to the current working directory since this option is only used when
|
||||
there isn't a referrer script or module.
|
||||
2. For any given `filename` that resolves to a specific path, once the process
|
||||
manages to load a particular module from that path, the result may be cached,
|
||||
and subsequent load of the same module from the same path would return the
|
||||
same thing. If the `filename` is a URL string, the cache would not be hit
|
||||
if it has different search parameters. For `filename`s that are not URL
|
||||
strings, there is currently no way to bypass the caching behavior.
|
||||
|
||||
### When `importModuleDynamically` is a function
|
||||
|
||||
When `importModuleDynamically` is a function, it will be invoked when `import()`
|
||||
is called in the compiled code for users to customize how the requested module
|
||||
should be compiled and evaluated. Currently, the Node.js instance must be
|
||||
launched with the `--experimental-vm-modules` flag for this option to work. If
|
||||
the flag isn't set, this callback will be ignored. If the code evaluated
|
||||
actually calls to `import()`, the result will reject with
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`][].
|
||||
|
||||
The callback `importModuleDynamically(specifier, referrer, importAttributes)`
|
||||
has the following signature:
|
||||
|
||||
* `specifier` {string} specifier passed to `import()`
|
||||
* `referrer` {vm.Script|Function|vm.SourceTextModule|Object}
|
||||
The referrer is the compiled `vm.Script` for `new vm.Script`,
|
||||
`vm.runInThisContext`, `vm.runInContext` and `vm.runInNewContext`. It's the
|
||||
compiled `Function` for `vm.compileFunction`, the compiled
|
||||
`vm.SourceTextModule` for `new vm.SourceTextModule`, and the context `Object`
|
||||
for `vm.createContext()`.
|
||||
* `importAttributes` {Object} The `"with"` value passed to the
|
||||
[`optionsExpression`][] optional parameter, or an empty object if no value was
|
||||
provided.
|
||||
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
|
||||
recommended in order to take advantage of error tracking, and to avoid issues
|
||||
with namespaces that contain `then` function exports.
|
||||
|
||||
```mjs
|
||||
// This script must be run with --experimental-vm-modules.
|
||||
import { Script, SyntheticModule } from 'node:vm';
|
||||
|
||||
const script = new Script('import("foo.json", { with: { type: "json" } })', {
|
||||
async importModuleDynamically(specifier, referrer, importAttributes) {
|
||||
console.log(specifier); // 'foo.json'
|
||||
console.log(referrer); // The compiled script
|
||||
console.log(importAttributes); // { type: 'json' }
|
||||
const m = new SyntheticModule(['bar'], () => { });
|
||||
await m.link(() => { });
|
||||
m.setExport('bar', { hello: 'world' });
|
||||
return m;
|
||||
},
|
||||
});
|
||||
const result = await script.runInThisContext();
|
||||
console.log(result); // { bar: { hello: 'world' } }
|
||||
```
|
||||
|
||||
```cjs
|
||||
// This script must be run with --experimental-vm-modules.
|
||||
const { Script, SyntheticModule } = require('node:vm');
|
||||
|
||||
(async function main() {
|
||||
const script = new Script('import("foo.json", { with: { type: "json" } })', {
|
||||
async importModuleDynamically(specifier, referrer, importAttributes) {
|
||||
console.log(specifier); // 'foo.json'
|
||||
console.log(referrer); // The compiled script
|
||||
console.log(importAttributes); // { type: 'json' }
|
||||
const m = new SyntheticModule(['bar'], () => { });
|
||||
await m.link(() => { });
|
||||
m.setExport('bar', { hello: 'world' });
|
||||
return m;
|
||||
},
|
||||
});
|
||||
const result = await script.runInThisContext();
|
||||
console.log(result); // { bar: { hello: 'world' } }
|
||||
})();
|
||||
```
|
||||
|
||||
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
|
||||
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
|
||||
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
|
||||
@ -1626,6 +1827,7 @@ are not controllable through the timeout either.
|
||||
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
|
||||
[Module Record]: https://262.ecma-international.org/14.0/#sec-abstract-module-records
|
||||
[Source Text Module Record]: https://tc39.es/ecma262/#sec-source-text-module-records
|
||||
[Support of dynamic `import()` in compilation APIs]: #support-of-dynamic-import-in-compilation-apis
|
||||
[Synthetic Module Record]: https://heycam.github.io/webidl/#synthetic-module-records
|
||||
[V8 Embedder's Guide]: https://v8.dev/docs/embed#contexts
|
||||
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
|
||||
|
@ -294,7 +294,6 @@ require('url'); // eslint-disable-line no-restricted-modules
|
||||
internalBinding('module_wrap');
|
||||
require('internal/modules/cjs/loader');
|
||||
require('internal/modules/esm/utils');
|
||||
require('internal/vm/module');
|
||||
|
||||
// Needed to refresh the time origin.
|
||||
require('internal/perf/utils');
|
||||
|
@ -52,7 +52,6 @@ const {
|
||||
SafeMap,
|
||||
SafeWeakMap,
|
||||
String,
|
||||
Symbol,
|
||||
StringPrototypeCharAt,
|
||||
StringPrototypeCharCodeAt,
|
||||
StringPrototypeEndsWith,
|
||||
@ -114,7 +113,6 @@ const {
|
||||
initializeCjsConditions,
|
||||
loadBuiltinModule,
|
||||
makeRequireFunction,
|
||||
normalizeReferrerURL,
|
||||
stripBOM,
|
||||
toRealPath,
|
||||
} = require('internal/modules/helpers');
|
||||
@ -125,12 +123,10 @@ const policy = getLazy(
|
||||
);
|
||||
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
|
||||
|
||||
const getCascadedLoader = getLazy(
|
||||
() => require('internal/process/esm_loader').esmLoader,
|
||||
);
|
||||
|
||||
const permission = require('internal/process/permission');
|
||||
|
||||
const {
|
||||
vm_dynamic_import_default_internal,
|
||||
} = internalBinding('symbols');
|
||||
// Whether any user-provided CJS modules had been loaded (executed).
|
||||
// Used for internal assertions.
|
||||
let hasLoadedAnyUserCJSModule = false;
|
||||
@ -1257,12 +1253,8 @@ let hasPausedEntry = false;
|
||||
* @param {object} codeCache The SEA code cache
|
||||
*/
|
||||
function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
|
||||
const hostDefinedOptionId = Symbol(`cjs:${filename}`);
|
||||
async function importModuleDynamically(specifier, _, importAttributes) {
|
||||
const cascadedLoader = getCascadedLoader();
|
||||
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
|
||||
importAttributes);
|
||||
}
|
||||
const hostDefinedOptionId = vm_dynamic_import_default_internal;
|
||||
const importModuleDynamically = vm_dynamic_import_default_internal;
|
||||
if (patched) {
|
||||
const wrapped = Module.wrap(content);
|
||||
const script = makeContextifyScript(
|
||||
|
@ -15,7 +15,6 @@ const {
|
||||
StringPrototypeReplaceAll,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeStartsWith,
|
||||
Symbol,
|
||||
SyntaxErrorPrototype,
|
||||
globalThis: { WebAssembly },
|
||||
} = primordials;
|
||||
@ -59,7 +58,9 @@ const { ModuleWrap } = moduleWrap;
|
||||
const asyncESM = require('internal/process/esm_loader');
|
||||
const { emitWarningSync } = require('internal/process/warning');
|
||||
const { internalCompileFunction } = require('internal/vm');
|
||||
|
||||
const {
|
||||
vm_dynamic_import_default_internal,
|
||||
} = internalBinding('symbols');
|
||||
// Lazy-loading to avoid circular dependencies.
|
||||
let getSourceSync;
|
||||
/**
|
||||
@ -206,9 +207,8 @@ function enrichCJSError(err, content, filename) {
|
||||
*/
|
||||
function loadCJSModule(module, source, url, filename) {
|
||||
let compiledWrapper;
|
||||
async function importModuleDynamically(specifier, _, importAttributes) {
|
||||
return asyncESM.esmLoader.import(specifier, url, importAttributes);
|
||||
}
|
||||
const hostDefinedOptionId = vm_dynamic_import_default_internal;
|
||||
const importModuleDynamically = vm_dynamic_import_default_internal;
|
||||
try {
|
||||
compiledWrapper = internalCompileFunction(
|
||||
source, // code,
|
||||
@ -226,8 +226,8 @@ function loadCJSModule(module, source, url, filename) {
|
||||
'__filename',
|
||||
'__dirname',
|
||||
],
|
||||
Symbol(`cjs:${filename}`), // hostDefinedOptionsId
|
||||
importModuleDynamically, // importModuleDynamically
|
||||
hostDefinedOptionId, // hostDefinedOptionsId
|
||||
importModuleDynamically, // importModuleDynamically
|
||||
).function;
|
||||
} catch (err) {
|
||||
enrichCJSError(err, source, filename);
|
||||
|
@ -4,7 +4,6 @@ const {
|
||||
ArrayIsArray,
|
||||
SafeSet,
|
||||
SafeWeakMap,
|
||||
Symbol,
|
||||
ObjectFreeze,
|
||||
} = primordials;
|
||||
|
||||
@ -14,8 +13,10 @@ const {
|
||||
},
|
||||
} = internalBinding('util');
|
||||
const {
|
||||
default_host_defined_options,
|
||||
vm_dynamic_import_default_internal,
|
||||
vm_dynamic_import_main_context_default,
|
||||
vm_dynamic_import_missing_flag,
|
||||
vm_dynamic_import_no_callback,
|
||||
} = internalBinding('symbols');
|
||||
|
||||
const {
|
||||
@ -28,12 +29,19 @@ const {
|
||||
loadPreloadModules,
|
||||
initializeFrozenIntrinsics,
|
||||
} = require('internal/process/pre_execution');
|
||||
const { getCWDURL } = require('internal/util');
|
||||
const {
|
||||
emitExperimentalWarning,
|
||||
getCWDURL,
|
||||
getLazy,
|
||||
} = require('internal/util');
|
||||
const {
|
||||
setImportModuleDynamicallyCallback,
|
||||
setInitializeImportMetaObjectCallback,
|
||||
} = internalBinding('module_wrap');
|
||||
const assert = require('internal/assert');
|
||||
const {
|
||||
normalizeReferrerURL,
|
||||
} = require('internal/modules/helpers');
|
||||
|
||||
let defaultConditions;
|
||||
/**
|
||||
@ -145,8 +153,10 @@ const moduleRegistries = new SafeWeakMap();
|
||||
*/
|
||||
function registerModule(referrer, registry) {
|
||||
const idSymbol = referrer[host_defined_option_symbol];
|
||||
if (idSymbol === default_host_defined_options ||
|
||||
idSymbol === vm_dynamic_import_missing_flag) {
|
||||
if (idSymbol === vm_dynamic_import_no_callback ||
|
||||
idSymbol === vm_dynamic_import_missing_flag ||
|
||||
idSymbol === vm_dynamic_import_main_context_default ||
|
||||
idSymbol === vm_dynamic_import_default_internal) {
|
||||
// The referrer is compiled without custom callbacks, so there is
|
||||
// no registry to hold on to. We'll throw
|
||||
// ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING when a callback is
|
||||
@ -158,26 +168,6 @@ function registerModule(referrer, registry) {
|
||||
moduleRegistries.set(idSymbol, registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the ModuleRegistry for dynamic import() calls with a realm
|
||||
* as the referrer. Similar to {@link registerModule}, but this function
|
||||
* generates a new id symbol instead of using the one from the referrer
|
||||
* object.
|
||||
* @param {globalThis} globalThis The globalThis object of the realm.
|
||||
* @param {ModuleRegistry} registry
|
||||
*/
|
||||
function registerRealm(globalThis, registry) {
|
||||
let idSymbol = globalThis[host_defined_option_symbol];
|
||||
// If the per-realm host-defined options is already registered, do nothing.
|
||||
if (idSymbol) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, register the per-realm host-defined options.
|
||||
idSymbol = Symbol('Realm globalThis');
|
||||
globalThis[host_defined_option_symbol] = idSymbol;
|
||||
moduleRegistries.set(idSymbol, registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the `import.meta` object for a given module.
|
||||
* @param {symbol} symbol - Reference to the module.
|
||||
@ -191,16 +181,44 @@ function initializeImportMetaObject(symbol, meta) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const getCascadedLoader = getLazy(
|
||||
() => require('internal/process/esm_loader').esmLoader,
|
||||
);
|
||||
|
||||
/**
|
||||
* Proxy the dynamic import to the default loader.
|
||||
* @param {string} specifier - The module specifier string.
|
||||
* @param {Record<string, string>} attributes - The import attributes object.
|
||||
* @param {string|null|undefined} referrerName - name of the referrer.
|
||||
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
|
||||
*/
|
||||
function defaultImportModuleDynamically(specifier, attributes, referrerName) {
|
||||
const parentURL = normalizeReferrerURL(referrerName);
|
||||
return getCascadedLoader().import(specifier, parentURL, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously imports a module dynamically using a callback function. The native callback.
|
||||
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
|
||||
* @param {string} specifier - The module specifier string.
|
||||
* @param {Record<string, string>} attributes - The import attributes object.
|
||||
* @param {string|null|undefined} referrerName - name of the referrer.
|
||||
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
|
||||
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
|
||||
*/
|
||||
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes) {
|
||||
async function importModuleDynamicallyCallback(referrerSymbol, specifier, attributes, referrerName) {
|
||||
// For user-provided vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, emit the warning
|
||||
// and fall back to the default loader.
|
||||
if (referrerSymbol === vm_dynamic_import_main_context_default) {
|
||||
emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER');
|
||||
return defaultImportModuleDynamically(specifier, attributes, referrerName);
|
||||
}
|
||||
// For script compiled internally that should use the default loader to handle dynamic
|
||||
// import, proxy the request to the default loader without the warning.
|
||||
if (referrerSymbol === vm_dynamic_import_default_internal) {
|
||||
return defaultImportModuleDynamically(specifier, attributes, referrerName);
|
||||
}
|
||||
|
||||
if (moduleRegistries.has(referrerSymbol)) {
|
||||
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
|
||||
if (importModuleDynamically !== undefined) {
|
||||
@ -273,7 +291,6 @@ async function initializeHooks() {
|
||||
|
||||
module.exports = {
|
||||
registerModule,
|
||||
registerRealm,
|
||||
initializeESM,
|
||||
initializeHooks,
|
||||
getDefaultConditions,
|
||||
|
@ -23,16 +23,19 @@ const { validateString } = require('internal/validators');
|
||||
const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched.
|
||||
const internalFS = require('internal/fs/utils');
|
||||
const path = require('path');
|
||||
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
|
||||
const { pathToFileURL, fileURLToPath } = require('internal/url');
|
||||
const assert = require('internal/assert');
|
||||
|
||||
const { getOptionValue } = require('internal/options');
|
||||
const { setOwnProperty } = require('internal/util');
|
||||
const { inspect } = require('internal/util/inspect');
|
||||
|
||||
const {
|
||||
privateSymbols: {
|
||||
require_private_symbol,
|
||||
},
|
||||
} = internalBinding('util');
|
||||
const { canParse: URLCanParse } = internalBinding('url');
|
||||
|
||||
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
|
||||
debug = fn;
|
||||
@ -288,14 +291,32 @@ function addBuiltinLibsToObject(object, dummyModuleName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* If a referrer is an URL instance or absolute path, convert it into an URL string.
|
||||
* @param {string | URL} referrer
|
||||
* Normalize the referrer name as a URL.
|
||||
* If it's a string containing an absolute path or a URL it's normalized as
|
||||
* a URL string.
|
||||
* Otherwise it's returned as undefined.
|
||||
* @param {string | null | undefined} referrerName
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function normalizeReferrerURL(referrer) {
|
||||
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
|
||||
return pathToFileURL(referrer).href;
|
||||
function normalizeReferrerURL(referrerName) {
|
||||
if (referrerName === null || referrerName === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return new URL(referrer).href;
|
||||
|
||||
if (typeof referrerName === 'string') {
|
||||
if (path.isAbsolute(referrerName)) {
|
||||
return pathToFileURL(referrerName).href;
|
||||
}
|
||||
|
||||
if (StringPrototypeStartsWith(referrerName, 'file://') ||
|
||||
URLCanParse(referrerName)) {
|
||||
return referrerName;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
assert.fail('Unreachable code reached by ' + inspect(referrerName));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -66,7 +66,6 @@ function prepareWorkerThreadExecution() {
|
||||
}
|
||||
|
||||
function prepareShadowRealmExecution() {
|
||||
const { registerRealm } = require('internal/modules/esm/utils');
|
||||
// Patch the process object with legacy properties and normalizations.
|
||||
// Do not expand argv1 as it is not available in ShadowRealm.
|
||||
patchProcessObject(false);
|
||||
@ -74,15 +73,24 @@ function prepareShadowRealmExecution() {
|
||||
|
||||
// Disable custom loaders in ShadowRealm.
|
||||
setupUserModules(true);
|
||||
registerRealm(globalThis, {
|
||||
__proto__: null,
|
||||
importModuleDynamically: (specifier, _referrer, attributes) => {
|
||||
// The handler for `ShadowRealm.prototype.importValue`.
|
||||
const { esmLoader } = require('internal/process/esm_loader');
|
||||
// `parentURL` is not set in the case of a ShadowRealm top-level import.
|
||||
return esmLoader.import(specifier, undefined, attributes);
|
||||
const {
|
||||
privateSymbols: {
|
||||
host_defined_option_symbol,
|
||||
},
|
||||
});
|
||||
} = internalBinding('util');
|
||||
const {
|
||||
vm_dynamic_import_default_internal,
|
||||
} = internalBinding('symbols');
|
||||
|
||||
// For ShadowRealm.prototype.importValue(), the referrer name is
|
||||
// always null, so the native ImportModuleDynamically() callback would
|
||||
// always fallback to look up the host-defined option from the
|
||||
// global object using host_defined_option_symbol. Using
|
||||
// vm_dynamic_import_default_internal as the host-defined option
|
||||
// instructs the JS-land importModuleDynamicallyCallback() to
|
||||
// proxy the request to defaultImportModuleDynamically().
|
||||
globalThis[host_defined_option_symbol] =
|
||||
vm_dynamic_import_default_internal;
|
||||
}
|
||||
|
||||
function prepareExecution(options) {
|
||||
|
@ -107,12 +107,10 @@ function extractSourceMapURLMagicComment(content) {
|
||||
function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
|
||||
const sourceMapsEnabled = getSourceMapsEnabled();
|
||||
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
|
||||
try {
|
||||
const { normalizeReferrerURL } = require('internal/modules/helpers');
|
||||
filename = normalizeReferrerURL(filename);
|
||||
} catch (err) {
|
||||
const { normalizeReferrerURL } = require('internal/modules/helpers');
|
||||
filename = normalizeReferrerURL(filename);
|
||||
if (filename === undefined) {
|
||||
// This is most likely an invalid filename in sourceURL of [eval]-wrapper.
|
||||
debug(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,9 @@ const {
|
||||
runInContext,
|
||||
} = ContextifyScript.prototype;
|
||||
const {
|
||||
default_host_defined_options,
|
||||
vm_dynamic_import_default_internal,
|
||||
vm_dynamic_import_main_context_default,
|
||||
vm_dynamic_import_no_callback,
|
||||
vm_dynamic_import_missing_flag,
|
||||
} = internalBinding('symbols');
|
||||
const {
|
||||
@ -27,7 +29,6 @@ const {
|
||||
getOptionValue,
|
||||
} = require('internal/options');
|
||||
|
||||
|
||||
function isContext(object) {
|
||||
validateObject(object, 'object', kValidateObjectAllowArray);
|
||||
|
||||
@ -35,6 +36,11 @@ function isContext(object) {
|
||||
}
|
||||
|
||||
function getHostDefinedOptionId(importModuleDynamically, hint) {
|
||||
if (importModuleDynamically === vm_dynamic_import_main_context_default ||
|
||||
importModuleDynamically === vm_dynamic_import_default_internal) {
|
||||
return importModuleDynamically;
|
||||
}
|
||||
|
||||
if (importModuleDynamically !== undefined) {
|
||||
// Check that it's either undefined or a function before we pass
|
||||
// it into the native constructor.
|
||||
@ -45,7 +51,7 @@ function getHostDefinedOptionId(importModuleDynamically, hint) {
|
||||
// We need a default host defined options that are the same for all
|
||||
// scripts not needing custom module callbacks so that the isolate
|
||||
// compilation cache can be hit.
|
||||
return default_host_defined_options;
|
||||
return vm_dynamic_import_no_callback;
|
||||
}
|
||||
// We should've thrown here immediately when we introduced
|
||||
// --experimental-vm-modules and importModuleDynamically, but since
|
||||
@ -61,6 +67,13 @@ function getHostDefinedOptionId(importModuleDynamically, hint) {
|
||||
}
|
||||
|
||||
function registerImportModuleDynamically(referrer, importModuleDynamically) {
|
||||
// If it's undefined or certain known symbol, there's no customization so
|
||||
// no need to register anything.
|
||||
if (importModuleDynamically === undefined ||
|
||||
importModuleDynamically === vm_dynamic_import_main_context_default ||
|
||||
importModuleDynamically === vm_dynamic_import_default_internal) {
|
||||
return;
|
||||
}
|
||||
const { importModuleDynamicallyWrap } = require('internal/vm/module');
|
||||
const { registerModule } = require('internal/modules/esm/utils');
|
||||
registerModule(referrer, {
|
||||
@ -99,9 +112,7 @@ function internalCompileFunction(
|
||||
result.function.cachedDataRejected = result.cachedDataRejected;
|
||||
}
|
||||
|
||||
if (importModuleDynamically !== undefined) {
|
||||
registerImportModuleDynamically(result.function, importModuleDynamically);
|
||||
}
|
||||
registerImportModuleDynamically(result.function, importModuleDynamically);
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -132,9 +143,7 @@ function makeContextifyScript(code,
|
||||
throw e; /* node-do-not-add-exception-line */
|
||||
}
|
||||
|
||||
if (importModuleDynamically !== undefined) {
|
||||
registerImportModuleDynamically(script, importModuleDynamically);
|
||||
}
|
||||
registerImportModuleDynamically(script, importModuleDynamically);
|
||||
return script;
|
||||
}
|
||||
|
||||
|
20
lib/vm.js
20
lib/vm.js
@ -23,6 +23,7 @@
|
||||
|
||||
const {
|
||||
ArrayPrototypeForEach,
|
||||
ObjectFreeze,
|
||||
Symbol,
|
||||
PromiseReject,
|
||||
ReflectApply,
|
||||
@ -61,6 +62,9 @@ const {
|
||||
isContext,
|
||||
registerImportModuleDynamically,
|
||||
} = require('internal/vm');
|
||||
const {
|
||||
vm_dynamic_import_main_context_default,
|
||||
} = internalBinding('symbols');
|
||||
const kParsingContext = Symbol('script parsing context');
|
||||
|
||||
class Script extends ContextifyScript {
|
||||
@ -108,9 +112,7 @@ class Script extends ContextifyScript {
|
||||
throw e; /* node-do-not-add-exception-line */
|
||||
}
|
||||
|
||||
if (importModuleDynamically !== undefined) {
|
||||
registerImportModuleDynamically(this, importModuleDynamically);
|
||||
}
|
||||
registerImportModuleDynamically(this, importModuleDynamically);
|
||||
}
|
||||
|
||||
runInThisContext(options) {
|
||||
@ -245,9 +247,7 @@ function createContext(contextObject = {}, options = kEmptyObject) {
|
||||
|
||||
makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
|
||||
// Register the context scope callback after the context was initialized.
|
||||
if (importModuleDynamically !== undefined) {
|
||||
registerImportModuleDynamically(contextObject, importModuleDynamically);
|
||||
}
|
||||
registerImportModuleDynamically(contextObject, importModuleDynamically);
|
||||
return contextObject;
|
||||
}
|
||||
|
||||
@ -378,6 +378,13 @@ function measureMemory(options = kEmptyObject) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const vmConstants = {
|
||||
__proto__: null,
|
||||
USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default,
|
||||
};
|
||||
|
||||
ObjectFreeze(vmConstants);
|
||||
|
||||
module.exports = {
|
||||
Script,
|
||||
createContext,
|
||||
@ -388,6 +395,7 @@ module.exports = {
|
||||
isContext,
|
||||
compileFunction,
|
||||
measureMemory,
|
||||
constants: vmConstants,
|
||||
};
|
||||
|
||||
// The vm module is patched to include vm.Module, vm.SourceTextModule
|
||||
|
@ -34,7 +34,6 @@
|
||||
// Symbols are per-isolate primitives but Environment proxies them
|
||||
// for the sake of convenience.
|
||||
#define PER_ISOLATE_SYMBOL_PROPERTIES(V) \
|
||||
V(default_host_defined_options, "default_host_defined_options") \
|
||||
V(fs_use_promises_symbol, "fs_use_promises_symbol") \
|
||||
V(async_id_symbol, "async_id_symbol") \
|
||||
V(handle_onclose_symbol, "handle_onclose") \
|
||||
@ -48,7 +47,11 @@
|
||||
V(onpskexchange_symbol, "onpskexchange") \
|
||||
V(resource_symbol, "resource_symbol") \
|
||||
V(trigger_async_id_symbol, "trigger_async_id_symbol") \
|
||||
V(vm_dynamic_import_missing_flag, "vm_dynamic_import_missing_flag")
|
||||
V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \
|
||||
V(vm_dynamic_import_main_context_default, \
|
||||
"vm_dynamic_import_main_context_default") \
|
||||
V(vm_dynamic_import_missing_flag, "vm_dynamic_import_missing_flag") \
|
||||
V(vm_dynamic_import_no_callback, "vm_dynamic_import_no_callback")
|
||||
|
||||
// Strings are per-isolate primitives but Environment proxies them
|
||||
// for the sake of convenience. Strings should be ASCII-only.
|
||||
|
@ -601,6 +601,7 @@ static MaybeLocal<Promise> ImportModuleDynamically(
|
||||
id,
|
||||
Local<Value>(specifier),
|
||||
attributes,
|
||||
resource_name,
|
||||
};
|
||||
|
||||
Local<Value> result;
|
||||
|
142
test/es-module/test-vm-main-context-default-loader.js
Normal file
142
test/es-module/test-vm-main-context-default-loader.js
Normal file
@ -0,0 +1,142 @@
|
||||
'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());
|
@ -107,7 +107,6 @@ expected.atRunTime = new Set([
|
||||
'NativeModule internal/net',
|
||||
'NativeModule internal/dns/utils',
|
||||
'NativeModule internal/process/pre_execution',
|
||||
'NativeModule internal/vm/module',
|
||||
'NativeModule internal/modules/esm/utils',
|
||||
]);
|
||||
|
||||
|
@ -227,6 +227,8 @@ const customTypesMap = {
|
||||
'vm.Module': 'vm.html#class-vmmodule',
|
||||
'vm.Script': 'vm.html#class-vmscript',
|
||||
'vm.SourceTextModule': 'vm.html#class-vmsourcetextmodule',
|
||||
'vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER':
|
||||
'vm.html#vmconstantsuse_main_context_default_loader',
|
||||
|
||||
'MessagePort': 'worker_threads.html#class-messageport',
|
||||
'Worker': 'worker_threads.html#class-worker',
|
||||
|
Loading…
Reference in New Issue
Block a user