模块解析
Nub 在 Node 之上分层的 TypeScript 感知解析——tsconfig 路径别名和 baseUrl、extends 链、无扩展名导入,以及 JavaScript 到 TypeScript 扩展名交换。
Nub 在 Node 自身的解析器之上添加了 TypeScript 感知的解析层。
// tsconfig.json paths 别名 — 无需额外工具即可工作
import { db } from "@db";
// 无扩展名 — 解析 ./config.ts
import { config } from "./config";
// .js→.ts 交换 — 磁盘上没有 .js 时解析 ./handler.ts
import { handler } from "./handler.js";如果你的编辑器的转到定义在导入上工作且 tsc 接受它,Nub 在运行时以相同方式解析——不需要 tsconfig-paths 包,不需要构建步骤。Nub 只负责 Node 没有意见的情况:tsconfig.json 路径别名、TypeScript 文件中的无扩展名导入,以及 .js→.ts 发射约定交换。其他一切(node_modules、exports / imports 映射、导出条件、包名)原封不动地回退到 Node。
tsconfig.json
Nub 读取最近的 tsconfig.json 并在运行时应用其解析设置——paths、baseUrl、extends——与你的编辑器和 tsc 已做的一致。
paths
compilerOptions.paths 和 compilerOptions.baseUrl 映射无需额外工具即可解析 @/... 风格的别名。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@db": ["src/db/index.ts"]
}
}
}import { db } from "@db";
import { Button } from "@/components/Button";匹配遵循 tsc 规则:精确模式整体获胜,在通配符模式中最长匹配前缀获胜。Node 内置始终优先——import "os" 是 node:os,不是 baseUrl/os.ts。
路径别名从任何文件工作,不只是 TypeScript 文件——导入 paths 别名的 .js 文件也解析它。(下方的无扩展名和 .js→.ts 行为是限定到 TypeScript 系列父文件的。)
baseUrl
设置了 baseUrl 且没有匹配的 paths 条目时,裸说明符相对于基础目录解析。import "lib/config" 自行找到 baseUrl/lib/config.ts。
{ "compilerOptions": { "baseUrl": "." } }import { config } from "lib/config"; // 解析 <baseUrl>/lib/config.ts没有 baseUrl 相对文件的裸说明符回退到 Node 的 node_modules 解析。
extends
Nub 向上遍历到最近的 tsconfig.json 并遵循 extends 链,包括数组 extends(后面的条目优先)和通过 node_modules 解析的包 extends 如 extends: "@tsconfig/node20/tsconfig.json"。TS 5.5 的 ${configDir} 模板相对于消费配置的目录插值。
配置每进程读取一次。编辑你的 tsconfig.json 并重启进程——或让 nub watch 为你重启。
无扩展名导入
在 TypeScript 系列文件中,无扩展名导入按 tsc 解析的方式解析——import "./foo" 找到 ./foo.ts。这是大多数 TypeScript 代码库使用的约定模式。
// 从 .ts / .tsx / .mts / .cts 文件:
import { config } from "./config"; // 解析 ./config.ts
// 解析 ./fixtures/index.ts(或其 package.json "main")
import data from "./fixtures";探测顺序是扩展名感知的——它取决于父文件的扩展名:
.ts 父文件 → .ts .tsx .js .jsx .json
.tsx 父文件 → .tsx .ts .jsx .js .json # JSX 扩展名排在前面
.mts 父文件 → .mts .ts .mjs .js .json
.cts 父文件 → .cts .ts .cjs .js .json目录导入在回退到探测 index.<ext> 之前遵循该目录的 package.json "main",使用相同的父文件感知顺序。只查询 "main"——exports 从不为目录路径导入读取,与 Node 一致,Node 只为包名解析遵循 exports。
此探测限定到带相对说明符的 TypeScript 系列父文件(.ts / .tsx / .mts / .cts)。普通 .js 文件保持对扩展名严格,与原生 Node 完全一样。
.js → .ts 解析
在 moduleResolution: "nodenext" 下,tsc 要求相对导入带 .js 扩展名,即使磁盘上的文件是 .ts。Nub 在运行时反转这一点,所以相同的源码在构建前后都能运行。
// 没有 ./handler.js 时解析 ./handler.ts
import { handler } from "./handler.js";字面扩展名始终优先:磁盘上真实的 ./handler.js 按原样使用,交换仅在写入的扩展名指向不存在的文件时触发。相同的重写覆盖 .jsx → .tsx、.mjs → .mts 和 .cjs → .cts——同样,真实的 .cjs 优先于同级的 .cts。这是 tsc/tsx 的发射约定,不是对文件类型的猜测。
什么回退到 Node
Nub 只解析 Node 没有意见的 TypeScript 感知情况。其他一切都是 Node 的工作,由 Node 自身的解析器原封不动处理:
node_modules解析exports/imports映射- 导出条件
- 包名
Yarn Plug'n'Play
Yarn 以 PnP 模式安装的项目直接运行——Nub 自动启用 Plug'n'Play,无需标志无需设置。从工作目录向上遍历,Nub 在项目根目录检测到 .pnp.cjs 并在其自身预加载之前注入(快速层上 --require .pnp.cjs,兼容层上 NODE_OPTIONS 等价物),所以 PnP 的解析器补丁先安装,Nub 的 TypeScript 解析在其之上分层。
nub index.ts # .pnp.cjs 被自动检测和加载
nub run build # 运行脚本和 nubx 也相同require 和 import 在 PnP 下都解析,通过不同路径:
| 模块系统 | 解析方式 |
|---|---|
CommonJS(require) | PnP 自身的 _resolveFilename 补丁,由注入的 --require .pnp.cjs 安装 |
ESM(import) | Nub 的解析钩子调用 pnpapi.resolveRequest——遵循导入条件(双包选择其 import 构建)并标记格式(zip 存储的纯 ESM 加载为 ESM) |
Nub 不注册 Yarn 的 .pnp.loader.mjs(其 ESM 解析器与快速层的 module.registerHooks 冲突),所以相同的 pnpapi.resolveRequest 路径服务两个层级。
它在整个支持的 Node 范围(18.19+)上工作,在 macOS、Linux 和 Windows 上——包括 zip 存储包的 import 和通过 nubx 运行 zip 存储的二进制文件。这是运行时运行 PnP 项目;Nub 的安装器自身不产生 PnP 安装——那仍是 Yarn 的工作。包 extends 链通过 node_modules 遍历而非 PnP API 解析——这仍覆盖常见的 npm / pnpm 安装。