mlly
Node.js 缺失的 ECMAScript 模块工具
适用于 Node.js 的缺失 ECMAScript 模块 工具
虽然 ESM 模块在 Node.js 生态系统中不断发展,但仍有许多所需功能仍处于实验阶段、缺失或需要支持 ESM。此包旨在填补这些空白。
用法
安装 npm 包
# using yarn
yarn add mlly
# using npm
npm install mlly
注意:推荐使用 Node.js 14+。
导入工具
// ESM
import {} from "mlly";
// CommonJS
const {} = require("mlly");
解析 ESM 模块
使 ESM 解析更简单的几个工具
- 遵循 ECMAScript 解析器算法
- 从 Node.js 实现中公开
- Windows 路径已规范化
- 支持自定义
extensions
和/index
解析 - 支持自定义
conditions
- 支持从多个路径或 URL 进行解析
resolve
/ resolveSync
通过遵循 ECMAScript 解析器算法 来解析模块(使用 wooorm/import-meta-resolve)。
此外,还支持在没有扩展名和 /index
的情况下进行解析,类似于 CommonJS。
import { resolve, resolveSync } from "mlly";
// file:///home/user/project/module.mjs
console.log(await resolve("./module.mjs", { url: import.meta.url }));
解析选项
url
: 用于解析的 URL 或字符串(默认为pwd()
)conditions
: 用于解析算法的条件数组(默认为['node', 'import']
)extensions
: 如果导入失败要检查的其他扩展名数组(默认为['.mjs', '.cjs', '.js', '.json']
)
resolvePath
/ resolvePathSync
类似于 resolve
,但使用 fileURLToPath
返回路径而不是 URL。
import { resolvePath, resolveSync } from "mlly";
// /home/user/project/module.mjs
console.log(await resolvePath("./module.mjs", { url: import.meta.url }));
createResolve
使用默认值创建一个 resolve
函数。
import { createResolve } from "mlly";
const _resolve = createResolve({ url: import.meta.url });
// file:///home/user/project/module.mjs
console.log(await _resolve("./module.mjs"));
示例:Ponyfill import.meta.resolve
import { createResolve } from "mlly";
import.meta.resolve = createResolve({ url: import.meta.url });
resolveImports
将所有带有相对路径的静态和动态导入解析为完整解析路径。
import { resolveImports } from "mlly";
// import foo from 'file:///home/user/project/bar.mjs'
console.log(
await resolveImports(`import foo from './bar.mjs'`, { url: import.meta.url }),
);
语法分析
isValidNodeImport
通过各种语法检测和启发式方法,此方法可以确定导入是否为有效导入,以便在触发错误之前使用动态 import()
进行导入!
当结果为 false
时,我们通常需要创建一个 CommonJS require 上下文或向打包器添加特定规则以转换依赖项。
import { isValidNodeImport } from "mlly";
// If returns true, we are safe to use `import('some-lib')`
await isValidNodeImport("some-lib", {});
算法
- 检查导入协议 - 如果是
data:
则返回true
(✅ 有效) - 如果不是node:
、file:
或data:
,则返回false
( ❌ 无效) - 使用 Node.js 解析算法 解析导入的完整路径
- 检查完整路径扩展名
- 如果是
.mjs
,.cjs
,.node
或.wasm
,则返回true
(✅ 有效) - 如果不是
.js
,则返回false
(❌ 无效) - 如果匹配已知的混合语法(
.esm.js
,.es.js
等),则返回false
( ❌ 无效)
- 如果是
- 读取解析路径最近的
package.json
文件 - 如果设置了
type: 'module'
字段,则返回true
(✅ 有效) - 读取解析路径的源代码
- 尝试检测 CommonJS 语法使用情况
- 如果是,则返回
true
(✅ 有效)
- 如果是,则返回
- 尝试检测 ESM 语法使用情况
- 如果是,则返回
false
( ❌ 无效)
- 如果是,则返回
注意事项
- 可能仍有一些算法无法涵盖的边缘情况。它的设计是尽力而为的。
- 此方法还允许动态导入 CommonJS 库,考虑到 Node.js 具有 CommonJS 互操作性。
hasESMSyntax
检测代码是否使用了 ESM 语法(静态 import
, ESM export
和 import.meta
使用)
import { hasESMSyntax } from "mlly";
hasESMSyntax("export default foo = 123"); // true
hasCJSSyntax
检测代码是否使用了 CommonJS 语法(exports
, module.exports
, require
和 global
使用)
import { hasCJSSyntax } from "mlly";
hasCJSSyntax("export default foo = 123"); // false
detectSyntax
针对 CJS 和 ESM 测试代码。
isMixed
表示两者都被检测到!这在遗留包中很常见,它们导出的 ESM 语法是半兼容的,旨在供打包器使用。
import { detectSyntax } from "mlly";
// { hasESM: true, hasCJS: true, isMixed: true }
detectSyntax('export default require("lodash")');
CommonJS 上下文
createCommonJS
此工具创建一个兼容的 CommonJS 上下文,而这在 ECMAScript 模块中是缺失的。
import { createCommonJS } from "mlly";
const { __dirname, __filename, require } = createCommonJS(import.meta.url);
注意:require
和 require.resolve
的实现是惰性函数。createRequire
将在首次使用时调用。
导入/导出分析
用于快速分析 ESM 语法并提取静态 import
/export
的工具
- 超快速的基于正则表达式的实现
- 处理大多数边缘情况
- 查找所有静态 ESM 导入
- 查找所有动态 ESM 导入
- 解析静态导入语句
- 查找所有命名、声明和默认导出
findStaticImports
查找所有静态 ESM 导入。
示例
import { findStaticImports } from "mlly";
console.log(
findStaticImports(`
// Empty line
import foo, { bar /* foo */ } from 'baz'
`),
);
输出
[
{
type: "static",
imports: "foo, { bar /* foo */ } ",
specifier: "baz",
code: "import foo, { bar /* foo */ } from 'baz'",
start: 15,
end: 55,
},
];
parseStaticImport
解析先前由 findStaticImports
匹配的动态 ESM 导入语句。
示例
import { findStaticImports, parseStaticImport } from "mlly";
const [match0] = findStaticImports(`import baz, { x, y as z } from 'baz'`);
console.log(parseStaticImport(match0));
输出
{
type: 'static',
imports: 'baz, { x, y as z } ',
specifier: 'baz',
code: "import baz, { x, y as z } from 'baz'",
start: 0,
end: 36,
defaultImport: 'baz',
namespacedImport: undefined,
namedImports: { x: 'x', y: 'z' }
}
findDynamicImports
查找所有动态 ESM 导入。
示例
import { findDynamicImports } from "mlly";
console.log(
findDynamicImports(`
const foo = await import('bar')
`),
);
findExports
import { findExports } from "mlly";
console.log(
findExports(`
export const foo = 'bar'
export { bar, baz }
export default something
`),
);
输出
[
{
type: "declaration",
declaration: "const",
name: "foo",
code: "export const foo",
start: 1,
end: 17,
},
{
type: "named",
exports: " bar, baz ",
code: "export { bar, baz }",
start: 26,
end: 45,
names: ["bar", "baz"],
},
{ type: "default", code: "export default ", start: 46, end: 61 },
];
findExportNames
与 findExports
相同,但返回导出名称数组。
import { findExportNames } from "mlly";
// [ "foo", "bar", "baz", "default" ]
console.log(
findExportNames(`
export const foo = 'bar'
export { bar, baz }
export default something
`),
);
resolveModuleExportNames
解析模块并读取其内容,以使用静态分析提取可能的导出名称。
import { resolveModuleExportNames } from "mlly";
// ["basename", "dirname", ... ]
console.log(await resolveModuleExportNames("mlly"));
评估模块
使用 data:
导入来评估 ESM 模块的一组工具
- 使用静态分析自动将导入重写为解析路径
- 允许绕过 ESM 缓存
- 堆栈跟踪支持
.json
加载器
evalModule
使用动态导入转换和评估模块代码。
import { evalModule } from "mlly";
await evalModule(`console.log("Hello World!")`);
await evalModule(
`
import { reverse } from './utils.mjs'
console.log(reverse('!emosewa si sj'))
`,
{ url: import.meta.url },
);
选项
- 所有
resolve
选项 url
: 文件 URL
loadModule
通过评估源代码动态加载模块。
import { loadModule } from "mlly";
await loadModule("./hello.mjs", { url: import.meta.url });
选项与 evalModule
相同。
transformModule
- 所有相对导入都将被解析
- 所有
import.meta.url
的使用都将被替换为url
或from
选项
import { transformModule } from "mlly";
console.log(transformModule(`console.log(import.meta.url)`), {
url: "test.mjs",
});
选项与 evalModule
相同。
其他工具
fileURLToPath
类似于 url.fileURLToPath,但也会将 Windows 反斜杠 \
转换为 Unix 斜杠 /
,并处理输入已经是路径的情况。
import { fileURLToPath } from "mlly";
// /foo/bar.js
console.log(fileURLToPath("file:///foo/bar.js"));
// C:/path
console.log(fileURLToPath("file:///C:/path/"));
pathToFileURL
类似于 url.pathToFileURL,但也处理 URL
输入,并返回带有 file://
协议的字符串。
import { pathToFileURL } from "mlly";
// /foo/bar.js
console.log(pathToFileURL("foo/bar.js"));
// C:/path
console.log(pathToFileURL("C:\\path"));
normalizeid
确保 ID 具有 node:
, data:
, http:
, https:
或 file:
协议之一。
import { ensureProtocol } from "mlly";
// file:///foo/bar.js
console.log(normalizeid("/foo/bar.js"));
loadURL
读取 URL 的源内容。(目前仅支持文件协议)
import { resolve, loadURL } from "mlly";
const url = await resolve("./index.mjs", { url: import.meta.url });
console.log(await loadURL(url));
toDataURL
使用 base64 编码将代码转换为 data:
URL。
import { toDataURL } from "mlly";
console.log(
toDataURL(`
// This is an example
console.log('Hello world')
`),
);
interopDefault
返回模块的顶级默认导出,以及任何其他命名导出。
// Assuming the shape { default: { foo: 'bar' }, baz: 'qux' }
import myModule from "my-module";
// Returns { foo: 'bar', baz: 'qux' }
console.log(interopDefault(myModule));
选项
preferNamespace
: 如果default
值存在但不可扩展(例如为字符串时),则按原样返回输入(默认为false
,表示即使不能扩展,也优先使用default
的值)
sanitizeURIComponent
替换 URI 段中的保留字符,使其与 rfc2396 兼容。
import { sanitizeURIComponent } from "mlly";
// foo_bar
console.log(sanitizeURIComponent(`foo:bar`));
sanitizeFilePath
使用 sanitizeURIComponent
净化文件名称或路径中的每个路径,以实现 URI 兼容性。
import { sanitizeFilePath } from "mlly";
// C:/te_st/_...slug_.jsx'
console.log(sanitizeFilePath("C:\\te#st\\[...slug].jsx"));
parseNodeModulePath
将 node_modules
中的绝对文件路径解析为三个片段
dir
: 包主目录的路径name
: 包名称subpath
: 可选的包子路径
如果解析失败,则返回一个空对象(带有部分键)。
import { parseNodeModulePath } from "mlly";
// dir: "/src/a/node_modules/"
// name: "lib"
// subpath: "./dist/index.mjs"
const { dir, name, subpath } = parseNodeModulePath(
"/src/a/node_modules/lib/dist/index.mjs",
);
lookupNodeModuleSubpath
解析 node_modules
中的绝对文件路径,并尝试反向查找(或猜测)其原始包导出子路径。
import { lookupNodeModuleSubpath } from "mlly";
// subpath: "./utils"
const subpath = lookupNodeModuleSubpath(
"/src/a/node_modules/lib/dist/utils.mjs",
);
许可
MIT - 用 💛 制作