扩展名、extensionAlias 与 Node 解析

问题背景

TypeScript 库源码和运行时 JavaScript 产物之间有一个天然矛盾:

源码文件可能是:

src/Button.ts

但 ESM 规范要求源码 import 写成运行时扩展名:

import './Button.js';

构建时需要解析到 Button.ts,输出时又要保留或生成 Button.js。这就是 Rslib 里 extensionAliasautoExtension、bundleless redirect 共同解决的问题。

extensionAlias 做什么

Rslib 内置配置中有:

extensionAlias: {
  '.js': ['.ts', '.tsx', '.js', '.jsx'],
  '.jsx': ['.tsx', '.jsx'],
  '.mjs': ['.mts', '.mjs'],
  '.cjs': ['.cts', '.cjs'],
}

它解决的是“源码解析”问题。用户写:

import './foo.js';

构建时可以解析到:

foo.ts

这符合 TypeScript + Node ESM 的推荐写法:源码里写运行时扩展名,但开发时仍解析 TS 源文件。

autoExtension 做什么

autoExtension 解决的是“输出扩展名和 import 扩展名”问题。它会结合:

  • format。
  • package.json type
  • 用户 output filename。

推导最终 JS extension 和 dts extension。

例如:

  • ESM 在某些 package type 下可输出 .js
  • ESM 也可能需要 .mjs
  • CJS 可能需要 .cjs
  • dts 可能需要 .d.mts.d.cts

为什么 bundleless 必须改 import 扩展名

Bundleless 输出保留文件间 import。如果源码是:

export { Button } from './Button';

输出 ESM 如果仍是:

export { Button } from './Button';

Node ESM 不会自动解析 ./Button.js。所以 Rslib 会补成:

export { Button } from './Button.js';

如果输出扩展名是 .mjs,就补 .mjs

目录 import

源码可能写:

import './Button';

而实际是:

Button/index.ts

redirect.js.path 关闭但 redirect.js.extension 开启时,Rslib 仍需要判断目录 import,补出:

import './Button/index.js';

否则只补成 ./Button.js 就错了。

用户自定义 filename

用户可以配置:

output: {
  filename: {
    js: '[name].jsx',
  },
}

这种情况下,Rslib 会从用户 filename 推导 final JS extension。bundleless redirect 也要跟着用 .jsx

测试 preserve-jsx 里就覆盖了 .jsx 输出。

dts extension

如果用户开启 dts.autoExtension,Rslib 会把 dtsExtension 传给 plugin-dts。这样:

index.mjs
index.d.mts

或:

index.cjs
index.d.cts

可以保持模块语义一致。

关掉 autoExtension 的风险

用户可以关掉 autoExtension,但风险是:

  • Node ESM import 可能缺扩展名。
  • CJS/ESM 在 package type 下可能被 Node 误判。
  • dts extension 可能和 JS 产物不匹配。
  • bundleless 输出 import 需要用户自己保证可解析。

因此 core 默认开启 autoExtension。

相关测试

应关注:

  • tests/integration/auto-extension
  • tests/integration/extension-alias
  • tests/integration/redirect
  • tests/integration/preserve-jsx
  • tests/integration/format

扩展名相关问题通常表现为运行时解析失败,而不是构建失败。