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 - 用 💛 制作