Logo of mlly

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 exportimport.meta 使用)

import { hasESMSyntax } from "mlly";

hasESMSyntax("export default foo = 123"); // true

hasCJSSyntax

检测代码是否使用了 CommonJS 语法(exports, module.exports, requireglobal 使用)

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);

注意:requirerequire.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 的使用都将被替换为 urlfrom 选项
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 - 用 💛 制作