跳到主要内容

@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 编译器分两个阶段解析:

  1. 收集 —— 把每个模块/全局声明(declare module "..."、命名空间、interface、class、类型别名、函数重载、export = / export * from 再导出)收集进共享符号表。
  2. 物化 —— 把收集到的符号转换为 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.tsharvest 声明(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 启动:

预算默认说明
maxFiles12入口优先收集 node_modules/@types/node 下声明
maxMs2500传给 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@ 兜底是诚实结果,不是缺陷

另见语言语义 — mock 边界。