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 并在运行时应用其解析设置——pathsbaseUrlextends——与你的编辑器和 tsc 已做的一致。

paths

compilerOptions.pathscompilerOptions.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 也相同

requireimport 在 PnP 下都解析,通过不同路径:

模块系统解析方式
CommonJS(requirePnP 自身的 _resolveFilename 补丁,由注入的 --require .pnp.cjs 安装
ESM(importNub 的解析钩子调用 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 安装。