Skip to main content

@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:

  1. Collect — walk every module/global declaration (declare module "...", namespaces, interfaces, classes, type aliases, function overloads, export = / export * from re-exports) into a shared symbol table.
  2. 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, process from @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 targetWhat happensProduct path
JS source package ships usable .js/.mjs (e.g. commander, ms, debug)Analysis executes the source through the Abs evaluator / checkSourcenudo check / nudo infer / LSP — no handwritten mock required for the zero-FP precision gate
Types package — @types/* installed or the package ships .d.tsDeclarations 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 typesAuto path yields empty modulesUse @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:

BudgetDefaultNotes
maxFiles12Entry-priority collect under node_modules/@types/node
maxMs2500Passed to harvestDts; excess files count as stats.skipped
disableNUDO_HARVEST_NODE=offReturns { 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.