@nudojs/harvester
API reference for the Nudo declaration harvester. @nudojs/harvester converts TypeScript .d.ts declarations into Nudo env definitions — TypeScript source that rebuilds those types with Abs constructors from @nudojs/core. It is the engine behind the nudo harvest CLI command and lets Nudo infer types for any package that ships @types declarations, without hand-written mocks.
Public API
The package exports two functions and one type from src/index.ts.
harvestDts
harvestDts(files: string[]): HarvestedEnv
Reads the given .d.ts file paths from disk and parses them with the TypeScript compiler in two phases:
- Collect — walk every module/global declaration (
declare module "...", namespaces, interfaces, classes, type aliases, function overloads,export =/export * fromre-exports) into a shared symbol table. - Materialize — convert the collected symbols into Abs values (see core). Because materialization runs only after the whole symbol table is populated, cross-file references resolve regardless of file order.
The result mirrors core's EnvDefinition shape ({ globals, modules }), so the emitted env plugs directly into the /// @nudo:env loading path.
type HarvestedEnv = {
globals: Record<string, Abs>;
modules: Record<string, Record<string, Abs>>;
stats: { files: number; symbols: number; skipped: number };
};
globals— symbols declared in the global scope (e.g.Buffer,processfrom@types/node)modules— per-module records, keyed by both bare and prefixed names ("path"and"node:path"for the same record)stats— counters surfaced by the CLI: files parsed, symbols emitted, declarations skipped (unsupported syntax)
Example:
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
Renders a HarvestedEnv as importable TypeScript source exporting a single defineEnv() function:
import { emitEnvModule } from "@nudojs/harvester";
const code = emitEnvModule(env, "@types/node");
The emitted file follows this shape (module records shared under several keys — e.g. "path" aliasing "node:path" — are emitted once as a const and referenced from every key):
// 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> = {
// …per-module symbols…
};
return {
globals: {
// …global symbols…
},
modules: {
path: mod0,
"node:path": mod0,
},
};
}
Individual symbols use the Abs constructors — e.g. these entries harvested from @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" }),
The nudo harvest Command
The CLI wraps this package: it locates the entry .d.ts for an installed @types/<pkg> package (from its package.json types/typings field, falling back to index.d.ts), breadth-first collects referenced files (<reference path="…" /> and relative imports, capped at 200 files), calls harvestDts + emitEnvModule, and writes the result:
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
By default the env file is written to ./nudo-harvest-<pkg>.ts; pass --out <file> to change it. See the CLI reference for the full command contract.
Using the Harvested Env
Add the generated file to your JavaScript source with a path-based /// @nudo:env directive (distinct from the named environments es / web / node documented in Directives):
/// @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");
Running nudo infer on this file shows the harvested signature of join flowing through the call site:
=== buildKey ===
call@L9: ("docs", "readme") => `${string}.md`
Path-based env files are loaded via dynamic import, so asynchronous consumers (nudo infer, analyzeFileAsync, the LSP validation path) preload them; the synchronous analyzeFile degrades when a file declares one. Async tooling should prefer analyzeFileAsync.
Automatic harvest path (three states)
When analysis meets a bare import (import x from "commander"), Nudo does not invent types. The auto path follows three states:
| Import target | What happens | Product path |
|---|---|---|
JS source package ships usable .js/.mjs (e.g. commander, ms, debug) | Analysis executes the source through the Abs evaluator / checkSource | nudo check / nudo infer / LSP — no handwritten mock required for the zero-FP precision gate |
Types package — @types/* installed or the package ships .d.ts | Declarations are harvested (harvestPackage / harvestNodeTypes / bareSpecToAbsModules → env modules) | nudo harvest, analysis injection via module-graph harvest, handwritten @nudojs/env stays primary for builtins |
| Neither JS analysis path nor types | Auto path yields empty modules | Use @nudo:mock, a path-based /// @nudo:env, or a sidecar hint — see the mock boundary below |
Library helper vs production injection. autoHarvestModules (exported from @nudojs/service) is the programmatic harvest helper used by tools/tests. Production analysis injection goes through evalAbsModuleGraph → bareSpecToAbsModules (harvest-to-abs.ts), then mergeHarvestUnderEnv so handwritten @nudojs/env wins on overlap. There is not a second analysis path.
barePackageName("lodash/fp") → lodash; relative / absolute / node: specifiers are never harvest targets (builtins use handwritten env).
Performance budgets (@types/node)
harvestNodeTypes in @nudojs/service is budgeted so IDE startup is not dragged down by huge .d.ts graphs:
| Budget | Default | Notes |
|---|---|---|
maxFiles | 12 | Entry-priority collect under node_modules/@types/node |
maxMs | 2500 | Passed to harvestDts; excess files count as stats.skipped |
| disable | NUDO_HARVEST_NODE=off | Returns { ok: false, reason: "disabled" } — explicit, not silent |
Results are cached in-process — success and terminal failures (not-found / no-dts / failed), keyed by package root + package.json mtime/size + budgets. disabled (NUDO_HARVEST_NODE=off) is never cached. Call clearNodeHarvestCache() after @types/node changes in watch/test paths. Handwritten @nudojs/env wins over harvest on overlapping module keys and export names — analysis injects via mergeHarvestUnderEnv in @nudojs/service (harvest only fills missing slots). Do not treat harvest output as the type-system source of truth.
Coverage baseline (resolved / leaf-clean / unknown / mock-required) is generated by pnpm run coverage:env → docs/reports/env-coverage-baseline.{json,md}. Resolution rate — and even leaf-clean ratio — is not a soundness claim.
Mock boundary (honest)
Still recommended for handwritten mock (aligned with docs/design-limitations.md §八 call-site ceiling):
- Native bindings (
child_process.spawn, native addons) — env may hold a signature, not side effects - Dynamic
require/ computed module graphs - Stream machine callbacks (Node Transform internals driven by the runtime)
- Dual-entry browser/node variants — call-site records do not cross files
- Functions with no call-site usage —
entry@fallback is honest, not a defect
See also Language Semantics — mock boundary.