mirror of https://github.com/nodejs/node.git
module: loader getSource, getFormat, transform hooks
PR-URL: https://github.com/nodejs/node/pull/30986 Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
This commit is contained in:
parent
20fd12310f
commit
2551a21553
|
@ -1383,6 +1383,6 @@ greater than `4` (its current default value). For more information, see the
|
|||
[debugger]: debugger.html
|
||||
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
|
||||
[emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
|
||||
[experimental ECMAScript Module loader]: esm.html#esm_resolve_hook
|
||||
[experimental ECMAScript Module loader]: esm.html#esm_experimental_loaders
|
||||
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
|
||||
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
|
||||
|
|
374
doc/api/esm.md
374
doc/api/esm.md
|
@ -994,7 +994,7 @@ node --experimental-wasm-modules index.mjs
|
|||
|
||||
would provide the exports interface for the instantiation of `module.wasm`.
|
||||
|
||||
## Experimental Loader hooks
|
||||
## Experimental Loaders
|
||||
|
||||
**Note: This API is currently being redesigned and will still change.**
|
||||
|
||||
|
@ -1006,39 +1006,49 @@ provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js.
|
|||
When hooks are used they only apply to ES module loading and not to any
|
||||
CommonJS modules loaded.
|
||||
|
||||
### Resolve hook
|
||||
### Hooks
|
||||
|
||||
The resolve hook returns the resolved file URL and module format for a
|
||||
given module specifier and parent file URL:
|
||||
#### <code>resolve</code> hook
|
||||
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
> signature may change. Do not rely on the API described below.
|
||||
|
||||
The `resolve` hook returns the resolved file URL for a given module specifier
|
||||
and parent URL. The module specifier is the string in an `import` statement or
|
||||
`import()` expression, and the parent URL is the URL of the module that imported
|
||||
this one, or `undefined` if this is the main entry point for the application.
|
||||
|
||||
```js
|
||||
import { URL, pathToFileURL } from 'url';
|
||||
const baseURL = pathToFileURL(process.cwd()).href;
|
||||
|
||||
/**
|
||||
* @param {string} specifier
|
||||
* @param {string} parentModuleURL
|
||||
* @param {function} defaultResolver
|
||||
* @param {object} context
|
||||
* @param {string} context.parentURL
|
||||
* @param {function} defaultResolve
|
||||
* @returns {object} response
|
||||
* @returns {string} response.url
|
||||
*/
|
||||
export async function resolve(specifier,
|
||||
parentModuleURL = baseURL,
|
||||
defaultResolver) {
|
||||
return {
|
||||
url: new URL(specifier, parentModuleURL).href,
|
||||
format: 'module'
|
||||
};
|
||||
export async function resolve(specifier, context, defaultResolve) {
|
||||
const { parentURL = null } = context;
|
||||
if (someCondition) {
|
||||
// For some or all specifiers, do some custom logic for resolving.
|
||||
// Always return an object of the form {url: <string>}
|
||||
return {
|
||||
url: (parentURL) ?
|
||||
new URL(specifier, parentURL).href : new URL(specifier).href
|
||||
};
|
||||
}
|
||||
// Defer to Node.js for all other specifiers.
|
||||
return defaultResolve(specifier, context, defaultResolve);
|
||||
}
|
||||
```
|
||||
|
||||
The `parentModuleURL` is provided as `undefined` when performing main Node.js
|
||||
load itself.
|
||||
#### <code>getFormat</code> hook
|
||||
|
||||
The default Node.js ES module resolution function is provided as a third
|
||||
argument to the resolver for easy compatibility workflows.
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
> signature may change. Do not rely on the API described below.
|
||||
|
||||
In addition to returning the resolved file URL value, the resolve hook also
|
||||
returns a `format` property specifying the module format of the resolved
|
||||
module. This can be one of the following:
|
||||
The `getFormat` hook provides a way to define a custom method of determining how
|
||||
a URL should be interpreted. This can be one of the following:
|
||||
|
||||
| `format` | Description |
|
||||
| --- | --- |
|
||||
|
@ -1046,74 +1056,120 @@ module. This can be one of the following:
|
|||
| `'commonjs'` | Load a Node.js CommonJS module |
|
||||
| `'dynamic'` | Use a [dynamic instantiate hook][] |
|
||||
| `'json'` | Load a JSON file |
|
||||
| `'module'` | Load a standard JavaScript module |
|
||||
| `'module'` | Load a standard JavaScript module (ES module) |
|
||||
| `'wasm'` | Load a WebAssembly module |
|
||||
|
||||
For example, a dummy loader to load JavaScript restricted to browser resolution
|
||||
rules with only JS file extension and Node.js builtin modules support could
|
||||
be written:
|
||||
|
||||
```js
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import Module from 'module';
|
||||
import { URL, pathToFileURL } from 'url';
|
||||
|
||||
const builtins = Module.builtinModules;
|
||||
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
|
||||
|
||||
const baseURL = pathToFileURL(process.cwd()).href;
|
||||
|
||||
/**
|
||||
* @param {string} specifier
|
||||
* @param {string} parentModuleURL
|
||||
* @param {function} defaultResolver
|
||||
* @param {string} url
|
||||
* @param {object} context (currently empty)
|
||||
* @param {function} defaultGetFormat
|
||||
* @returns {object} response
|
||||
* @returns {string} response.format
|
||||
*/
|
||||
export async function resolve(specifier,
|
||||
parentModuleURL = baseURL,
|
||||
defaultResolver) {
|
||||
if (builtins.includes(specifier)) {
|
||||
export async function getFormat(url, context, defaultGetFormat) {
|
||||
if (someCondition) {
|
||||
// For some or all URLs, do some custom logic for determining format.
|
||||
// Always return an object of the form {format: <string>}, where the
|
||||
// format is one of the strings in the table above.
|
||||
return {
|
||||
url: specifier,
|
||||
format: 'builtin'
|
||||
format: 'module'
|
||||
};
|
||||
}
|
||||
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
|
||||
// For node_modules support:
|
||||
// return defaultResolver(specifier, parentModuleURL);
|
||||
throw new Error(
|
||||
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
|
||||
}
|
||||
const resolved = new URL(specifier, parentModuleURL);
|
||||
const ext = path.extname(resolved.pathname);
|
||||
if (!JS_EXTENSIONS.has(ext)) {
|
||||
throw new Error(
|
||||
`Cannot load file with non-JavaScript file extension ${ext}.`);
|
||||
}
|
||||
return {
|
||||
url: resolved.href,
|
||||
format: 'module'
|
||||
};
|
||||
// Defer to Node.js for all other URLs.
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
```
|
||||
|
||||
With this loader, running:
|
||||
#### <code>getSource</code> hook
|
||||
|
||||
```console
|
||||
NODE_OPTIONS='--experimental-loader ./custom-loader.mjs' node x.js
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
> signature may change. Do not rely on the API described below.
|
||||
|
||||
The `getSource` hook provides a way to define a custom method for retrieving
|
||||
the source code of an ES module specifier. This would allow a loader to
|
||||
potentially avoid reading files from disk.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {object} context
|
||||
* @param {string} context.format
|
||||
* @param {function} defaultGetSource
|
||||
* @returns {object} response
|
||||
* @returns {string|buffer} response.source
|
||||
*/
|
||||
export async function getSource(url, context, defaultGetSource) {
|
||||
const { format } = context;
|
||||
if (someCondition) {
|
||||
// For some or all URLs, do some custom logic for retrieving the source.
|
||||
// Always return an object of the form {source: <string|buffer>}.
|
||||
return {
|
||||
source: '...'
|
||||
};
|
||||
}
|
||||
// Defer to Node.js for all other URLs.
|
||||
return defaultGetSource(url, context, defaultGetSource);
|
||||
}
|
||||
```
|
||||
|
||||
would load the module `x.js` as an ES module with relative resolution support
|
||||
(with `node_modules` loading skipped in this example).
|
||||
#### <code>transformSource</code> hook
|
||||
|
||||
### Dynamic instantiate hook
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
> signature may change. Do not rely on the API described below.
|
||||
|
||||
The `transformSource` hook provides a way to modify the source code of a loaded
|
||||
ES module file after the source string has been loaded but before Node.js has
|
||||
done anything with it.
|
||||
|
||||
If this hook is used to convert unknown-to-Node.js file types into executable
|
||||
JavaScript, a resolve hook is also necessary in order to register any
|
||||
unknown-to-Node.js file extensions. See the [transpiler loader example][] below.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {string|buffer} source
|
||||
* @param {object} context
|
||||
* @param {string} context.url
|
||||
* @param {string} context.format
|
||||
* @param {function} defaultTransformSource
|
||||
* @returns {object} response
|
||||
* @returns {string|buffer} response.source
|
||||
*/
|
||||
export async function transformSource(source,
|
||||
context,
|
||||
defaultTransformSource) {
|
||||
const { url, format } = context;
|
||||
if (someCondition) {
|
||||
// For some or all URLs, do some custom logic for modifying the source.
|
||||
// Always return an object of the form {source: <string|buffer>}.
|
||||
return {
|
||||
source: '...'
|
||||
};
|
||||
}
|
||||
// Defer to Node.js for all other sources.
|
||||
return defaultTransformSource(
|
||||
source, context, defaultTransformSource);
|
||||
}
|
||||
```
|
||||
|
||||
#### <code>dynamicInstantiate</code> hook
|
||||
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
> signature may change. Do not rely on the API described below.
|
||||
|
||||
To create a custom dynamic module that doesn't correspond to one of the
|
||||
existing `format` interpretations, the `dynamicInstantiate` hook can be used.
|
||||
This hook is called only for modules that return `format: 'dynamic'` from
|
||||
the `resolve` hook.
|
||||
the [`getFormat` hook][].
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {object} response
|
||||
* @returns {array} response.exports
|
||||
* @returns {function} response.execute
|
||||
*/
|
||||
export async function dynamicInstantiate(url) {
|
||||
return {
|
||||
exports: ['customExportName'],
|
||||
|
@ -1129,6 +1185,179 @@ With the list of module exports provided upfront, the `execute` function will
|
|||
then be called at the exact point of module evaluation order for that module
|
||||
in the import tree.
|
||||
|
||||
### Examples
|
||||
|
||||
The various loader hooks can be used together to accomplish wide-ranging
|
||||
customizations of Node.js’ code loading and evaluation behaviors.
|
||||
|
||||
#### HTTPS loader
|
||||
|
||||
In current Node.js, specifiers starting with `https://` are unsupported. The
|
||||
loader below registers hooks to enable rudimentary support for such specifiers.
|
||||
While this may seem like a significant improvement to Node.js core
|
||||
functionality, there are substantial downsides to actually using this loader:
|
||||
performance is much slower than loading files from disk, there is no caching,
|
||||
and there is no security.
|
||||
|
||||
```js
|
||||
// https-loader.mjs
|
||||
import { get } from 'https';
|
||||
|
||||
export function resolve(specifier, context, defaultResolve) {
|
||||
const { parentURL = null } = context;
|
||||
|
||||
// Normally Node.js would error on specifiers starting with 'https://', so
|
||||
// this hook intercepts them and converts them into absolute URLs to be
|
||||
// passed along to the later hooks below.
|
||||
if (specifier.startsWith('https://')) {
|
||||
return {
|
||||
url: specifier
|
||||
};
|
||||
} else if (parentURL && parentURL.startsWith('https://')) {
|
||||
return {
|
||||
url: new URL(specifier, parentURL).href
|
||||
};
|
||||
}
|
||||
|
||||
// Let Node.js handle all other specifiers.
|
||||
return defaultResolve(specifier, context, defaultResolve);
|
||||
}
|
||||
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
// This loader assumes all network-provided JavaScript is ES module code.
|
||||
if (url.startsWith('https://')) {
|
||||
return {
|
||||
format: 'module'
|
||||
};
|
||||
}
|
||||
|
||||
// Let Node.js handle all other URLs.
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
||||
export function getSource(url, context, defaultGetSource) {
|
||||
// For JavaScript to be loaded over the network, we need to fetch and
|
||||
// return it.
|
||||
if (url.startsWith('https://')) {
|
||||
return new Promise((resolve, reject) => {
|
||||
get(url, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => resolve({ source: data }));
|
||||
}).on('error', (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
// Let Node.js handle all other URLs.
|
||||
return defaultGetSource(url, context, defaultGetSource);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// main.mjs
|
||||
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
|
||||
|
||||
console.log(VERSION);
|
||||
```
|
||||
|
||||
With this loader, running:
|
||||
|
||||
```console
|
||||
node --experimental-loader ./https-loader.mjs ./main.js
|
||||
```
|
||||
|
||||
Will print the current version of CoffeeScript per the module at the URL in
|
||||
`main.mjs`.
|
||||
|
||||
#### Transpiler loader
|
||||
|
||||
Sources that are in formats Node.js doesn’t understand can be converted into
|
||||
JavaScript using the [`transformSource` hook][]. Before that hook gets called,
|
||||
however, other hooks need to tell Node.js not to throw an error on unknown file
|
||||
types; and to tell Node.js how to load this new file type.
|
||||
|
||||
This is obviously less performant than transpiling source files before running
|
||||
Node.js; a transpiler loader should only be used for development and testing
|
||||
purposes.
|
||||
|
||||
```js
|
||||
// coffeescript-loader.mjs
|
||||
import { URL, pathToFileURL } from 'url';
|
||||
import CoffeeScript from 'coffeescript';
|
||||
|
||||
const baseURL = pathToFileURL(`${process.cwd()}/`).href;
|
||||
|
||||
// CoffeeScript files end in .coffee, .litcoffee or .coffee.md.
|
||||
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
|
||||
|
||||
export function resolve(specifier, context, defaultResolve) {
|
||||
const { parentURL = baseURL } = context;
|
||||
|
||||
// Node.js normally errors on unknown file extensions, so return a URL for
|
||||
// specifiers ending in the CoffeeScript file extensions.
|
||||
if (extensionsRegex.test(specifier)) {
|
||||
return {
|
||||
url: new URL(specifier, parentURL).href
|
||||
};
|
||||
}
|
||||
|
||||
// Let Node.js handle all other specifiers.
|
||||
return defaultResolve(specifier, context, defaultResolve);
|
||||
}
|
||||
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
// Now that we patched resolve to let CoffeeScript URLs through, we need to
|
||||
// tell Node.js what format such URLs should be interpreted as. For the
|
||||
// purposes of this loader, all CoffeeScript URLs are ES modules.
|
||||
if (extensionsRegex.test(url)) {
|
||||
return {
|
||||
format: 'module'
|
||||
};
|
||||
}
|
||||
|
||||
// Let Node.js handle all other URLs.
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
||||
export function transformSource(source, context, defaultTransformSource) {
|
||||
const { url, format } = context;
|
||||
|
||||
if (extensionsRegex.test(url)) {
|
||||
return {
|
||||
source: CoffeeScript.compile(source, { bare: true })
|
||||
};
|
||||
}
|
||||
|
||||
// Let Node.js handle all other sources.
|
||||
return defaultTransformSource(source, context, defaultTransformSource);
|
||||
}
|
||||
```
|
||||
|
||||
```coffee
|
||||
# main.coffee
|
||||
import { scream } from './scream.coffee'
|
||||
console.log scream 'hello, world'
|
||||
|
||||
import { version } from 'process'
|
||||
console.log "Brought to you by Node.js version #{version}"
|
||||
```
|
||||
|
||||
```coffee
|
||||
# scream.coffee
|
||||
export scream = (str) -> str.toUpperCase()
|
||||
```
|
||||
|
||||
With this loader, running:
|
||||
|
||||
```console
|
||||
node --experimental-loader ./coffeescript-loader.mjs main.coffee
|
||||
```
|
||||
|
||||
Will cause `main.coffee` to be turned into JavaScript after its source code is
|
||||
loaded from disk but before Node.js executes it; and so on for any `.coffee`,
|
||||
`.litcoffee` or `.coffee.md` files referenced via `import` statements of any
|
||||
loaded file.
|
||||
|
||||
## Resolution Algorithm
|
||||
|
||||
### Features
|
||||
|
@ -1409,11 +1638,14 @@ success!
|
|||
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
|
||||
[`esm`]: https://github.com/standard-things/esm#readme
|
||||
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
|
||||
[`getFormat` hook]: #esm_code_getformat_code_hook
|
||||
[`import()`]: #esm_import-expressions
|
||||
[`import.meta.url`]: #esm_import_meta
|
||||
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
|
||||
[`module.createRequire()`]: modules.html#modules_module_createrequire_filename
|
||||
[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports
|
||||
[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
|
||||
[`transformSource` hook]: #esm_code_transformsource_code_hook
|
||||
[dynamic instantiate hook]: #esm_code_dynamicinstantiate_code_hook
|
||||
[special scheme]: https://url.spec.whatwg.org/#special-scheme
|
||||
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
|
||||
[transpiler loader example]: #esm_transpiler_loader
|
||||
|
|
|
@ -1334,7 +1334,7 @@ E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error);
|
|||
E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error);
|
||||
E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError);
|
||||
E('ERR_UNKNOWN_FILE_EXTENSION',
|
||||
'Unknown file extension "%s" for %s imported from %s',
|
||||
'Unknown file extension "%s" for %s',
|
||||
TypeError);
|
||||
E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError);
|
||||
E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError);
|
||||
|
|
|
@ -51,8 +51,10 @@ function checkSyntax(source, filename) {
|
|||
if (filename === '[stdin]' || filename === '[eval]') {
|
||||
isModule = getOptionValue('--input-type') === 'module';
|
||||
} else {
|
||||
const resolve = require('internal/modules/esm/default_resolve');
|
||||
const { format } = resolve(pathToFileURL(filename).toString());
|
||||
const { defaultResolve } = require('internal/modules/esm/resolve');
|
||||
const { defaultGetFormat } = require('internal/modules/esm/get_format');
|
||||
const { url } = defaultResolve(pathToFileURL(filename).toString());
|
||||
const { format } = defaultGetFormat(url);
|
||||
isModule = format === 'module';
|
||||
}
|
||||
if (isModule) {
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
SafeMap,
|
||||
} = primordials;
|
||||
|
||||
const internalFS = require('internal/fs/utils');
|
||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||
const { extname } = require('path');
|
||||
const { realpathSync } = require('fs');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
|
||||
const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
||||
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
||||
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
|
||||
const experimentalSpeciferResolution =
|
||||
getOptionValue('--experimental-specifier-resolution');
|
||||
const typeFlag = getOptionValue('--input-type');
|
||||
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
|
||||
const { resolve: moduleWrapResolve,
|
||||
getPackageType } = internalBinding('module_wrap');
|
||||
const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
|
||||
const { ERR_INPUT_TYPE_NOT_ALLOWED,
|
||||
ERR_UNKNOWN_FILE_EXTENSION,
|
||||
ERR_UNSUPPORTED_ESM_URL_SCHEME } = require('internal/errors').codes;
|
||||
|
||||
const realpathCache = new SafeMap();
|
||||
|
||||
// const TYPE_NONE = 0;
|
||||
// const TYPE_COMMONJS = 1;
|
||||
const TYPE_MODULE = 2;
|
||||
|
||||
const extensionFormatMap = {
|
||||
'__proto__': null,
|
||||
'.cjs': 'commonjs',
|
||||
'.js': 'module',
|
||||
'.mjs': 'module'
|
||||
};
|
||||
|
||||
const legacyExtensionFormatMap = {
|
||||
'__proto__': null,
|
||||
'.cjs': 'commonjs',
|
||||
'.js': 'commonjs',
|
||||
'.json': 'commonjs',
|
||||
'.mjs': 'module',
|
||||
'.node': 'commonjs'
|
||||
};
|
||||
|
||||
if (experimentalWasmModules)
|
||||
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
|
||||
|
||||
if (experimentalJsonModules)
|
||||
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
|
||||
|
||||
function resolve(specifier, parentURL) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(specifier);
|
||||
if (parsed.protocol === 'data:') {
|
||||
const [ , mime ] = /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/.exec(parsed.pathname) || [ null, null, null ];
|
||||
const format = ({
|
||||
'__proto__': null,
|
||||
'text/javascript': 'module',
|
||||
'application/json': 'json',
|
||||
'application/wasm': experimentalWasmModules ? 'wasm' : null
|
||||
})[mime] || null;
|
||||
return {
|
||||
url: specifier,
|
||||
format
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
|
||||
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME();
|
||||
if (NativeModule.canBeRequiredByUsers(specifier)) {
|
||||
return {
|
||||
url: specifier,
|
||||
format: 'builtin'
|
||||
};
|
||||
}
|
||||
if (parentURL && parentURL.startsWith('data:')) {
|
||||
// This is gonna blow up, we want the error
|
||||
new URL(specifier, parentURL);
|
||||
}
|
||||
|
||||
const isMain = parentURL === undefined;
|
||||
if (isMain) {
|
||||
parentURL = pathToFileURL(`${process.cwd()}/`).href;
|
||||
|
||||
// This is the initial entry point to the program, and --input-type has
|
||||
// been passed as an option; but --input-type can only be used with
|
||||
// --eval, --print or STDIN string input. It is not allowed with file
|
||||
// input, to avoid user confusion over how expansive the effect of the
|
||||
// flag should be (i.e. entry point only, package scope surrounding the
|
||||
// entry point, etc.).
|
||||
if (typeFlag)
|
||||
throw new ERR_INPUT_TYPE_NOT_ALLOWED();
|
||||
}
|
||||
|
||||
let url = moduleWrapResolve(specifier, parentURL);
|
||||
|
||||
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
|
||||
const real = realpathSync(fileURLToPath(url), {
|
||||
[internalFS.realpathCacheKey]: realpathCache
|
||||
});
|
||||
const old = url;
|
||||
url = pathToFileURL(real);
|
||||
url.search = old.search;
|
||||
url.hash = old.hash;
|
||||
}
|
||||
|
||||
const ext = extname(url.pathname);
|
||||
let format;
|
||||
if (ext === '.js' || ext === '') {
|
||||
format = getPackageType(url.href) === TYPE_MODULE ? 'module' : 'commonjs';
|
||||
} else {
|
||||
format = extensionFormatMap[ext];
|
||||
}
|
||||
if (!format) {
|
||||
if (experimentalSpeciferResolution === 'node') {
|
||||
process.emitWarning(
|
||||
'The Node.js specifier resolution in ESM is experimental.',
|
||||
'ExperimentalWarning');
|
||||
format = legacyExtensionFormatMap[ext];
|
||||
} else {
|
||||
throw new ERR_UNKNOWN_FILE_EXTENSION(
|
||||
ext,
|
||||
fileURLToPath(url),
|
||||
fileURLToPath(parentURL));
|
||||
}
|
||||
}
|
||||
return { url: `${url}`, format };
|
||||
}
|
||||
|
||||
module.exports = resolve;
|
|
@ -0,0 +1,77 @@
|
|||
'use strict';
|
||||
|
||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||
const { extname } = require('path');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
|
||||
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
|
||||
const experimentalSpeciferResolution =
|
||||
getOptionValue('--experimental-specifier-resolution');
|
||||
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
|
||||
const { getPackageType } = internalBinding('module_wrap');
|
||||
const { URL, fileURLToPath } = require('internal/url');
|
||||
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
|
||||
|
||||
// const TYPE_NONE = 0;
|
||||
// const TYPE_COMMONJS = 1;
|
||||
const TYPE_MODULE = 2;
|
||||
|
||||
const extensionFormatMap = {
|
||||
'__proto__': null,
|
||||
'.cjs': 'commonjs',
|
||||
'.js': 'module',
|
||||
'.mjs': 'module'
|
||||
};
|
||||
|
||||
const legacyExtensionFormatMap = {
|
||||
'__proto__': null,
|
||||
'.cjs': 'commonjs',
|
||||
'.js': 'commonjs',
|
||||
'.json': 'commonjs',
|
||||
'.mjs': 'module',
|
||||
'.node': 'commonjs'
|
||||
};
|
||||
|
||||
if (experimentalWasmModules)
|
||||
extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
|
||||
|
||||
if (experimentalJsonModules)
|
||||
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
|
||||
|
||||
function defaultGetFormat(url, context, defaultGetFormat) {
|
||||
if (NativeModule.canBeRequiredByUsers(url)) {
|
||||
return { format: 'builtin' };
|
||||
}
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'data:') {
|
||||
const [ , mime ] = /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/.exec(parsed.pathname) || [ null, null, null ];
|
||||
const format = ({
|
||||
'__proto__': null,
|
||||
'text/javascript': 'module',
|
||||
'application/json': experimentalJsonModules ? 'json' : null,
|
||||
'application/wasm': experimentalWasmModules ? 'wasm' : null
|
||||
})[mime] || null;
|
||||
return { format };
|
||||
} else if (parsed.protocol === 'file:') {
|
||||
const ext = extname(parsed.pathname);
|
||||
let format;
|
||||
if (ext === '.js' || ext === '') {
|
||||
format = getPackageType(parsed.href) === TYPE_MODULE ?
|
||||
'module' : 'commonjs';
|
||||
} else {
|
||||
format = extensionFormatMap[ext];
|
||||
}
|
||||
if (!format) {
|
||||
if (experimentalSpeciferResolution === 'node') {
|
||||
process.emitWarning(
|
||||
'The Node.js specifier resolution in ESM is experimental.',
|
||||
'ExperimentalWarning');
|
||||
format = legacyExtensionFormatMap[ext];
|
||||
} else {
|
||||
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
|
||||
}
|
||||
}
|
||||
return { format: format || null };
|
||||
}
|
||||
}
|
||||
exports.defaultGetFormat = defaultGetFormat;
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const { Buffer } = require('buffer');
|
||||
|
||||
const fs = require('fs');
|
||||
const { URL } = require('url');
|
||||
const { promisify } = require('internal/util');
|
||||
const {
|
||||
ERR_INVALID_URL,
|
||||
ERR_INVALID_URL_SCHEME,
|
||||
} = require('internal/errors').codes;
|
||||
const readFileAsync = promisify(fs.readFile);
|
||||
|
||||
const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
|
||||
|
||||
async function defaultGetSource(url, { format } = {}, defaultGetSource) {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'file:') {
|
||||
return {
|
||||
source: await readFileAsync(parsed)
|
||||
};
|
||||
} else if (parsed.protocol === 'data:') {
|
||||
const match = DATA_URL_PATTERN.exec(parsed.pathname);
|
||||
if (!match) {
|
||||
throw new ERR_INVALID_URL(url);
|
||||
}
|
||||
const [ , base64, body ] = match;
|
||||
return {
|
||||
source: Buffer.from(body, base64 ? 'base64' : 'utf8')
|
||||
};
|
||||
} else {
|
||||
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
|
||||
}
|
||||
}
|
||||
exports.defaultGetSource = defaultGetSource;
|
|
@ -13,18 +13,21 @@ const {
|
|||
ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK,
|
||||
ERR_UNKNOWN_MODULE_FORMAT
|
||||
} = require('internal/errors').codes;
|
||||
const {
|
||||
URL,
|
||||
pathToFileURL
|
||||
} = require('url');
|
||||
const { URL, pathToFileURL } = require('internal/url');
|
||||
const { validateString } = require('internal/validators');
|
||||
const ModuleMap = require('internal/modules/esm/module_map');
|
||||
const ModuleJob = require('internal/modules/esm/module_job');
|
||||
|
||||
const defaultResolve = require('internal/modules/esm/default_resolve');
|
||||
const { defaultResolve } = require('internal/modules/esm/resolve');
|
||||
const { defaultGetFormat } = require('internal/modules/esm/get_format');
|
||||
const { defaultGetSource } = require(
|
||||
'internal/modules/esm/get_source');
|
||||
const { defaultTransformSource } = require(
|
||||
'internal/modules/esm/transform_source');
|
||||
const createDynamicModule = require(
|
||||
'internal/modules/esm/create_dynamic_module');
|
||||
const { translators } = require('internal/modules/esm/translators');
|
||||
const { translators } = require(
|
||||
'internal/modules/esm/translators');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
|
||||
const debug = require('internal/util/debuglog').debuglog('esm');
|
||||
|
@ -46,13 +49,23 @@ class Loader {
|
|||
|
||||
// The resolver has the signature
|
||||
// (specifier : string, parentURL : string, defaultResolve)
|
||||
// -> Promise<{ url : string, format: string }>
|
||||
// -> Promise<{ url : string }>
|
||||
// where defaultResolve is ModuleRequest.resolve (having the same
|
||||
// signature itself).
|
||||
this._resolve = defaultResolve;
|
||||
// This hook is called after the module is resolved but before a translator
|
||||
// is chosen to load it; the format returned by this function is the name
|
||||
// of a translator.
|
||||
// If `.format` on the returned value is 'dynamic', .dynamicInstantiate
|
||||
// will be used as described below.
|
||||
this._resolve = defaultResolve;
|
||||
// This hook is only called when resolve(...).format is 'dynamic' and
|
||||
this._getFormat = defaultGetFormat;
|
||||
// This hook is called just before the source code of an ES module file
|
||||
// is loaded.
|
||||
this._getSource = defaultGetSource;
|
||||
// This hook is called just after the source code of an ES module file
|
||||
// is loaded, but before anything is done with the string.
|
||||
this._transformSource = defaultTransformSource;
|
||||
// This hook is only called when getFormat is 'dynamic' and
|
||||
// has the signature
|
||||
// (url : string) -> Promise<{ exports: { ... }, execute: function }>
|
||||
// Where `exports` is an object whose property names define the exported
|
||||
|
@ -69,27 +82,35 @@ class Loader {
|
|||
if (!isMain)
|
||||
validateString(parentURL, 'parentURL');
|
||||
|
||||
const resolved = await this._resolve(specifier, parentURL, defaultResolve);
|
||||
|
||||
if (typeof resolved !== 'object')
|
||||
const resolveResponse = await this._resolve(
|
||||
specifier, { parentURL }, defaultResolve);
|
||||
if (typeof resolveResponse !== 'object') {
|
||||
throw new ERR_INVALID_RETURN_VALUE(
|
||||
'object', 'loader resolve', resolved
|
||||
);
|
||||
'object', 'loader resolve', resolveResponse);
|
||||
}
|
||||
|
||||
const { url, format } = resolved;
|
||||
|
||||
if (typeof url !== 'string')
|
||||
const { url } = resolveResponse;
|
||||
if (typeof url !== 'string') {
|
||||
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
|
||||
'string', 'loader resolve', 'url', url
|
||||
);
|
||||
'string', 'loader resolve', 'url', url);
|
||||
}
|
||||
|
||||
if (typeof format !== 'string')
|
||||
const getFormatResponse = await this._getFormat(
|
||||
url, {}, defaultGetFormat);
|
||||
if (typeof getFormatResponse !== 'object') {
|
||||
throw new ERR_INVALID_RETURN_VALUE(
|
||||
'object', 'loader getFormat', getFormatResponse);
|
||||
}
|
||||
|
||||
const { format } = getFormatResponse;
|
||||
if (typeof format !== 'string') {
|
||||
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
|
||||
'string', 'loader resolve', 'format', format
|
||||
);
|
||||
'string', 'loader getFormat', 'format', format);
|
||||
}
|
||||
|
||||
if (format === 'builtin')
|
||||
if (format === 'builtin') {
|
||||
return { url: `node:${url}`, format };
|
||||
}
|
||||
|
||||
if (this._resolve !== defaultResolve) {
|
||||
try {
|
||||
|
@ -101,13 +122,15 @@ class Loader {
|
|||
}
|
||||
}
|
||||
|
||||
if (format !== 'dynamic' &&
|
||||
if (this._resolve === defaultResolve &&
|
||||
format !== 'dynamic' &&
|
||||
!url.startsWith('file:') &&
|
||||
!url.startsWith('data:')
|
||||
)
|
||||
) {
|
||||
throw new ERR_INVALID_RETURN_PROPERTY(
|
||||
'file: or data: url', 'loader resolve', 'url', url
|
||||
);
|
||||
}
|
||||
|
||||
return { url, format };
|
||||
}
|
||||
|
@ -142,7 +165,7 @@ class Loader {
|
|||
return module.getNamespace();
|
||||
}
|
||||
|
||||
hook({ resolve, dynamicInstantiate }) {
|
||||
hook({ resolve, dynamicInstantiate, getFormat, getSource, transformSource }) {
|
||||
// Use .bind() to avoid giving access to the Loader instance when called.
|
||||
if (resolve !== undefined)
|
||||
this._resolve = FunctionPrototypeBind(resolve, null);
|
||||
|
@ -150,6 +173,15 @@ class Loader {
|
|||
this._dynamicInstantiate =
|
||||
FunctionPrototypeBind(dynamicInstantiate, null);
|
||||
}
|
||||
if (getFormat !== undefined) {
|
||||
this._getFormat = FunctionPrototypeBind(getFormat, null);
|
||||
}
|
||||
if (getSource !== undefined) {
|
||||
this._getSource = FunctionPrototypeBind(getSource, null);
|
||||
}
|
||||
if (transformSource !== undefined) {
|
||||
this._transformSource = FunctionPrototypeBind(transformSource, null);
|
||||
}
|
||||
}
|
||||
|
||||
async getModuleJob(specifier, parentURL) {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
SafeMap,
|
||||
} = primordials;
|
||||
|
||||
const internalFS = require('internal/fs/utils');
|
||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||
const { realpathSync } = require('fs');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
|
||||
const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
||||
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
||||
const typeFlag = getOptionValue('--input-type');
|
||||
const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
|
||||
const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
|
||||
const { ERR_INPUT_TYPE_NOT_ALLOWED,
|
||||
ERR_UNSUPPORTED_ESM_URL_SCHEME } = require('internal/errors').codes;
|
||||
|
||||
const realpathCache = new SafeMap();
|
||||
|
||||
function defaultResolve(specifier, { parentURL } = {}, defaultResolve) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(specifier);
|
||||
if (parsed.protocol === 'data:') {
|
||||
return {
|
||||
url: specifier
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
|
||||
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME();
|
||||
if (NativeModule.canBeRequiredByUsers(specifier)) {
|
||||
return {
|
||||
url: specifier
|
||||
};
|
||||
}
|
||||
if (parentURL && parentURL.startsWith('data:')) {
|
||||
// This is gonna blow up, we want the error
|
||||
new URL(specifier, parentURL);
|
||||
}
|
||||
|
||||
const isMain = parentURL === undefined;
|
||||
if (isMain) {
|
||||
parentURL = pathToFileURL(`${process.cwd()}/`).href;
|
||||
|
||||
// This is the initial entry point to the program, and --input-type has
|
||||
// been passed as an option; but --input-type can only be used with
|
||||
// --eval, --print or STDIN string input. It is not allowed with file
|
||||
// input, to avoid user confusion over how expansive the effect of the
|
||||
// flag should be (i.e. entry point only, package scope surrounding the
|
||||
// entry point, etc.).
|
||||
if (typeFlag)
|
||||
throw new ERR_INPUT_TYPE_NOT_ALLOWED();
|
||||
}
|
||||
|
||||
let url = moduleWrapResolve(specifier, parentURL);
|
||||
|
||||
if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
|
||||
const real = realpathSync(fileURLToPath(url), {
|
||||
[internalFS.realpathCacheKey]: realpathCache
|
||||
});
|
||||
const old = url;
|
||||
url = pathToFileURL(real);
|
||||
url.search = old.search;
|
||||
url.hash = old.hash;
|
||||
}
|
||||
|
||||
return { url: `${url}` };
|
||||
}
|
||||
exports.defaultResolve = defaultResolve;
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
function defaultTransformSource(source, { url, format } = {},
|
||||
defaultTransformSource) {
|
||||
return { source };
|
||||
}
|
||||
exports.defaultTransformSource = defaultTransformSource;
|
|
@ -9,26 +9,22 @@ const {
|
|||
StringPrototypeReplace,
|
||||
} = primordials;
|
||||
|
||||
const { Buffer } = require('buffer');
|
||||
|
||||
const {
|
||||
stripBOM,
|
||||
loadNativeModule
|
||||
} = require('internal/modules/cjs/helpers');
|
||||
const CJSModule = require('internal/modules/cjs/loader').Module;
|
||||
const internalURLModule = require('internal/url');
|
||||
const { defaultGetSource } = require(
|
||||
'internal/modules/esm/get_source');
|
||||
const { defaultTransformSource } = require(
|
||||
'internal/modules/esm/transform_source');
|
||||
const createDynamicModule = require(
|
||||
'internal/modules/esm/create_dynamic_module');
|
||||
const fs = require('fs');
|
||||
const { fileURLToPath, URL } = require('url');
|
||||
const { debuglog } = require('internal/util/debuglog');
|
||||
const { promisify, emitExperimentalWarning } = require('internal/util');
|
||||
const {
|
||||
ERR_INVALID_URL,
|
||||
ERR_INVALID_URL_SCHEME,
|
||||
ERR_UNKNOWN_BUILTIN_MODULE
|
||||
} = require('internal/errors').codes;
|
||||
const readFileAsync = promisify(fs.readFile);
|
||||
const { emitExperimentalWarning } = require('internal/util');
|
||||
const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes;
|
||||
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
|
||||
const moduleWrap = internalBinding('module_wrap');
|
||||
const { ModuleWrap } = moduleWrap;
|
||||
|
@ -38,23 +34,6 @@ const debug = debuglog('esm');
|
|||
const translators = new SafeMap();
|
||||
exports.translators = translators;
|
||||
|
||||
const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
|
||||
function getSource(url) {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'file:') {
|
||||
return readFileAsync(parsed);
|
||||
} else if (parsed.protocol === 'data:') {
|
||||
const match = DATA_URL_PATTERN.exec(parsed.pathname);
|
||||
if (!match) {
|
||||
throw new ERR_INVALID_URL(url);
|
||||
}
|
||||
const [ , base64, body ] = match;
|
||||
return Buffer.from(body, base64 ? 'base64' : 'utf8');
|
||||
} else {
|
||||
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
|
||||
}
|
||||
}
|
||||
|
||||
function errPath(url) {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'file:') {
|
||||
|
@ -77,7 +56,11 @@ async function importModuleDynamically(specifier, { url }) {
|
|||
|
||||
// Strategy for loading a standard JavaScript module
|
||||
translators.set('module', async function moduleStrategy(url) {
|
||||
const source = `${await getSource(url)}`;
|
||||
let { source } = await this._getSource(
|
||||
url, { format: 'module' }, defaultGetSource);
|
||||
source = `${source}`;
|
||||
({ source } = await this._transformSource(
|
||||
source, { url, format: 'module' }, defaultTransformSource));
|
||||
maybeCacheSourceMap(url, source);
|
||||
debug(`Translating StandardModule ${url}`);
|
||||
const module = new ModuleWrap(url, undefined, source, 0, 0);
|
||||
|
@ -150,7 +133,11 @@ translators.set('json', async function jsonStrategy(url) {
|
|||
});
|
||||
}
|
||||
}
|
||||
const content = `${await getSource(url)}`;
|
||||
let { source } = await this._getSource(
|
||||
url, { format: 'json' }, defaultGetSource);
|
||||
source = `${source}`;
|
||||
({ source } = await this._transformSource(
|
||||
source, { url, format: 'json' }, defaultTransformSource));
|
||||
if (pathname) {
|
||||
// A require call could have been called on the same file during loading and
|
||||
// that resolves synchronously. To make sure we always return the identical
|
||||
|
@ -164,7 +151,7 @@ translators.set('json', async function jsonStrategy(url) {
|
|||
}
|
||||
}
|
||||
try {
|
||||
const exports = JSONParse(stripBOM(content));
|
||||
const exports = JSONParse(stripBOM(source));
|
||||
module = {
|
||||
exports,
|
||||
loaded: true
|
||||
|
@ -189,11 +176,14 @@ translators.set('json', async function jsonStrategy(url) {
|
|||
// Strategy for loading a wasm module
|
||||
translators.set('wasm', async function(url) {
|
||||
emitExperimentalWarning('Importing Web Assembly modules');
|
||||
const buffer = await getSource(url);
|
||||
let { source } = await this._getSource(
|
||||
url, { format: 'wasm' }, defaultGetSource);
|
||||
({ source } = await this._transformSource(
|
||||
source, { url, format: 'wasm' }, defaultTransformSource));
|
||||
debug(`Translating WASMModule ${url}`);
|
||||
let compiled;
|
||||
try {
|
||||
compiled = await WebAssembly.compile(buffer);
|
||||
compiled = await WebAssembly.compile(source);
|
||||
} catch (err) {
|
||||
err.message = errPath(url) + ': ' + err.message;
|
||||
throw err;
|
||||
|
|
5
node.gyp
5
node.gyp
|
@ -156,9 +156,12 @@
|
|||
'lib/internal/modules/cjs/loader.js',
|
||||
'lib/internal/modules/esm/loader.js',
|
||||
'lib/internal/modules/esm/create_dynamic_module.js',
|
||||
'lib/internal/modules/esm/default_resolve.js',
|
||||
'lib/internal/modules/esm/get_format.js',
|
||||
'lib/internal/modules/esm/get_source.js',
|
||||
'lib/internal/modules/esm/module_job.js',
|
||||
'lib/internal/modules/esm/module_map.js',
|
||||
'lib/internal/modules/esm/resolve.js',
|
||||
'lib/internal/modules/esm/transform_source.js',
|
||||
'lib/internal/modules/esm/translators.js',
|
||||
'lib/internal/net.js',
|
||||
'lib/internal/options.js',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Flags: --experimental-json-modules
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('assert');
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/get-source.mjs
|
||||
/* eslint-disable node-core/require-common-first, node-core/required-modules */
|
||||
import assert from 'assert';
|
||||
import { message } from '../fixtures/es-modules/message.mjs';
|
||||
|
||||
assert.strictEqual(message, 'WOOHOO!');
|
|
@ -6,10 +6,8 @@ const { spawnSync } = require('child_process');
|
|||
const fixture = fixtures.path('/es-modules/import-invalid-ext.mjs');
|
||||
const child = spawnSync(process.execPath, [fixture]);
|
||||
const errMsg = 'TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension';
|
||||
const importMsg = `imported from ${fixture}`;
|
||||
|
||||
assert.strictEqual(child.status, 1);
|
||||
assert.strictEqual(child.signal, null);
|
||||
assert.strictEqual(child.stdout.toString().trim(), '');
|
||||
assert(child.stderr.toString().includes(errMsg));
|
||||
assert(child.stderr.toString().includes(importMsg));
|
||||
assert.ok(child.stderr.toString().includes(errMsg));
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-get-format.mjs
|
||||
import { mustCall, mustNotCall } from '../common/index.mjs';
|
||||
import assert from 'assert';
|
||||
|
||||
import('../fixtures/es-modules/package-type-module/extension.unknown')
|
||||
.then(
|
||||
mustCall((ns) => {
|
||||
assert.strictEqual(ns.default, 'unknown');
|
||||
}),
|
||||
// Do not use .catch; want exclusive or
|
||||
mustNotCall(() => {})
|
||||
);
|
|
@ -4,8 +4,7 @@ import assert from 'assert';
|
|||
|
||||
import('../fixtures/es-modules/test-esm-ok.mjs')
|
||||
.then(assert.fail, expectsError({
|
||||
code: 'ERR_INVALID_RETURN_PROPERTY_VALUE',
|
||||
message: 'Expected string to be returned for the "format" from the ' +
|
||||
'"loader resolve" function but got type undefined.'
|
||||
code: 'ERR_UNKNOWN_MODULE_FORMAT',
|
||||
message: /Unknown module format: esm/
|
||||
}))
|
||||
.then(mustCall());
|
||||
|
|
|
@ -4,9 +4,7 @@ import assert from 'assert';
|
|||
|
||||
import('../fixtures/es-modules/test-esm-ok.mjs')
|
||||
.then(assert.fail, expectsError({
|
||||
code: 'ERR_INVALID_RETURN_PROPERTY',
|
||||
message: 'Expected a valid url to be returned for the "url" from the ' +
|
||||
'"loader resolve" function but got ' +
|
||||
'../fixtures/es-modules/test-esm-ok.mjs.'
|
||||
code: 'ERR_INVALID_URL',
|
||||
message: 'Invalid URL: ../fixtures/es-modules/test-esm-ok.mjs'
|
||||
}))
|
||||
.then(mustCall());
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
require('../common');
|
||||
|
||||
const assert = require('assert');
|
||||
const resolve = require('internal/modules/esm/default_resolve');
|
||||
const {
|
||||
defaultResolve: resolve
|
||||
} = require('internal/modules/esm/resolve');
|
||||
|
||||
assert.throws(
|
||||
() => resolve('target', undefined),
|
||||
() => resolve('target'),
|
||||
{
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
name: 'Error',
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/transform-source.mjs
|
||||
/* eslint-disable node-core/require-common-first, node-core/required-modules */
|
||||
import assert from 'assert';
|
||||
import { message } from '../fixtures/es-modules/message.mjs';
|
||||
|
||||
assert.strictEqual(message, 'A MESSAGE');
|
|
@ -1,7 +1,16 @@
|
|||
import module from 'module';
|
||||
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
if (module.builtinModules.includes(url)) {
|
||||
return {
|
||||
format: 'dynamic'
|
||||
};
|
||||
}
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
||||
export function dynamicInstantiate(url) {
|
||||
const builtinInstance = module._load(url.substr(5));
|
||||
const builtinInstance = module._load(url);
|
||||
const builtinExports = ['default', ...Object.keys(builtinInstance)];
|
||||
return {
|
||||
exports: builtinExports,
|
||||
|
@ -12,13 +21,3 @@ export function dynamicInstantiate(url) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resolve(specifier, base, defaultResolver) {
|
||||
if (module.builtinModules.includes(specifier)) {
|
||||
return {
|
||||
url: `node:${specifier}`,
|
||||
format: 'dynamic'
|
||||
};
|
||||
}
|
||||
return defaultResolver(specifier, base);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,44 @@
|
|||
import url from 'url';
|
||||
import { URL } from 'url';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import { builtinModules } from 'module';
|
||||
|
||||
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
|
||||
|
||||
const baseURL = new url.URL('file://');
|
||||
const baseURL = new URL('file://');
|
||||
baseURL.pathname = process.cwd() + '/';
|
||||
|
||||
export function resolve(specifier, parentModuleURL = baseURL /*, defaultResolve */) {
|
||||
export function resolve(specifier, { parentURL = baseURL }, defaultResolve) {
|
||||
if (builtinModules.includes(specifier)) {
|
||||
return {
|
||||
url: specifier,
|
||||
format: 'builtin'
|
||||
url: specifier
|
||||
};
|
||||
}
|
||||
if (/^\.{1,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
|
||||
// For node_modules support:
|
||||
// return defaultResolve(specifier, parentModuleURL);
|
||||
// return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
throw new Error(
|
||||
`imports must be URLs or begin with './', or '../'; '${specifier}' does not`);
|
||||
}
|
||||
const resolved = new url.URL(specifier, parentModuleURL);
|
||||
const ext = path.extname(resolved.pathname);
|
||||
const resolved = new URL(specifier, parentURL);
|
||||
return {
|
||||
url: resolved.href
|
||||
};
|
||||
}
|
||||
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
if (builtinModules.includes(url)) {
|
||||
return {
|
||||
format: 'builtin'
|
||||
};
|
||||
}
|
||||
const { pathname } = new URL(url);
|
||||
const ext = path.extname(pathname);
|
||||
if (!JS_EXTENSIONS.has(ext)) {
|
||||
throw new Error(
|
||||
`Cannot load file with non-JavaScript file extension ${ext}.`);
|
||||
}
|
||||
return {
|
||||
url: resolved.href,
|
||||
format: 'module'
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export async function getSource(url, { format }, defaultGetSource) {
|
||||
if (url.endsWith('fixtures/es-modules/message.mjs')) {
|
||||
// Oh, I’ve got that one in my cache!
|
||||
return {
|
||||
source: `export const message = 'Woohoo!'.toUpperCase();`
|
||||
}
|
||||
} else {
|
||||
return defaultGetSource(url, {format}, defaultGetSource);
|
||||
}
|
||||
}
|
|
@ -1,20 +1,9 @@
|
|||
import { URL } from 'url';
|
||||
import { builtinModules } from 'module';
|
||||
|
||||
const baseURL = new URL('file://');
|
||||
baseURL.pathname = process.cwd() + '/';
|
||||
|
||||
export function resolve (specifier, base = baseURL) {
|
||||
if (builtinModules.includes(specifier)) {
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
// Load all .js files as ESM, regardless of package scope
|
||||
if (url.endsWith('.js')) {
|
||||
return {
|
||||
url: specifier,
|
||||
format: 'builtin'
|
||||
};
|
||||
format: 'module'
|
||||
}
|
||||
}
|
||||
// load all dependencies as esm, regardless of file extension
|
||||
const url = new URL(specifier, base).href;
|
||||
return {
|
||||
url,
|
||||
format: 'module'
|
||||
};
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export async function getFormat(url, context, defaultGetFormat) {
|
||||
try {
|
||||
if (new URL(url).pathname.endsWith('.unknown')) {
|
||||
return {
|
||||
format: 'module'
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
|
@ -1,8 +1,17 @@
|
|||
export async function resolve(specifier, parentModuleURL, defaultResolve) {
|
||||
if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
|
||||
export async function resolve(specifier, { parentURL }, defaultResolve) {
|
||||
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
|
||||
return {
|
||||
url: 'file:///asdf'
|
||||
};
|
||||
}
|
||||
return defaultResolve(specifier, parentModuleURL);
|
||||
return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
if (url === 'file:///asdf') {
|
||||
return {
|
||||
format: 'esm'
|
||||
}
|
||||
}
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
/* eslint-disable node-core/required-modules */
|
||||
export async function resolve(specifier, parentModuleURL, defaultResolve) {
|
||||
if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
|
||||
export async function resolve(specifier, { parentURL }, defaultResolve) {
|
||||
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
|
||||
return {
|
||||
url: specifier,
|
||||
format: 'esm'
|
||||
url: specifier
|
||||
};
|
||||
}
|
||||
return defaultResolve(specifier, parentModuleURL);
|
||||
return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {createRequire} from '../../common/index.mjs';
|
|||
const require = createRequire(import.meta.url);
|
||||
const dep = require('./loader-dep.js');
|
||||
|
||||
export function resolve(specifier, base, defaultResolve) {
|
||||
export function resolve(specifier, { parentURL }, defaultResolve) {
|
||||
assert.strictEqual(dep.format, 'module');
|
||||
return defaultResolve(specifier, base);
|
||||
return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
export async function resolve(specifier, parent, defaultResolve) {
|
||||
export async function resolve(specifier, { parentURL }, defaultResolve) {
|
||||
if (specifier === 'unknown-builtin-module') {
|
||||
return { url: 'unknown-builtin-module', format: 'builtin' };
|
||||
return {
|
||||
url: 'unknown-builtin-module'
|
||||
};
|
||||
}
|
||||
return defaultResolve(specifier, parent);
|
||||
}
|
||||
return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
|
||||
export async function getFormat(url, context, defaultGetFormat) {
|
||||
if (url === 'unknown-builtin-module') {
|
||||
return {
|
||||
format: 'builtin'
|
||||
};
|
||||
}
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import {createRequire} from '../../common/index.mjs';
|
|||
const require = createRequire(import.meta.url);
|
||||
const dep = require('./loader-dep.js');
|
||||
|
||||
export function resolve (specifier, base, defaultResolve) {
|
||||
export function resolve (specifier, { parentURL }, defaultResolve) {
|
||||
return {
|
||||
url: defaultResolve(specifier, base).url,
|
||||
url: defaultResolve(specifier, {parentURL}, defaultResolve).url,
|
||||
format: dep.format
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
export function resolve(specifier, parentModule, defaultResolver) {
|
||||
if (specifier !== 'test') {
|
||||
return defaultResolver(specifier, parentModule);
|
||||
export function resolve(specifier, { parentURL }, defaultResolve) {
|
||||
if (specifier === 'test') {
|
||||
return {
|
||||
url: 'file://'
|
||||
};
|
||||
}
|
||||
return { url: 'file://', format: 'dynamic' };
|
||||
return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
|
||||
export function getFormat(url, context, defaultGetFormat) {
|
||||
if (url === 'file://') {
|
||||
return {
|
||||
format: 'dynamic'
|
||||
}
|
||||
}
|
||||
return defaultGetFormat(url, context, defaultGetFormat);
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ import assert from 'assert';
|
|||
// a loader that asserts that the defaultResolve will throw "not found"
|
||||
// (skipping the top-level main of course)
|
||||
let mainLoad = true;
|
||||
export async function resolve (specifier, base, defaultResolve) {
|
||||
export async function resolve(specifier, { parentURL }, defaultResolve) {
|
||||
if (mainLoad) {
|
||||
mainLoad = false;
|
||||
return defaultResolve(specifier, base);
|
||||
return defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
try {
|
||||
await defaultResolve(specifier, base);
|
||||
await defaultResolve(specifier, {parentURL}, defaultResolve);
|
||||
}
|
||||
catch (e) {
|
||||
assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND');
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export async function transformSource(
|
||||
source, { url, format }, defaultTransformSource) {
|
||||
if (source && source.replace) {
|
||||
return {
|
||||
source: source.replace(`'A message';`, `'A message'.toUpperCase();`)
|
||||
};
|
||||
} else { // source could be a buffer, e.g. for WASM
|
||||
return defaultTransformSource(
|
||||
source, {url, format}, defaultTransformSource);
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
throw new Error('NO, NEVER');
|
||||
export default 'unknown';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
(node:*) ExperimentalWarning: The ESM module loader is experimental.
|
||||
(node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
|
||||
internal/modules/esm/default_resolve.js:*
|
||||
internal/modules/esm/resolve.js:*
|
||||
let url = moduleWrapResolve(specifier, parentURL);
|
||||
^
|
||||
|
||||
Error: Cannot find package 'i-dont-exist' imported from *
|
||||
at Loader.resolve [as _resolve] (internal/modules/esm/default_resolve.js:*:*)
|
||||
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*)
|
||||
at Loader.resolve (internal/modules/esm/loader.js:*:*)
|
||||
at Loader.getModuleJob (internal/modules/esm/loader.js:*:*)
|
||||
at Loader.import (internal/modules/esm/loader.js:*:*)
|
||||
|
|
|
@ -50,10 +50,13 @@ const expectedModules = new Set([
|
|||
'NativeModule internal/modules/cjs/helpers',
|
||||
'NativeModule internal/modules/cjs/loader',
|
||||
'NativeModule internal/modules/esm/create_dynamic_module',
|
||||
'NativeModule internal/modules/esm/default_resolve',
|
||||
'NativeModule internal/modules/esm/get_format',
|
||||
'NativeModule internal/modules/esm/get_source',
|
||||
'NativeModule internal/modules/esm/loader',
|
||||
'NativeModule internal/modules/esm/module_job',
|
||||
'NativeModule internal/modules/esm/module_map',
|
||||
'NativeModule internal/modules/esm/resolve',
|
||||
'NativeModule internal/modules/esm/transform_source',
|
||||
'NativeModule internal/modules/esm/translators',
|
||||
'NativeModule internal/process/esm_loader',
|
||||
'NativeModule internal/options',
|
||||
|
|
Loading…
Reference in New Issue