@nudojs/harvester
Nudo 声明收割器(harvester)的 API 参考。@nudojs/harvester 把 TypeScript .d.ts 声明转换为 Nudo env 定义 —— 即用 @nudojs/core 的 Abs 构造器重建这些类型的 TypeScript 源码。它是 nudo harvest CLI 命令背后的引擎,让 Nudo 能为任何发布 @types 声明的包推断类型,无需手写 mock。
公开 API
该包从 src/index.ts 导出两个函数和一个类型。
harvestDts
harvestDts(files: string[]): HarvestedEnv
从磁盘读取给定的 .d.ts 文件路径,并用 TypeScript 编译器分两个阶段解析:
- 收集 —— 把每个模块/全局声明(
declare module "..."、命名空间、interface、class、类型别名、函数重载、export =/export * from再导出)收集进共享符号表。 - 物化 —— 把收集到的符号转换为 Abs 值(见 core)。物化只在整张符号表填充完毕后运行,因此跨文件引用与文件顺序无关,均可解析。
结果与 core 的 EnvDefinition 形状一致({ globals, modules }),因此产出的 env 可以直接接入 /// @nudo:env 加载路径。
type HarvestedEnv = {
globals: Record<string, Abs>;
modules: Record<string, Record<string, Abs>>;
stats: { files: number; symbols: number; skipped: number };
};
globals—— 全局作用域中声明的符号(如@types/node的Buffer、process)modules—— 按模块键控的记录,同时以裸 名和带前缀名注册(同一条记录同时有"path"和"node:path"两个键)stats—— CLI 展示的计数:解析的文件数、产出的符号数、跳过的声明数(不支持的语法)
示例:
import { harvestDts } from "@nudojs/harvester";
const env = harvestDts(["node_modules/@types/node/fs.d.ts"]);
env.stats; // { files: 1, symbols: …, skipped: … }
emitEnvModule
emitEnvModule(env: HarvestedEnv, pkgName: string): string
把 HarvestedEnv 渲染为可导入的 TypeScript 源码, 导出单个 defineEnv() 函数:
import { emitEnvModule } from "@nudojs/harvester";
const code = emitEnvModule(env, "@types/node");
产出文件遵循如下形状(被多个键共享的模块记录 —— 例如 "path" 与 "node:path" 指向同一记录 —— 只产出一次 const,各键引用它):
// Auto-generated by nudo harvest — DO NOT EDIT
// Source package: @types/node
import { num, str, bool, never as absNever, unknown as absUnknown, numLit, strLit, boolLit, objOf, relationFn, abs as makeAbs } from "@nudojs/core";
export function defineEnv() {
const mod0: Record<string, unknown> = {
// …各模块符号…
};
return {
globals: {
// …全局符号…
},
modules: {
path: mod0,
"node:path": mod0,
},
};
}
单个符号使用 Abs 构造器 —— 例如从 @types/node 收割出的这些条目:
platform: relationFn([], makeAbs({ k: "sum", members: [strLit("aix"), strLit("android"), strLit("darwin"), /* … */ strLit("netbsd")] }, undefined, undefined, "exact"), { conf: "exact" }),
join: relationFn([makeAbs({ k: "arr", element: str() }, undefined, undefined, "exact")], str(), { conf: "exact" }),
nudo harvest 命令
CLI 封装了此包:它定位已安装 @types/<pkg> 包的入口 .d.ts(读取其 package.json 的 types/typings 字段,回退到 index.d.ts),广度优先收集被引用的文件(<reference path="…" /> 与相对导入,上限 200 个文件),调用 harvestDts + emitEnvModule 并写出结果:
npx @nudojs/cli harvest node
Harvested @types/node → nudo-harvest-node.ts
files: 80
symbols: 1671
skipped: 148
Usage — add this directive at the top of your JS file:
/// @nudo:env nudo-harvest-node.ts
env 文件默认写到 ./nudo-harvest-<pkg>.ts;传入 --out <file> 可更改。完整命令契约见 CLI 参考。
使用收割出的 env
在 JavaScript 源文件顶部用基于路径的 /// @nudo:env 指令引入生成的文件(与指令文档中的具名环境 es / web / node 不同):
/// @nudo:env nudo-harvest-node.ts
import { join } from "node:path";
export function buildKey(dir, name) {
const p = join(dir, name);
return p + ".md";
}
const key = buildKey("docs", "readme");
对该文件运行 nudo infer,可以看到收割出的 join 签名贯穿调用点:
=== buildKey ===
call@L9: ("docs", "readme") => `${string}.md`
基于路径的 env 文件通过动态 import 加载,因此异步消费方(nudo infer、analyzeFileAsync、LSP 验证路径)会预加载它们;同步的 analyzeFile 在文件声明了路径 env 时会降级。异步工具链应优先使用 analyzeFileAsync。
自动 harvest 路径(三态)
分析遇到裸 import(import x from "commander")时,Nudo 不会凭空发明类型。自动路径遵循三态:
| Import 目标 | 行为 | 产品路径 |
|---|---|---|
JS 源码包含可用 .js/.mjs(如 commander、ms、debug) | 通过 Abs 求值器 / checkSource 执行源码 | nudo check / nudo infer / LSP —— zero-FP 门禁 无需手写 mock |
类型包——已装 @types/* 或包自带 .d.ts | harvest 声明(harvestPackage / harvestNodeTypes / bareSpecToAbsModules → env modules) | nudo harvest、模块图 harvest 注入;内建 API 仍以手写 @nudojs/env 为主 |
| 两者皆无 | 自动路径返回空 modules | 使用 @nudo:mock、路径 /// @nudo:env 或侧车提示——见下方 mock 边界 |
库 helper vs 生产注入。 @nudojs/service 导出的 autoHarvestModules 是工具/测试用的程序化 harvest helper。生产分析注入走 evalAbsModuleGraph → bareSpecToAbsModules(harvest-to-abs.ts),再经 mergeHarvestUnderEnv 让手写 @nudojs/env 在重叠处 wins。不存在第二条分析注入路径。
barePackageName("lodash/fp") → lodash;相对 / 绝对 / node: 说明符永远不会成为 harvest 目标(内建走手写 env)。
性能预算(@types/node)
@nudojs/service 的 harvestNodeTypes 有预算保护,避免巨大 .d.ts 图拖垮 IDE 启动:
| 预算 | 默认 | 说明 |
|---|---|---|
maxFiles | 12 | 入口优先收集 node_modules/@types/node 下声明 |
maxMs | 2500 | 传给 harvestDts;超预算文件计入 stats.skipped |
| 关闭 | NUDO_HARVEST_NODE=off | 返回 { ok: false, reason: "disabled" } —— 显式而非静默 |
结果在进程内缓存——成功与终态失败(not-found / no-dts / failed),键:包根 + package.json mtime/size + 预算。disabled(NUDO_HARVEST_NODE=off)不进缓存。@types/node 在 watch/测试中变更后应调用 clearNodeHarvestCache()。手写 @nudojs/env 在重叠模块键 / 导出名上 wins——分析路径经 @nudojs/service 的 mergeHarvestUnderEnv 注入(harvest 只补缺失槽)。不要把 harvest 产物当作类型系统真相源。
覆盖基线(resolved / unknown / mock-required)由 pnpm run coverage:env 生成到 docs/reports/env-coverage-baseline.{json,md}。解析率不是完备性承诺。
Mock 边界(诚实清单)
仍建议手写 mock 的类别(与 docs/design-limitations.md §八 调用点天花板对齐):
- Native bindings(
child_process.spawn、原生 addon)—— env 可有签名,无副作用模拟 - 动态
require/ 计算模块图 - 流机器回调(Node Transform 运行时驱动的内部回调)
- browser/node 双入口变体——调用点记录不跨文件注入
- 无调用现场的函数——
entry@兜底是诚实结果,不是缺陷