Infer a project's Node.js and npm, pnpm or Yarn versions from explicit inputs, nearby project files and measured lockfile compatibility. Requires Node.js ≥18. Bun and Deno are outside the scope.
中文 · Website and calculator · Maintenance procedure · Human/AI maintenance instructions
Known installation bugs can fail a frozen-install test even when the lock format is semantically compatible. Reviewed exceptions preserve those failed observations and keep compatible versions selectable; known-package-manager-bug warns when one is selected. Full test provenance stays in the maintenance repository, outside the npm package.
Explicit versions and semver ranges follow the same rule: a retained API/CLI or project-file declaration can admit matching prereleases for its own tool. Ranges use standard semver matching. Both [email protected] and pnpm@>=10.0.0-rc.1 <10.0.0 can select a pnpm prerelease. Inferring Node from that manager still selects stable versions only unless Node has its own explicit declaration admitting prereleases. Lockfile inference, local fallback without a matching declaration, release statistics and compatibility maintenance remain stable-only.
import { infer } from 'node-toolchain-infer'
const result = await infer({
cwd: '/path/to/project',
node: '>=18 <23',
packageManager: 'pnpm@^9',
})
console.log(result.node?.version, result.packageManager?.version)
console.log(result.warnings, result.trace)Both inputs accept semver ranges, including 18, 18.20, exact versions, comparators and ||. An omitted package manager defaults to npm. Conflicting or malformed declarations produce warnings and a trace of the retained conditions.
Files are collected from the real cwd through the nearest Git root, inclusive. A .git file is recognized for worktrees. Without a Git root, only cwd is read and a warning is returned. Directory proximity comes before source priority. Within each directory the order is:
package.json#packageManagerpnpm-lock.yaml,shrinkwrap.yaml,yarn.lock,npm-shrinkwrap.json,package-lock.jsonvolta.node/pnpm/yarn/npm,.node-version,.nvmrc,.tool-versions(nodejs)devEngines.runtime(name: node),devEngines.packageManagerengines.node, then the selected manager'senginesentry
Caller inputs precede every file. Equal-ranked occurrences retain their input order. A concrete value is a singleton constraint; it does not promote its source. A conflicting lower-priority condition and its derived Node requirement are ignored together. Package-manager versions retain their own engines.node requirements throughout resolution.
Volta declarations follow volta.extends, resolving each path relative to its declaring file; child values override inherited values. Their trace retains the declaring path and the child project's directory priority. volta.pnpm and volta.yarn select a manager; volta.npm constrains npm only when npm is selected, allowing npm to coexist with pnpm/Yarn. Within the Volta group, Node, pnpm, Yarn, npm are considered in that order. Invalid inheritance produces a warning while preserving readable declarations.
Node selection: retained exact version → current version if eligible → greatest eligible published version. Manager selection: retained exact version → verified local version if eligible → greatest eligible published version. For npm, a separately verified local npm is preferred only on the running Node, followed by the selected Node's bound npm. No Node or npm version is hardcoded as a fallback.
infer() reads project declarations before requesting metadata. It starts with Node and the first valid package-manager selector, or npm when there is no selector. Other managers are fetched only if they can still change the result under the declaration-priority rules. A normal pnpm project requests only Node and pnpm; unrelated Yarn endpoints and inactive Yarn engine declarations do not add requests. Explicit-version lookups follow the same tool selection. fetchCatalog() continues to fetch the complete catalog by default; fetchCatalog({ tools: ['node', 'pnpm'] }) explicitly requests a subset.
The complete catalog covers eight official endpoints: the Node release index, npm registry metadata for npm, pnpm, historical Yarn, Yarn cli-dist, Yarn CLI, Yarn's official Berry tags, and Yarn release lines. Yarn supports Classic (<2), Berry (>=2 <6) and native Yarn 6+ (yarnpkg/zpm). The native release list unions releaseLines.zpm.tags, stable and canary, then filters by SemVer stability, regardless of the channel label. Native releases carry runtime: 'native' and an unrestricted host Node constraint (node: '*'); this does not remove the project's Node declarations or the inference package's Node requirement. Older Yarn declarations prefer cli-dist, then yarn, then cli for duplicate versions. Tags absent from the registries trigger an additional package-manifest request at the official tag; absent engines stay unknown. Normal release lists omit prereleases. Explicit prerelease declarations trigger on-demand metadata queries. An exact version queries its official manifest or release entry; a range queries the relevant official version lists and retains only matching prereleases. Node ranges query the official dist, rc, nightly, v8-canary and test indexes. These records never enter the normal release statistics. Returned receipts include source URLs, fetch/check timestamps and response SHA-256.
The Node package cache holds metadata in this process only. Calls use ETag/Last-Modified revalidation, reuse 304 responses and share simultaneous requests. There is no default disk cache or bundled release list. A failed refresh may use earlier successful data with a timestamped warning. infer() retains successful sources when another fails, with a metadata-source-unavailable warning. Without metadata, inference can use known runtime candidates and reports the limitation; it cannot assert a global maximum. fetchCatalog() requires all primary sources by default so maintenance snapshots remain complete; callers can opt into partial results with allowPartial: true.
Transient transport failures and HTTP 429/500/502/503/504 receive up to two retries (three attempts total), only for the failed source. Backoff is approximately 200 ms then 800 ms with jitter, respecting Retry-After; each source has one 20-second deadline including requests, bodies and waits. HTTP 404, certificate errors and invalid response data are not retried. Caller cancellation stops pending work when no other caller shares it. Missing explicitly pinned stable versions are queried individually as well as prereleases, retaining the official version's Node requirement and bound npm where applicable.
Only the compiled lockfile compatibility table is bundled. Confirmed behavior boundaries become semver intervals during maintenance. The last supported interval has no upper bound until a measured incompatibility establishes one. A format absent from this package version's table gives * while retaining the manager identity. This is a permissive inference policy, not proof that every project will install successfully.
Yarn lockfile identity includes its serialization family: native JSON (features.yarnFamily: 'zpm') is distinct from Berry YAML even when both have __metadata.version: 9. Native formats without measured stable-version rules remain * and produce a warning. Existing open Berry ranges are not closed merely because Yarn changes implementation. Inference downloads metadata only, never package-manager executables.
The repository contains 23 compatibility fixtures: npm package-lock and shrinkwrap 1/2/3; pnpm legacy shrinkwrap 3/4 and lockfile 5/5.1/5.2/5.3/5.4/6/9; Yarn Classic v1 and Modern 4/5/6/7/8/9/10. A fixture passing at one version does not establish a complete range; unconfirmed formats remain *. See the retained matrix and boundary reports for measured coverage. The initial data report records the exact catalog, per-manager attempted/conclusive counts, unresolved cases and reproduction commands.
Run the complete initial matrix with pnpm data:seed --concurrency 6. The first run pins the official release catalog in maintenance/evidence/initial-matrix/catalog.json; every npm, pnpm and Yarn release, excluding prereleases and including historical stable Yarn distributions, is attempted against every fixture for that manager. Repeat the same command to resume. Use --retry-incomplete to retry environment/provisioning failures. Individual rows, logs, dependency bootstrap locks and summary.json are preserved; successful records are reused only when the tested inputs and experiment contract match. A new catalog uses a separate --output directory. npm package-lock and shrinkwrap are separate compatibility predicates.
Historical Yarn bundles are also inventoried from the official tags. Maintenance resolves each tag to a commit, downloads the standalone script at that immutable commit, and records its URL, size and computed SHA-512. This maintenance digest is not represented as a registry-published SRI. Native Yarn maintenance downloads the matching official @yarnpkg/yarn-<platform> npm artifact, checks manifest identity, platform, SRI and the executable’s reported version, and runs it directly. Its receipt retains the platform package, exact version, source URLs, SRI and binary SHA-256. Missing platform artifacts are incomplete checks, not lock incompatibility. Expanded catalogs preserve original snapshots; the compiler can reuse old observations only when artifact identity and fixture bytes still match. See initial compilation and pair retesting.
An explicit Yarn 6 integration verification exercises the native executable, JSON lock collection, node_modules/PnP frozen installation and inference. Its RC records are outside compatibility matrices, compiled ranges, release statistics and known-bug rules.
The bundled table contains 23 rules: 21 measured ranges plus two explicit Yarn project policies. Classic v1 uses >=0.21.0 <2.1.0, with an adopted support floor; Modern v7 has no supported stable release (empty range). These policies do not claim exhaustive historical measurements. The stable-only report preserves the earlier evidence-only compilation and its unresolved observations.
infer(options?): filesystem collection, runtime detection, metadata and resolution.collect({cwd?, node?, packageManager?}): read-only collection, no project code execution.resolve({sources, runtime}, catalog, rules?): pure resolver, also exported fromnode-toolchain-infer/resolve.detectRuntime({cwd?}): current Node, adjacent npm, optional local pnpm/Yarn and separately installed Volta npm.fetchCatalog(),loadCatalog(path),loadRules(): official facts and explicit snapshots/rules.createSource(key, value, options?),SOURCE_DEFINITIONS: construct inputs for simulation.
infer accepts explicit runtime, catalog, rules, fetcher and signal for deterministic tests or caller-managed metadata. A supplied catalog makes no additional network requests, including for prereleases admitted by explicit versions or ranges but absent from that snapshot. Filesystem scan results are not cached.
Local pnpm/Yarn detection first probes the target project directory (cwd, defaulting to the current directory), allowing Corepack to use an already cached project version. If that probe fails, it probes the machine default from the Node installation directory with project selection disabled. Both probes disable Corepack networking and auto-pinning; detection neither downloads a missing Corepack version nor adds a packageManager field. infer() passes its scanned directory to detection; an explicit runtime bypasses detection.
When the selected PATH command belongs to Volta, detection reads project/default settings and checks the installed cache instead of invoking a shim or volta which, which can fetch missing tools. It verifies the cached package's manifest, Node requirement, entrypoint and actual version under the running Node. A cached project pin takes priority; an unavailable pin falls back to a verified cached default. Native pnpm support follows VOLTA_FEATURE_PNPM; legacy global pnpm installations are also recognized. An inactive Volta installation does not override another command earlier on PATH. Detection does not install tools, change Volta defaults or write project files. See Volta's project inheritance and pnpm support.
runtime.npm remains the npm bound to the running Node. runtime.localNpm optionally records a separately verified npm; it never changes node.bundledNpm. A Volta project with a Node pin but no npm pin uses bundled npm and does not inherit the machine's custom npm. nvm, fnm and asdf need no activation commands: current Node comes from the running process, while .nvmrc, .node-version and .tool-versions remain project constraints. Inference does not switch the calling process's Node version.
node-toolchain-infer --cwd . --node '18' --package-manager 'pnpm@^9' --json
node-toolchain-infer --helpThe result includes the selected versions, warnings, ordered trace, retained constraints, candidate versions, scanned directories and metadata timestamp. The CLI exits nonzero for operational failures or when no runnable pair exists; declaration conflicts alone are warnings.
Warnings carry a stable code, structured params, an English message, and source context (sourceId, path, blockers) where available. Use formatWarning(warning, 'zh-CN') or formatWarning(warning, 'en') to render from the code and parameters, without matching message text. Import it from node-toolchain-infer or the browser-safe node-toolchain-infer/warnings entry. Invalid declarations include params.reason and params.value. Unknown codes and old payloads missing required parameters retain their original message; external error details are preserved as received.
Metadata failures also retain requestFailures: source ID, URL, attempt count, elapsed milliseconds, HTTP status when available, and nested error messages/codes. The same structured detail is available as onSource events' failure and MetadataError.diagnostics; MetadataError.failures remains an array of readable messages.
Use Node 24 and pnpm 10.33.0 for repository tooling; the published package runs on Node ≥18.
pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm build
pnpm pack
pnpm website:build
pnpm website:servewebsite/ is a pure frontend with four independent tabs and a Chinese/English switch. Its fetch adapter uses cache: no-cache to let the browser revalidate its HTTP cache, without manually adding conditional headers that trigger CORS preflight. The application keeps version data in memory and does not write version snapshots to localStorage or IndexedDB; HTTP cache storage follows browser policy. It imports the same resolver and official metadata parser as the package. The server prints its local URL; set PORT=49295 to choose that port. Browser checks use pnpm exec playwright install chromium then pnpm website:test; CHROME_PATH can select an installed Chrome executable.
tsc is TypeScript 7.0.2. eslint-config-unicute uses the official @typescript/typescript6 compatibility API through Microsoft's documented side-by-side aliases. No dependency overrides are required. Formatting is performed by pnpm lint:fix.
Scheduled compatibility checks, fixture generation, retained evidence, failure exit codes and the AI maintenance procedure are documented in maintenance/. GitHub Actions preserves new maintenance evidence on the compatibility-data branch. Pushes to main build and deploy the bilingual website to GitHub Pages. Version tags publish the npm package through the npm Environment using trusted publishing; see publishing.
Frozen installation uses each pnpm release’s historical option: --frozen-shrinkwrap before 3.0.0-alpha.3, then --frozen-lockfile. The option is selected by the tested manager version for both lockfile names. An ignored option cannot establish support because the contradictory-manifest control must also pass.