ECMAScript 模块是来打包 JavaScript 代码以供重用的官方标准格式。
模块使用各种 import
和 export
语句定义。
以下是 ES 模块导出函数的示例:
// addTwo.mjs
function addTwo(num) {
return num + 2;
}
export { addTwo };
以下是 ES 模块从 addTwo.mjs
导入函数的示例:
// app.mjs
import { addTwo } from './addTwo.mjs';
// 打印: 6
console.log(addTwo(4));
Node.js 完全支持当前指定的 ECMAScript 模块,并且提供它们与其原始模块格式 CommonJS 之间的互操作性。
Node.js 有两个模块系统:CommonJS 模块和 ECMAScript 模块。
作者可以通过 .mjs
文件扩展名、package.json
"type"
字段、或 --input-type
标志告诉 Node.js 使用 ECMAScript 模块加载器。
在这些情况之外,Node.js 将使用 CommonJS 模块加载器。
参阅确定模块系统了解更多详细信息。
此章节已移至包模块。
import
语句的说明符是 from
关键字之后的字符串,例如 import { sep } from 'path'
中的 'path'
。
说明符也用于 export from
语句,并作为 import()
表达式的参数。
有三种类型的说明符:
相对说明符,如 './startup.js'
或 '../config.mjs'
。
它们指的是相对于导入文件位置的路径。
文件扩展名对于这些始终是必需的。
裸说明符,如 'some-package'
或 'some-package/shuffle'
。
它们可以通过包名称来引用包的主要入口点,或者根据示例分别以包名称为前缀的包中的特定功能模块。
包括文件扩展名仅适用于没有 "exports"
字段的包。
绝对说明符,如 'file:///opt/nodejs/config.js'
。
它们直接且明确地引用完整的路径。
裸说明符解析由 Node.js 模块解析算法处理。 所有其他说明符解析始终仅使用标准的相对网址解析语义进行解析。
就像在 CommonJS 中一样,包中的模块文件可以通过在包名后附加路径来访问,除非包的 package.json
包含 "exports"
字段,在这种情况下,包中的文件只能通过 "exports"
中定义的路径访问。
有关这些适用于 Node.js 模块解析中的裸说明符的包解析规则的详细信息,请参阅包文档。
当使用 import
关键字解析相对或绝对的说明符时,必须提供文件扩展名。
还必须完全指定目录索引(例如 './startup/index.js'
)。
此行为与 import
在浏览器环境中的行为方式相匹配,假设服务器是典型配置的。
ES 模块被解析并缓存为 URL。
这意味着特殊字符必须是百分比编码的,比如使用 %23
的 #
、使用 %3F
的 ?
。
支持 file:
、node:
和 data:
URL 协议。
除非使用自定义的 HTTPS 加载器,否则 Node.js 本身不支持像 'https://example.com/app.js'
这样的说明符
如果用于解析模块的 import
说明符具有不同的查询或片段,则会多次加载模块。
import './foo.mjs?query=1'; // 加载具有 "?query=1" 查询的 ./foo.mjs
import './foo.mjs?query=2'; // 加载具有 "?query=2" 查询的 ./foo.mjs
卷根可以通过 /
、//
或 file:///
引用。
鉴于网址和路径解析的差异(例如百分比编码细节),建议在导入路径时使用 url.pathToFileURL。
data:
URL 支持使用以下 MIME 类型导入:
text/javascript
用于 ES 模块application/json
用于 JSONapplication/wasm
用于 Wasmdata:
URL 只为内置模块解析裸说明符和绝对说明符。
解析相对说明符不起作用,因为 data:
不是特殊协议。
例如,尝试从 data:text/javascript,import "./foo";
加载 ./foo
无法解析,因为 data:
URL 没有相对解析的概念。
正在使用的 data:
URL 示例是:
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"';
支持 node:
URL 作为加载 Node.js 内置模块的替代方法。
此 URL 协议允许有效的绝对的 URL 字符串引用内置模块。
import fs from 'node:fs/promises';
导入断言提案为模块导入语句添加了内联语法,以便在模块说明符旁边传入更多信息。
import fooData from './foo.json' assert { type: 'json' };
const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
Node.js 支持以下 type
值,其断言是强制性的:
断言 type | 用于 |
---|---|
'json' | JSON 模块 |
核心模块提供了其公共 API 的命名导出。
还提供了默认导出,其是 CommonJS 导出的值。
默认导出可用于修改命名导出等。
内置模块的命名导出仅通过调用 module.syncBuiltinESMExports()
进行更新。
import EventEmitter from 'events';
const e = new EventEmitter();
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
});
import fs, { readFileSync } from 'fs';
import { syncBuiltinESMExports } from 'module';
import { Buffer } from 'buffer';
fs.readFileSync = () => Buffer.from('Hello, ESM');
syncBuiltinESMExports();
fs.readFileSync === readFileSync;
动态的 import()
在 CommonJS 和 ES 模块中都受支持。
在 CommonJS 模块中它可以用来加载 ES 模块。
import.meta
#import.meta
元属性是包含以下属性的 Object
。
import.meta.url
#file:
URL。这与提供当前模块文件 URL 的浏览器中的定义完全相同。
这可以启用有用的模式,例如相对文件加载
import { readFileSync } from 'fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
import.meta.resolve(specifier[, parent])
#此特性仅在启用 --experimental-import-meta-resolve
命令标志时可用。
specifier
<string> 相对于 parent
解析的模块说明符。parent
<string> | <URL> 要解析的绝对的父模块 URL。
如果未指定,则使用 import.meta.url
的值作为默认值。提供作用域为每个模块的模块相关解析函数,返回 URL 字符串。
const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
import.meta.resolve
还接受第二个参数,它是从中解析的父模块:
await import.meta.resolve('./dep', import.meta.url);
此函数是异步的,因为 Node.js 中的 ES 模块解析器是允许异步的。
import
语句可以引用 ES 模块或 CommonJS 模块。
import
语句只允许在 ES 模块中使用,但 CommonJS 支持动态 import()
表达式来加载 ES 模块。
当导入 CommonJS 模块 时,提供 module.exports
对象作为默认导出。
命名导出可能可用,由静态分析提供,以方便更好的生态系统兼容性。
require
#CommonJS 模块 require
总是将它引用的文件视为 CommonJS。
不支持使用 require
加载 ES 模块,因为 ES 模块具有异步执行。
而是,使用 import()
从 CommonJS 模块加载 ES 模块。
CommonJS 模块由可以是任何类型的 module.exports
对象组成。
当导入 CommonJS 模块时,可以使用 ES 模块默认导入或其对应的语法糖可靠地导入:
import { default as cjs } from 'cjs';
// 下面的导入语句是上面的导入语句中
// `{ default as cjsSugar }` 的 "语法糖"(等价但更甜):
import cjsSugar from 'cjs';
console.log(cjs);
console.log(cjs === cjsSugar);
// 打印:
// <module.exports>
// true
CommonJS 模块的 ECMAScript 模块命名空间表示始终是使用 default
导出键指向 CommonJS module.exports
值的命名空间。
当使用 import * as m from 'cjs'
或动态导入时,可以直接观察到此模块命名空间外来对象:
import * as m from 'cjs';
console.log(m);
console.log(m === await import('cjs'));
// 打印:
// [Module] { default: <module.exports> }
// true
为了更好地兼容 JS 生态系统中的现有用法,Node.js 还尝试确定每个导入的 CommonJS 模块的 CommonJS 命名导出,以使用静态分析过程将它们作为单独的 ES 模块导出提供。
例如,考虑编写的 CommonJS 模块:
// cjs.cjs
exports.name = 'exported';
前面的模块支持 ES 模块中的命名导入:
import { name } from './cjs.cjs';
console.log(name);
// 打印: 'exported'
import cjs from './cjs.cjs';
console.log(cjs);
// 打印: { name: 'exported' }
import * as m from './cjs.cjs';
console.log(m);
// 打印: [Module] { default: { name: 'exported' }, name: 'exported' }
从上一个记录模块命名空间外来对象的示例中可以看出,name
导出是从 module.exports
对象复制出来的,并在导入模块时直接设置在 ES 模块命名空间上。
未检测到这些命名导出的实时绑定更新或添加到 module.exports
的新导出。
命名导出的检测基于通用语法模式,但并不总是正确地检测命名导出。 在这些情况下,使用上述默认导入形式可能是更好的选择。
命名导出检测涵盖了许多常见的导出模式、再导出模式、以及构建工具和转译器输出。 参阅 cjs-module-lexer 以了解实现的确切语义。
在大多数情况下,可以使用 ES 模块 import
加载 CommonJS 模块。
如果需要,可以使用 module.createRequire()
在 ES 模块中构造 require
函数。
这些 CommonJS 变量在 ES 模块中不可用。
__filename
和 __dirname
用例可以通过 import.meta.url
复制。
ES 模块导入当前不支持原生模块。
它们可以改为加载 module.createRequire()
或 process.dlopen
。
相对解析可以通过 new URL('./local', import.meta.url)
处理。
对于完整的 require.resolve
替换,有标记的实验性 import.meta.resolve
API。
也可以使用 module.createRequire()
。
NODE_PATH
不是解析 import
说明符的一部分。
如果需要这种行为,则使用符号链接。
require.extensions
没有被 import
使用。
期望加载器钩子在未来可以提供这个工作流。
require.cache
没有被 import
使用,因为 ES 模块加载器有自己独立的缓存。
import
可以引用 JSON 文件:
import packageConfig from './package.json' assert { type: 'json' };
assert { type: 'json' }
语法是强制性的;参见导入断言。
导入的 JSON 只暴露一个 default
导出。
不支持命名导出。
在 CommonJS 缓存中创建缓存条目,以避免重复。
如果 JSON 模块已经从同一路径导入,则在 CommonJS 中返回相同的对象。
在 --experimental-wasm-modules
标志下支持导入 WebAssembly 模块,允许将任何 .wasm
文件作为普通模块导入,同时也支持它们的模块导入。
此集成符合 WebAssembly 的 ES 模块集成提案。
例如,index.mjs
包含:
import * as M from './module.wasm';
console.log(M);
在以下条件下执行:
node --experimental-wasm-modules index.mjs
将为 module.wasm
的实例化提供导出接口。
await
关键字可以用在 ECMAScript 模块的顶层主体中。
假设 a.mjs
具有
export const five = await Promise.resolve(5);
并且 b.mjs
具有
import { five } from './a.mjs';
console.log(five); // 记录 `5`
node b.mjs # 有效
如果顶层 await
表达式永远无法解析,则 node
进程将以 13
状态码退出。
import { spawn } from 'child_process';
import { execPath } from 'process';
spawn(execPath, [
'--input-type=module',
'--eval',
// 永不解决的 Promise:
'await new Promise(() => {})',
]).once('exit', (code) => {
console.log(code); // 记录 `13`
});
在 --experimental-network-imports
标志下支持使用 https:
和 http:
导入基于网络的模块。
这允许类似网络浏览器的导入在 Node.js 中工作,但由于应用程序稳定性和安全问题在特权环境而不是浏览器沙箱中运行时会有所不同,因此存在一些差异。
尚不支持 HTTP/2 和 HTTP/3 的自动协议协商。
http:
易受中间人攻击,不允许用于 IPv4 地址 127.0.0.0/8
(127.0.0.1
到 127.255.255.255
)和 IPv6 地址 ::1
之外的地址。
对 http:
的支持旨在用于本地开发。
Authorization
、Cookie
和 Proxy-Authorization
标头未发送到服务器。
避免在部分导入的 URL 中包含用户信息。
正在研究在服务器上安全使用这些的安全模型。
CORS 旨在允许服务器将 API 的使用者限制为一组特定的主机。 这不受支持,因为它对于基于服务器的实现没有意义。
这些模块不能访问不超过 http:
或 https:
的其他模块。
要在避免安全问题的同时仍然访问本地模块,则传入对本地依赖项的引用:
// file.mjs
import worker_threads from 'worker_threads';
import { configure, resize } from 'https://example.com/imagelib.mjs';
configure({ worker_threads });
// https://example.com/imagelib.mjs
let worker_threads;
export function configure(opts) {
worker_threads = opts.worker_threads;
}
export function resize(img, size) {
// 在工作线程中调整大小以避免主线程阻塞
}
目前,需要 --experimental-network-imports
标志来启用通过 http:
或 https:
加载资源。
将来,将使用不同的机制来执行此操作。
需要选择加入以防止不经意间使用可能影响 Node.js 应用程序可靠性的潜在可变状态的传递依赖关系。
此 API 目前正在重新设计,并将继续更改。
要自定义默认的模块解析,则可以选择通过 Node.js 的 --experimental-loader ./loader-name.mjs
参数提供加载器钩子。
当使用钩子时,它们适用于入口点和所有 import
调用。
它们不适用于 require
调用;那些仍然遵循 CommonJS 规则。
resolve(specifier, context, defaultResolve)
#加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。
specifier
<string>context
<Object>
conditions
<string[]>importAssertions
<Object>parentURL
<string> | <undefined>defaultResolve
<Function> Node.js 默认解析器。format
<string> | <null> | <undefined>
'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'
url
<string> 导入目标的绝对网址(例如 file://…
)resolve
钩子返回给定模块说明符和父 URL 的解析文件 URL,以及可选的格式(例如 'module'
)作为对 load
钩子的提示。
如果指定了格式,则 load
钩子最终负责提供最终的 format
值(可以随意忽略 resolve
提供的提示);如果 resolve
提供了 format
,则需要自定义 load
钩子,即使只是通过 Node.js 默认 load
钩子的值。
模块说明符是 import
语句或 import()
表达式中的字符串,父 URL 是导入此模块的 URL,如果这是应用程序的主要入口点,则为 undefined
context
中的 conditions
属性是适用于此解析请求的包导出条件的条件数组。
它们可用于在别处查找条件映射或在调用默认解析逻辑时修改列表。
当前的包导出条件始终在传入钩子的 context.conditions
数组中。
为了在调用 defaultResolve
时保证默认的 Node.js 模块说明符解析行为,传给它的 context.conditions
数组必须包含最初传到 resolve
钩子的 context.conditions
数组的所有元素。
/**
* @param {string} specifier
* @param {{
* conditions: string[],
* parentURL: string | undefined,
* }} context
* @param {Function} defaultResolve
* @returns {Promise<{ url: string }>}
*/
export async function resolve(specifier, context, defaultResolve) {
const { parentURL = null } = context;
if (Math.random() > 0.5) { // 一些条件。
// 对于部分或全部说明符,做一些自定义逻辑来解决。
// 总是返回 {url: <string>} 形式的对象。
return {
url: parentURL ?
new URL(specifier, parentURL).href :
new URL(specifier).href,
};
}
if (Math.random() < 0.5) { // 另一个条件。
// 当调用 `defaultResolve` 时,可以修改参数。
// 在这种情况下,它为匹配条件导出添加了另一个值。
return defaultResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
// 对于所有其他说明符,请遵循 Node.js。
return defaultResolve(specifier, context, defaultResolve);
}
load(url, context, defaultLoad)
#加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。
在此 API 的先前版本中,它被拆分为 3 个单独的、现已弃用的钩子(
getFormat
、getSource
和transformSource
)。
url
<string>context
<Object>
format
<string> | <null> | <undefined> resolve
钩子可选地提供的格式。importAssertions
<Object>defaultLoad
<Function>format
<string>source
<string> | <ArrayBuffer> | <TypedArray>load
钩子提供了一种方式来定义确定网址应如何解释、检索、以及解析的自定义方法。
它还负责验证导入断言。
format
的最终值必须是以下之一:
format | 描述 | Acceptable types for source returned by load |
---|---|---|
'builtin' | 加载 Node.js 内置模块 | 不适用 |
'commonjs' | 加载 Node.js CommonJS 模块 | 不适用 |
'json' | 加载 JSON 文件 | { string , ArrayBuffer , TypedArray } |
'module' | 加载 ES 模块 | { string , ArrayBuffer , TypedArray } |
'wasm' | 加载 WebAssembly 模块 | { ArrayBuffer , TypedArray } |
source
的值对于类型 'builtin'
被忽略,因为目前无法替换 Node.js 内置(核心)模块的值。
source
的值对于类型 'commonjs'
被忽略,因为 CommonJS 模块加载器没有为 ES 模块加载器提供覆盖 CommonJS 模块返回值 的机制。
这个限制将来可能会被克服。
警告:ESM
load
钩子和来自 CommonJS 模块的命名空间导出不兼容。 尝试将它们一起使用将导致导入中的空对象。 这可能会在未来得到解决。
这些类型都对应于 ECMAScript 中定义的类。
ArrayBuffer
对象是 SharedArrayBuffer
。TypedArray
对象是 Uint8Array
。如果基于文本的格式(即 'json'
、'module'
)的源值不是字符串,则使用 util.TextDecoder
将其转换为字符串。
load
钩子提供了一种方法来定义用于检索 ES 模块说明符的源代码的自定义方法。
这将允许加载器潜在地避免从磁盘读取文件。
它还可以用于将无法识别的格式映射到支持的格式,例如 yaml
到 module
。
/**
* @param {string} url
* @param {{
format: string,
}} context If resolve settled with a `format`, that value is included here.
* @param {Function} defaultLoad
* @returns {Promise<{
format: string,
source: string | ArrayBuffer | SharedArrayBuffer | Uint8Array,
}>}
*/
export async function load(url, context, defaultLoad) {
const { format } = context;
if (Math.random() > 0.5) { // 一些条件。
/*
For some or all URLs, do some custom logic for retrieving the source.
Always return an object of the form {
format: <string>,
source: <string|buffer>,
}.
*/
return {
format,
source: '...',
};
}
// 所有其他 URL 都遵循 Node.js。
return defaultLoad(url, context, defaultLoad);
}
在更高级的场景中,这也可用于将不受支持的源转换为受支持的源(请参阅下面的示例)。
globalPreload()
#加载器 API 正在重新设计。 这个钩子可能会消失,或者它的签名可能会改变。 不要依赖下面描述的 API。
在此 API 的先前版本中,此钩子被命名为
getGlobalPreloadCode
。
有时可能需要在应用程序运行所在的同一全局范围内运行一些代码。 此钩子允许返回在启动时作为宽松模式脚本运行的字符串。
类似于 CommonJS 封装器的工作方式,代码在隐式函数范围内运行。
唯一的参数是类似 require
的函数,可用于加载内置函数,如 "fs":getBuiltin(request: string)
。
如果代码需要更高级的 require
特性,则必须使用 module.createRequire()
构建自己的 require
。
/**
* @param {{
port: MessagePort,
}} utilities Things that preload code might find useful
* @returns {string} 在应用程序启动之前运行的代码
*/
export function globalPreload(utilities) {
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');
const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');
const require = createRequire(cwd() + '/<preload>');
// [...]
`;
}
为了允许应用程序和加载程序之间进行通信,预加载代码中提供了另一个参数:port
。
这可以作为加载器钩子的参数和钩子返回的源文本内部。
必须注意正确调用 port.ref()
和 port.unref()
以防止进程处于无法正常关闭的状态。
/**
* This example has the application context send a message to the loader
* and sends the message back to the application context
* @param {{
port: MessagePort,
}} utilities Things that preload code might find useful
* @returns {string} 在应用程序启动之前运行的代码
*/
export function globalPreload({ port }) {
port.onmessage = (evt) => {
port.postMessage(evt.data);
};
return `\
port.postMessage('console.log("I went to the Loader and back");');
port.onmessage = (evt) => {
eval(evt.data);
};
`;
}
各种加载器钩子可以一起使用来完成对 Node.js 代码加载和评估行为的广泛定制。
在当前的 Node.js 中,不支持以 https://
开头的说明符。
下面的加载器注册钩子以启用对此类说明符的基本支持。
虽然这似乎是对 Node.js 核心功能的重大改进,但实际使用这个加载器有很大的缺点:性能比从磁盘加载文件慢得多,没有缓存,也没有安全性。
// https-loader.mjs
import { get } from 'https';
export function resolve(specifier, context, defaultResolve) {
const { parentURL = null } = context;
// 通常,Node.js 会在以 'https://' 开头的说明符上出错,
// 因此此钩子会拦截它们并将它们转换为绝对 URL,
// 以便传给下面的后面的钩子。
if (specifier.startsWith('https://')) {
return {
url: specifier
};
} else if (parentURL && parentURL.startsWith('https://')) {
return {
url: new URL(specifier, parentURL).href
};
}
// 让 Node.js 处理所有其他说明符。
return defaultResolve(specifier, context, defaultResolve);
}
export function load(url, context, defaultLoad) {
// 要通过网络加载 JavaScript,
// 则需要获取并返回它。
if (url.startsWith('https://')) {
return new Promise((resolve, reject) => {
get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve({
// 本示例假设所有网络提供的 JavaScript
// 都是 ES 模块代码。
format: 'module',
source: data,
}));
}).on('error', (err) => reject(err));
});
}
// 让 Node.js 处理所有其他 URL。
return defaultLoad(url, context, defaultLoad);
}
// main.mjs
import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
console.log(VERSION);
使用前面的加载器,运行 node --experimental-loader ./https-loader.mjs ./main.mjs
会在 main.mjs
中的 URL 处按照模块打印当前版本的 CoffeeScript。
可以使用 load
钩子将 Node.js 无法理解的格式的源转换为 JavaScript。
但是,在调用该钩子之前,resolve
钩子需要告诉 Node.js 不要在未知文件类型上抛出错误。
这比在运行 Node.js 之前转译源文件的性能要低;转译加载器应该只用于开发和测试目的。
// coffeescript-loader.mjs
import { readFile } from 'node:fs/promises';
import { dirname, extname, resolve as resolvePath } from 'node:path';
import { cwd } from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import CoffeeScript from 'coffeescript';
const baseURL = pathToFileURL(`${cwd()}/`).href;
// CoffeeScript 文件以 .coffee、.litcoffee 或 .coffee.md 结尾。
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
export async function resolve(specifier, context, defaultResolve) {
const { parentURL = baseURL } = context;
// Node.js 通常在未知文件扩展名上出错,
// 因此返回以 CoffeeScript 文件扩展名结尾的说明符的 URL。
if (extensionsRegex.test(specifier)) {
return {
url: new URL(specifier, parentURL).href
};
}
// 让 Node.js 处理所有其他说明符。
return defaultResolve(specifier, context, defaultResolve);
}
export async function load(url, context, defaultLoad) {
// 现在修补了解决以让 CoffeeScript URL 通过,
// 需要告诉 Node.js 这样的 URL 应该被解释为什么格式。
// 为了这个加载器的目的,所有 CoffeeScript URL 都是 ES 模块。
// 因为 CoffeeScript 会转译成 JavaScript,
// 所以它应该是两种 JavaScript 格式之一:'commonjs' 或 'module'。
if (extensionsRegex.test(url)) {
// CoffeeScript 文件可以是 CommonJS 或 ES 模块,
// 因此我们希望 Node.js 将任何 CoffeeScript 文件视为相同位置的 .js 文件。
// 要确定 Node.js 如何解释任意 .js 文件,
// 则在文件系统中搜索最近的父 package.json 文件
// 并读取其 "type" 字段。
const format = await getPackageType(url);
// 当钩子返回 'commonjs' 格式时,则 `source` 将被忽略。
// 为了处理 CommonJS 文件,需要使用 `require.extensions` 注册句柄,
// 以便使用 CommonJS 加载器处理文件。
// 避免需要单独的 CommonJS 处理程序
// 是 ES 模块加载器计划的未来增强功能。
if (format === 'commonjs') {
return { format };
}
const { source: rawSource } = await defaultLoad(url, { format });
// 此钩子将所有导入的 CoffeeScript 文件的 CoffeeScript 源代码
// 转换为的 JavaScript 源代码。
const transformedSource = CoffeeScript.compile(rawSource.toString(), {
bare: true,
filename: url,
});
return {
format,
source: transformedSource,
};
}
// 让 Node.js 处理所有其他 URL。
return defaultLoad(url, context, defaultLoad);
}
async function getPackageType(url) {
// `url` is only a file path during the first iteration when passed the
// resolved url from the load() hook
// an actual file path from load() will contain a file extension as it's
// required by the spec
// this simple truthy check for whether `url` contains a file extension will
// work for most projects but does not cover some edge-cases (such as
// extensionless files or a url ending in a trailing space)
const isFilePath = !!extname(url);
// 如果是文件路径,则获取它所在的目录
const dir = isFilePath ?
dirname(fileURLToPath(url)) :
url;
// 生成同一个目录下的 package.json 的文件路径,
// 文件可能存在也可能不存在
const packagePath = resolvePath(dir, 'package.json');
// 尝试读取可能不存在的 package.json
const type = await readFile(packagePath, { encoding: 'utf8' })
.then((filestring) => JSON.parse(filestring).type)
.catch((err) => {
if (err?.code !== 'ENOENT') console.error(err);
});
// 如果 package.json 存在并包含带有值的 `type` 字段
if (type) return type;
// 否则,(如果不在根目录下)继续检查下一个目录
// 如果在根目录,则停止并返回 false
return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
}
# 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}"
# scream.coffee
export scream = (str) -> str.toUpperCase()
使用前面的加载器,运行 node --experimental-loader ./coffeescript-loader.mjs main.coffee
会导致 main.coffee
在其源代码从磁盘加载之后但在 Node.js 执行之前转换为 JavaScript;对于通过任何加载文件的 import
语句引用的任何 .coffee
、.litcoffee
或 .coffee.md
文件,依此类推。
解析器具有以下属性:
加载 ES 模块说明符的算法通过下面的 ESM_RESOLVE 方法给出。 它返回相对于 parentURL 的模块说明符的解析 URL。
确定解析 URL 的模块格式的算法由 ESM_FORMAT 提供,它返回任何文件的唯一模块格式。 "module" 格式为 ECMAScript 模块返回,而 "commonjs" 格式用于指示通过旧版 CommonJS 加载器加载。 其他格式,如 "addon" 可以在未来的更新中扩展。
在以下算法中,除非另有说明,否则所有子程序错误都将作为这些顶层程序的错误传播。
defaultConditions 是条件环境名称数组,["node", "import"]
。
解析器可能会抛出以下错误:
ESM_RESOLVE(specifier, parentURL)
- Let resolved be undefined.
- If specifier is a valid URL, then
- Set resolved to the result of parsing and reserializing specifier as a URL.
- Otherwise, if specifier starts with "/", "./" or "../", then
- Set resolved to the URL resolution of specifier relative to parentURL.
- Otherwise, if specifier starts with "#", then
- Set resolved to the destructured value of the result of PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, defaultConditions).
- Otherwise,
- Note: specifier is now a bare specifier.
- Set resolved the result of PACKAGE_RESOLVE(specifier, parentURL).
- Let format be undefined.
- If resolved is a "file:" URL, then
- If resolved contains any percent encodings of "/" or "\" ("%2F" and "%5C" respectively), then
- Throw an Invalid Module Specifier error.
- If the file at resolved is a directory, then
- Throw an Unsupported Directory Import error.
- If the file at resolved does not exist, then
- Throw a Module Not Found error.
- Set resolved to the real path of resolved, maintaining the same URL querystring and fragment components.
- Set format to the result of ESM_FILE_FORMAT(resolved).
- Otherwise,
- Set format the module format of the content type associated with the URL resolved.
- Load resolved as module format, format.
PACKAGE_RESOLVE(packageSpecifier, parentURL)
- Let packageName be undefined.
- If packageSpecifier is an empty string, then
- Throw an Invalid Module Specifier error.
- If packageSpecifier is a Node.js builtin module name, then
- Return the string "node:" concatenated with packageSpecifier.
- If packageSpecifier does not start with "@", then
- Set packageName to the substring of packageSpecifier until the first "/" separator or the end of the string.
- Otherwise,
- If packageSpecifier does not contain a "/" separator, then
- Throw an Invalid Module Specifier error.
- Set packageName to the substring of packageSpecifier until the second "/" separator or the end of the string.
- If packageName starts with "." or contains "\" or "%", then
- Throw an Invalid Module Specifier error.
- Let packageSubpath be "." concatenated with the substring of packageSpecifier from the position at the length of packageName.
- If packageSubpath ends in "/", then
- Throw an Invalid Module Specifier error.
- Let selfUrl be the result of PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL).
- If selfUrl is not undefined, return selfUrl.
- While parentURL is not the file system root,
- Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL.
- Set parentURL to the parent folder URL of parentURL.
- If the folder at packageURL does not exist, then
- Continue the next loop iteration.
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson is not null and pjson.exports is not null or undefined, then
- Return the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- Otherwise, if packageSubpath is equal to ".", then
- If pjson.main is a string, then
- Return the URL resolution of main in packageURL.
- Otherwise,
- Return the URL resolution of packageSubpath in packageURL.
- Throw a Module Not Found error.
PACKAGE_SELF_RESOLVE(packageName, packageSubpath, parentURL)
- Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
- If packageURL is null, then
- Return undefined.
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson is null or if pjson.exports is null or undefined, then
- Return undefined.
- If pjson.name is equal to packageName, then
- Return the resolved destructured value of the result of PACKAGE_EXPORTS_RESOLVE(packageURL, packageSubpath, pjson.exports, defaultConditions).
- Otherwise, return undefined.
PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions)
- If exports is an Object with both a key starting with "." and a key not starting with ".", throw an Invalid Package Configuration error.
- If subpath is equal to ".", then
- Let mainExport be undefined.
- If exports is a String or Array, or an Object containing no keys starting with ".", then
- Set mainExport to exports.
- Otherwise if exports is an Object containing a "." property, then
- Set mainExport to exports["."].
- If mainExport is not undefined, then
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, mainExport, "", false, false, conditions).
- If resolved is not null or undefined, then
- Return resolved.
- Otherwise, if exports is an Object and all keys of exports start with ".", then
- Let matchKey be the string "./" concatenated with subpath.
- Let resolvedMatch be result of PACKAGE_IMPORTS_EXPORTS_RESOLVE( matchKey, exports, packageURL, false, conditions).
- If resolvedMatch.resolve is not null or undefined, then
- Return resolvedMatch.
- Throw a Package Path Not Exported error.
PACKAGE_IMPORTS_RESOLVE(specifier, parentURL, conditions)
- Assert: specifier begins with "#".
- If specifier is exactly equal to "#" or starts with "#/", then
- Throw an Invalid Module Specifier error.
- Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(parentURL).
- If packageURL is not null, then
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson.imports is a non-null Object, then
- Let resolvedMatch be the result of PACKAGE_IMPORTS_EXPORTS_RESOLVE(specifier, pjson.imports, packageURL, true, conditions).
- If resolvedMatch.resolve is not null or undefined, then
- Return resolvedMatch.
- Throw a Package Import Not Defined error.
PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions)
- If matchKey is a key of matchObj and does not end in "/" or contain "*", then
- Let target be the value of matchObj[matchKey].
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, target, "", false, isImports, conditions).
- Return the object { resolved, exact: true }.
- Let expansionKeys be the list of keys of matchObj either ending in "/" or containing only a single "*", sorted by the sorting function PATTERN_KEY_COMPARE which orders in descending order of specificity.
- For each key expansionKey in expansionKeys, do
- Let patternBase be null.
- If expansionKey contains "*", set patternBase to the substring of expansionKey up to but excluding the first "*" character.
- If patternBase is not null and matchKey starts with but is not equal to patternBase, then
- If matchKey ends with "/", throw an Invalid Module Specifier error.
- Let patternTrailer be the substring of expansionKey from the index after the first "*" character.
- If patternTrailer has zero length, or if matchKey ends with patternTrailer and the length of matchKey is greater than or equal to the length of expansionKey, then
- Let target be the value of matchObj[expansionKey].
- Let subpath be the substring of matchKey starting at the index of the length of patternBase up to the length of matchKey minus the length of patternTrailer.
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, true, isImports, conditions).
- Return the object { resolved, exact: true }.
- Otherwise if patternBase is null and matchKey starts with expansionKey, then
- Let target be the value of matchObj[expansionKey].
- Let subpath be the substring of matchKey starting at the index of the length of expansionKey.
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, target, subpath, false, isImports, conditions).
- Return the object { resolved, exact: false }.
- Return the object { resolved: null, exact: true }.
PATTERN_KEY_COMPARE(keyA, keyB)
- Assert: keyA ends with "/" or contains only a single "*".
- Assert: keyB ends with "/" or contains only a single "*".
- Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise.
- Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise.
- If baseLengthA is greater than baseLengthB, return -1.
- If baseLengthB is greater than baseLengthA, return 1.
- If keyA does not contain "*", return 1.
- If keyB does not contain "*", return -1.
- If the length of keyA is greater than the length of keyB, return -1.
- If the length of keyB is greater than the length of keyA, return 1.
- Return 0.
PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions)
- If target is a String, then
- If pattern is false, subpath has non-zero length and target does not end with "/", throw an Invalid Module Specifier error.
- If target does not start with "./", then
- If internal is true and target does not start with "../" or "/" and is not a valid URL, then
- If pattern is true, then
- Return PACKAGE_RESOLVE(target with every instance of "*" replaced by subpath, packageURL + "/").
- Return PACKAGE_RESOLVE(target + subpath, packageURL + "/").
- Otherwise, throw an Invalid Package Target error.
- If target split on "/" or "\" contains any ".", ".." or "node_modules" segments after the first segment, case insensitive and including percent encoded variants, throw an Invalid Package Target error.
- Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
- Assert: resolvedTarget is contained in packageURL.
- If subpath split on "/" or "\" contains any ".", ".." or "node_modules" segments, case insensitive and including percent encoded variants, throw an Invalid Module Specifier error.
- If pattern is true, then
- Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
- Otherwise,
- Return the URL resolution of the concatenation of subpath and resolvedTarget.
- Otherwise, if target is a non-null Object, then
- If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error.
- For each property p of target, in object insertion order as,
- If p equals "default" or conditions contains an entry for p, then
- Let targetValue be the value of the p property in target.
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, subpath, pattern, internal, conditions).
- If resolved is equal to undefined, continue the loop.
- Return resolved.
- Return undefined.
- Otherwise, if target is an Array, then
- If _target.length is zero, return null.
- For each item targetValue in target, do
- Let resolved be the result of PACKAGE_TARGET_RESOLVE( packageURL, targetValue, subpath, pattern, internal, conditions), continuing the loop on any Invalid Package Target error.
- If resolved is undefined, continue the loop.
- Return resolved.
- Return or throw the last fallback resolution null return or error.
- Otherwise, if target is null, return null.
- Otherwise throw an Invalid Package Target error.
ESM_FILE_FORMAT(url)
- Assert: url corresponds to an existing file.
- If url ends in ".mjs", then
- Return "module".
- If url ends in ".cjs", then
- Return "commonjs".
- If url ends in ".json", then
- Return "json".
- Let packageURL be the result of LOOKUP_PACKAGE_SCOPE(url).
- Let pjson be the result of READ_PACKAGE_JSON(packageURL).
- If pjson?.type exists and is "module", then
- If url ends in ".js", then
- Return "module".
- Throw an Unsupported File Extension error.
- Otherwise,
- Throw an Unsupported File Extension error.
LOOKUP_PACKAGE_SCOPE(url)
- Let scopeURL be url.
- While scopeURL is not the file system root,
- Set scopeURL to the parent URL of scopeURL.
- If scopeURL ends in a "node_modules" path segment, return null.
- Let pjsonURL be the resolution of "package.json" within scopeURL.
- if the file at pjsonURL exists, then
- Return scopeURL.
- Return null.
READ_PACKAGE_JSON(packageURL)
- Let pjsonURL be the resolution of "package.json" within packageURL.
- If the file at pjsonURL does not exist, then
- Return null.
- If the file at packageURL does not parse as valid JSON, then
- Throw an Invalid Package Configuration error.
- Return the parsed JSON source of the file at pjsonURL.
不要依赖此标志。我们计划在加载器 API 发展到可以通过自定义加载器实现等效功能时将其删除。
当前的说明符解析不支持 CommonJS 加载器的所有默认行为。 行为差异之一是文件扩展名的自动解析以及导入具有索引文件的目录的能力。
--experimental-specifier-resolution=[mode]
标志可用于自定义扩展解析算法。
默认模式是 explicit
,这需要向加载器提供模块的完整路径。
要启用自动扩展解析并从包含索引文件的目录导入,则使用 node
模式。
$ node index.mjs
success!
$ node index # 失败!
Error: Cannot find module
$ node --experimental-specifier-resolution=node index
success!