npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@smoothbricks/nx-plugin

v0.7.11

Published

Local Nx plugin for workspace-standard package setup and missing inferred targets.

Readme

Nx Plugin

Local Nx plugin for workspace-standard package setup and missing inferred targets.

Workspace policy preserves configured plugin entries other than the disabled @nx/js/typescript, including their options, include/exclude filters and order. It adds the bare @smoothbricks/nx-plugin entry only when the plugin is absent; repeated updates do not replace an existing configured entry or discard scoped instances. Malformed plugin values are reported rather than replaced, and an unchanged Nx Tree configuration is not serialized again.

The public plugin entry loads its transformed bootstrap product synchronously, matching Nx's real handleImport require path in Node and Bun. A missing dist/index.js refuses by name and gives the ttsc bootstrap command; it never returns an empty graph or loads raw development source in place of transformed code.

Writing plugin code

These rules govern every Nx plugin written in this repository or in a consuming repository, a plugin local to one workspace included. Target Ownership lists what this plugin already infers; read it before adding inference.

  1. Reuse Nx; never re-implement it. Graph and target shapes (devkit's ProjectGraph and TargetConfiguration), dependsOn normalization (normalizeDependencyConfigDefinition) and task-graph expansion (createTaskGraph) come from Nx, imported from nx/src/… as this package's sources import them (nx/src/devkit-exports.js is the module @nx/devkit publishes). A plugin carries no traversal of its own and no second dependsOn parser, and checks what Nx already ships before adding a dependency. dependsOn takes ^, wildcards such as ^*-js, project:target and object forms; a copy answers for the spellings its author thought of and drifts from the Nx that schedules the task, and it never receives a repair of the pinned Nx (Nx 24.0.3 Runtime Patch).
  2. Put each decision in the hook that sees its data. createNodesV2 infers targets for each project file it matches, before any graph exists. createDependencies adds project edges and has no dependency graph yet. createMetadata runs after both, with every project's merged targets and the edges the plugins added (Nx adds a project's own implicitDependencies edges after it), so whole-graph validation belongs there, where a throw fails graph construction. A check in an earlier hook judges a graph that is not built.
  3. Derive inputs and outputs from the source of truth. A target hashes exactly what determines its result and declares every output a cache hit must restore. Where the plugin can compute a set, it reads it from the file that owns it and keeps no hand list beside it (AGENTS.md, Done): a missing entry replays a stale result, and an extra one misses the cache. Mutable tool state is never an output (Cowshed Build-State Output Policy).
  4. Decide in a pure core. A hook is a thin shell: it reads Nx's context and the files, calls a pure function over plain values (a ProjectGraph, a TargetConfiguration, parsed manifest text) and returns the answer. Table-driven tests call the function with in-memory devkit values, so its verdict is the same in every checkout and reproduces without a workspace (AGENTS.md, Proof).
  5. Refuse at graph time. When a project, target or edge is wrong, or the plugin cannot tell, graph construction fails with a typed error that names the project, the target, the edge and the fix. The plugin never warns and continues, and never leaves the case to a gate or CI: a warning scrolls past while tasks run and cache results on a graph the plugin could not judge, and a gate or CI finds the problem only after those results exist.
  6. Test through devkit values, never an Nx process. The plugin's contract is the value its hook returns; Nx's behavior is Nx's to test (AGENTS.md, Proof). A wiring gets at most one nx show project smoke. Test Fixtures That Run Nx owns the rule and its fixtures.

Target Ownership

Official @nx/js/typescript inference is disabled because it cannot run the workspace's source transformers. A package with tsconfig.lib.json receives transformer-aware tsc-js and native typecheck targets from this plugin.

@smoothbricks/nx-plugin also owns inferred targets Nx does not provide here:

  • typecheck-tests and typecheck-tests:watch from tsconfig.test.json
  • test-watch from explicit test commands for Bun and Vitest packages
  • Cargo workspace targets from a neighboring workspace-root Cargo.toml
  • aggregate build and lint targets
  • a cached shard-timings check, which lint depends on, beside bun test targets sharded with --shard and balanced with --timings
  • a cached deploy for a PRIVATE project with a wrangler manifest: the wrangler-deploy executor, which runs smoo wrangler deploy-stage --stage <stage>

The deploy is one target that takes whichever stage it is given, never a configuration per stage: pull-request stages are prN for unbounded N, so nx run <project>:deploy --stage=pr42 has to work for a stage no enumeration could list. It is cached on what it uploads (the project's files and its build half's output) and the stage, but the executor's hasher answers that hash only while the stage serves the version tagged with it. Any other live version, or none it can read, adds a nonce to the hash, so the run misses and deploys, still tagging the artifact's version; a rollback is never answered from the cache. Hashing the live tag in instead would never converge, since the version is tagged with the hash. Declaring deploy locally overrides only the properties it names, so a project adds cross-project ordering with "dependsOn": ["...", "backend:deploy"] and keeps the inferred executor; a project that declares deploy-build gets that edge instead of build, and the plugin caches that half without inventing its command.

private: true is half the detection, because a wrangler manifest alone does not mean deployable: a published library ships one to document the Durable Object binding it implements, and that manifest carries the same name, main and compatibility_date a deployable worker's does. So the inferred target goes to packages npm will never publish, and a published package that really is deployed declares deploy itself. Whether CI deploys any of them is a separate question, answered only by the deploy tags the CLI reads (stage-deploy-target and its three siblings).

Lint commands are inferred per project. A workspace Biome configuration enables the project-wide Biome check; an ESLint flat configuration enables ESLint for existing JavaScript/TypeScript files under that project's src and the files its tsconfig.test.json declares, wherever they live. TypeScript resolves inherited include, files and exclude. Only files in Nx's workspace file index (.gitignore and .nxignore applied) count, so a gitignored generated module that a build left on disk is neither linted nor keyed, and the lint target is the same in every checkout of one tree. Rust-only source trees keep their Cargo validation and manifest checks without invoking ESLint on nonexistent JavaScript inputs. Test-config diagnostics retain TypeScript's parsed-command-line types through bootstrap emit; invalid declarations fail with their diagnostic code and message, while an empty test program (TS18003) contributes no test files.

The ESLint command names no file. It is eslint {eslintSources}, and the target's eslintProjectRoot option names the project whose sources the placeholder stands for. A graph holds the files of the moment it was computed, and the daemon serves it past a deletion or an addition its watcher never reported: a list in the command made ESLint refuse a pattern no file matches, and left a new file unlinted, until the graph recomputed. The all-verdicts executor replaces the placeholder when the command runs, with the files the rules above select out of a fresh workspace file index: a new native context over the filesystem, never the table of the process or of the daemon, which learn of a write only when a watcher event reaches them. A project with none passes without starting ESLint. Adding JavaScript/TypeScript sources or changing the declared test program changes what the next run lints, with no graph computation in between.

The lint inputs key the same program without naming each file: the test declaration and its existing extends chain, one glob over the JavaScript and TypeScript kinds for each directory the declaration's include patterns walk (none for a directory outside the workspace or inside a build output directory), and every other program file by name. A file the program gains is keyed the moment it exists. A glob also covers what exclude removes from the program: keyed, never linted. The target's metadata.eslintFileSet retains the command and the workspace-relative file list the graph resolved, for cache inspectors: the list describes that graph, and what runs is read when it runs. @smoothbricks/nx-plugin/eslint-file-set owns its type and generated validator; an inspector can resolve each file's actual ESLint parser program instead of guessing from the project or target name.

The lint target runs its commands with this plugin's all-verdicts executor, not Nx's run-commands, which in parallel terminates the other commands once one fails and serially never starts the ones after a failure: one tool's errors would hide another's until a later run. all-verdicts starts every command at once, and none ends another. Each command's output prints as one block when it closes, then every command's verdict: PASS, FAIL (exit n), or UNJUDGED with its cause when a signal ended it, it never started, or the run itself was signalled. A signal to the run ends each command's process group, and the run then ends by the same signal. The target passes only when every command passed; an empty command list is refused, since it could never fail. The options are commands, cwd and, for a project with an ESLint command, eslintProjectRoot; a command's block and verdict name it as it ran.

Lint inputs include dependency production inputs because type-aware ESLint reads dependencies' declarations. This uses ^production unless the workspace opts into the versionless production input described below. Keep targetDefaults.lint limited to shared cache policy or additive inputs such as ["...", "^production"]. Smoo's workspace policy removes static lint executors, commands, dependencies, outputs and replacement input lists because they override source-aware inference for every project. Project-local nx.targets.lint declarations retain normal Nx override precedence.

The workspace ESLint configuration keys every lint target with the config files and exactly the workspace files they load, read the way ESLint loads them (src/eslint-config-closure.ts). Under Node, ESLint loads a TypeScript config through jiti and any other config with a native import(), and every module a config reaches loads the same way. For a jiti config the walk runs jiti's own transform, so an import that only types use is erased exactly as jiti erases it, and jiti's own resolver picks each file under the require or import conditions of the call. The walk runs in whichever process builds the graph (the Nx CLI, its daemon, smoo), so it restores Error.prepareStackTrace and Error.stackTraceLimit after each transform: jiti's Babel replaces both for the life of the process, and under Bun the wrapper it leaves makes every later Error.captureStackTrace onto a non-Error object throw (oven-sh/bun#46393), as follow-redirects' error types do when axios loads. For a native config import type and export type are erased, and the node that runs ESLint resolves each load with createRequire(module).resolve or import.meta.resolve(specifier, module), one process per walk layer. A builtin, or a package installed outside the workspace's own files, adds nothing: the project's bun lock-scope node among the check inputs already keys what is installed. A package that resolves into the workspace (a member linked into node_modules) is keyed and walked like a relative module. Every loaded file also keys the package.json files that decide how it loads: the nearest one (its module format), a member's own (its entry) and that of a directory a relative specifier enters (its main). A load whose specifier the source does not spell (require(name), import(path), module.require, module['require'], require.call/apply/bind, an alias of require, createRequire), a specifier that is neither relative nor a package name, one the loader cannot resolve, and a workspace file outside Nx's index fail graph construction with the config, the loading module and the load: lint cannot cache a verdict its key does not cover. A file beside the config's modules that no config loads keys no lint target.

Smoo's workspace policy also sets "cacheDirectory": ".nx/cache" in the root nx.json, and smoo monorepo check fails without it. With no NX_*_DIRECTORY variable set, as in a host shell, the pinned Nx otherwise moves the task cache and its database to a per-user ~/.nx/<id> keyed by the repository's remote, which every checkout of the repository shares. A configured cacheDirectory keeps the cache in the checkout's .nx/cache and the task database in its .nx/workspace-data (nx.json has no key for the database's directory). Cowshed links both into the checkout's build volume, and src/nx-state-location.test.ts checks where this repository's host Nx resolves them.

The plugin's own nx run nx-plugin:test aggregates four bounded Bun targets. Each uses Bun's native --timings=test-timings.json --shard=N/4 partition to balance measured per-file work, while normal discovery still runs every test file (including newly added files) exactly once. A failed shard fails the aggregate. Each shard, and test-host, is cached the way the unsharded test was: targetDefaults.test keys only the target named test, so a target split from it states "cache": true and its inputs itself (test-host also hashes the cowshed build it runs). smoo monorepo validate refuses a member of a cached aggregate that states no cache, since Nx then never caches it: the shards ran on every gate from 10-02 to 10-22 for that reason. The shard count and standard 120-second target deadline stay unchanged. Bun cuts the path-sorted files into four contiguous runs and costs a file the manifest does not name at the manifest's median, so files added without a refresh cluster into one shard and can push it past its deadline. The inferred shard-timings target makes that loud instead: for any project whose bun test targets carry both --shard=N/M and --timings=<file>, it replays Bun's discovery rules without running a test and fails naming every file the shards would run that the manifest does not name, with the refresh command smoo nx refresh-test-timings <project>. That command runs the suite once without --shard, adding --update-timings, and then repeats the check. Refresh after adding or slowing a test file; the manifest is scheduling data, not a test allowlist. The shards of one suite must share one cwd and every argument but --shard, pass flag values as --flag=value, and use no shell syntax beyond quoting; a shard command that breaks this fails graph construction, because the files it runs could only be guessed.

Real Nx fixtures own their cache and workspace-data directories and stop their daemon before removing the fixture. The daemon is the process its workspace record names, and its descendants, found by walking down its children; those get SIGTERM and are awaited. Nothing is signalled because it works in the fixture: where a process works says nothing about whose it is (specs/cowshed/11_shell.md, "Process identity and signal ownership"). A process that left the daemon's tree (a child its last act started, reparented when it exited) stays in the tree the job's supervisor observes, and is retired with the test command's subtree when that command ends (bounded-exec, below). No fixture helper reads a process it was not given: each starts from pids its caller holds (a child it spawned, a daemon's record, the test process itself) and walks down their children (pgrep -P on macOS, each parent's /proc/<pid>/task/*/children on Linux), then lists exactly those with ps -p, each under a deadline. A census of the user's processes by working directory or TMPDIR (lsof -d cwd, ps -E) asked every one of them, and one caught in an uninterruptible kernel wait held the census with it: measured 2026-10-25, lsof -d cwd slept 77.8 s on a cowshed fork's first write to its build image. What a signalled test process still runs of its own tree is named on stderr (descendantsDiagnostic) when it deletes its runs. Fixtures that intentionally strip caller directory overrides declare cacheDirectory: ".nx/cache" in their own nx.json; this keeps the native task database local too, rather than leaving a new ~/.nx/<repoKey> behind. Offline, path-only Cargo fixtures likewise own a temporary CARGO_HOME; production commands still keep the caller's Cargo home.

Fixtures the suite infers in its own process own their Nx state the same way. The package's test preload (src/__tests__/isolate-nx-state.ts) drops the NX_WORKSPACE_DATA_DIRECTORY, NX_PROJECT_GRAPH_CACHE_DIRECTORY and NX_CACHE_DIRECTORY the suite inherits from its job (CI's setup-devenv action, a cowshed job), so every fixture root resolves its own .nx. Nx's native file index shares one walk among every context of one workspace-data directory: the walk holds nx_files.lock there, and a context that waits on it loads the archive that walk wrote instead of walking its own root. Resolved through the job's absolute directory, a fixture's context gets the files of whichever root walked there last.

Fixture sockets and loopback listeners live where a Cowshed sandbox admits them, never at a literal /tmp path or on port 0. A fixture's short socket parent alias lives beside the Nx socket directory the job supplies (the parent of NX_SOCKET_DIR, the workspace's short runtime link), else under COWSHED_RUNTIME_LINK, else the temp directory. The alias points into the fixture's own physical .nx/sockets, outside every checkout; its nx socket leaf is a real directory. The foreign-workspace guard therefore sees the fixture as its owner, without a new grant or identity rule. A shell's variable counts only when it belongs to this checkout, as nx-socket-dir.sh decides it; one that leads elsewhere or cannot be looked at is passed over, and a refusal names it. The temp fallback admits a user-owned parent or the root-owned public sticky directory with mode 1777, not an inaccessible sticky directory. A socket path over 95 UTF-8 bytes is refused with an error naming it. The shell owner retains a failed socket-directory creation's exit status; the foreign-workspace provider refuses before running a target and preserves its complete diagnostic. fixtureNxEnv gives each fixture its own NX_SOCKET_DIR; retirement stops its daemon before unlinking its verified parent alias and removing the fixture root. A test whose subject is two daemons names a distinct real leaf for each. A listener another process connects to (the counted Cargo broker, a remote-cache server) binds through bindListener: an odd-offset service port of the workspace's COWSHED_PORT_BASE/COWSHED_PORT_BLOCK_SIZE block, lowest first, skipping only ports in use, and port 0 outside a block. A client that must be refused gets claimRefusingPort: the even session port of a service port the test holds, which nothing binds (specs/cowshed/08_testing.md), never a port bound and closed, which the next allocator in the block binds.

On macOS the working-directory census invokes /usr/sbin/lsof explicitly, independent of a test or devenv PATH. A census that cannot be read says so in its diagnostic instead of naming no process; it never fails cleanup, since it decides nothing. Only lsof's exit-1 process-disappearance race retains the partial inventory it produced.

The provider-real-Nx snapshot walk uses asynchronous children so its case timer can run while a producer or capture is active. Its 60 s case guard covers one provider, daemon lifecycle and all provenance/refusal checks; it does not split those checks into separately rebuilt fixtures.

Ignored-lockfile validity uses one daemon-successor walk: first refusal at the request boundary, then the successor's request-free safety-poll invalidation. Both boundaries keep their original observations without a second fixture start.

Daemon-validity readiness waits for an actual successful PING under its existing 30 s guard, probing every 50 ms; socket-directory events are not readiness because a bind/listen or closed-connection transition need not emit another one. Expiry reports the expected and current daemon PID, observed process state and descendants, and the daemon log's last 60 lines before fixture retirement removes them. The record-change and restart oracles remain event-driven.

Cargo Workspace Layouts

The plugin discovers Cargo workspaces beside Nx project manifests at any depth:

  • Workspace owner: the project beside a workspace Cargo.toml owns one cargo-fetch, cargo-test-build, cargo-lint, cross-check, mutation and bench target. Its cargo-lint and cargo-test aggregates reach all workspace crates, including a root [package]; cargo-test is the ordinary suite, and the project's test also reaches the serial test groups (below).
  • Crate owner: the deepest Nx project directory containing a crate owns cargo-test-<crate> targets. Its cargo-lint delegates to the workspace owner's one verdict, and lint never depends on the workspace test suite. Projects can own multiple crates. A nested independent workspace has its own prerequisites and does not inherit those of an outer workspace.
  • Commands: validation runs from the governing Cargo workspace directory. One cargo fmt --all --check and one cargo --frozen clippy --workspace --all-targets --target-dir target/cargo-lint --config 'build.warnings="deny"' cover every member. Clippy of a crate is a check build of its whole closure, so a target directory per crate rebuilt every shared dependency once per crate — 49 closures and 11 GiB on one repository — and no per-crate cache hit repaid it. Clippy writes its own target/cargo-lint, so the test build, the Wasm and N-API builds and every other dev build never queue behind a workspace-wide check on Cargo's build-directory lock. The price is that clippy compiles its own host units (build scripts, proc-macros) once per directory. cargo-lint-cross keeps target/cargo-lint-cross for the same lock reason: a foreign triple's units are distinct anyway, and its own lock keeps the cross check from queueing behind host builds. Warnings are denied by Cargo's build.warnings, not -- -D warnings: arguments after -- are clippy-driver's CLIPPY_ARGS, which enter every member's fingerprint, so a -- -D warnings gate would fingerprint every member differently from a plain cargo clippy --workspace --all-targets. build.warnings also fails on warnings replayed from Fresh units, so the gate still fails when a warning replays instead of recompiling. Cargo's own lock serializes the invocations that share a directory.
  • Retention: Cargo never deletes a unit generation a later build superseded, so a busy checkout's target directory only grows. After each Nx run that executed a Cargo producer (an inferred target that builds, checks or tests into the target directory, marked by its cargoProducer metadata: cargo-test-build, cargo-test, cargo-lint, cargo-lint-cross, mutation, bench, and the napi and wasm builds), the plugin's postTasksExecution runs its Cargo retention hook, which starts a pass of @smoothbricks/cargo-retain for each Cargo workspace that is due for maintenance, in a process of its own (below). In an mbx project, whose canonical environment names MBX_CACHE_DIR beside MBX_TARGET_VIEWS=1, the targets are mbx's to collect and the hook does nothing. Automatic passes run at most once per 24 hours after a successful completed pass, one that reclaimed in every root; its existing receipt records completion time and configuration/workspace/target identity. A not-due hook reports that scheduling decision without locks, a fingerprint scan, cleanup or receipt replacement. It does not claim newer outputs were inspected or the graph is unchanged. Busy, refused, invalid, foreign or backward-clock receipt evidence never postpones the next attempt. The first executed producer after the interval runs a current pass; cache replays, fetch and noop tasks start none. Manual preview and apply always run fresh. Due passes reclaim superseded generations under <profile>/build/<package>/<META>/. They keep the newest generation, by last use, of each unit identity of a workspace member (identity is every fingerprint field but its edges, source state and toolchain, which is rustc and the --sysroot flag; integers read exactly, so a newer toolchain supersedes an older one), every unit used in the last 24 hours (-Zmtime-on-use stamps each use; cowshed exports CARGO_UNSTABLE_MTIME_ON_USE=true to every host shell and job of a Cargo project, and a repository outside cowshed sets [unstable] mtime-on-use = true), and the transitive dependency closure of those across every profile, host and cross alike. A root whose hash files show no stamp (none newer than its compile by more than 24 hours) reports recency-unknown: mtime-on-use off: its last use is then only its last compile. A profile that holds a legacy layout beside the current one is read for the current one, and its legacy entries are retained as legacy-layout. A pass holds every profile's Cargo locks without waiting: one held lock skips the pass as busy, and a profile it cannot read (unreadable fingerprint, missing build/ or lock, or a legacy layout's records alone) refuses the whole pass, as does a lock it cannot attempt at all, with the reason named: under Node, macOS takes its flock(2)s in-process (open(2) with O_EXLOCK | O_NONBLOCK) and starts no helper, and other platforms hold them in a perl child, so a sandbox that refuses to exec perl, like a lock file that cannot be opened, refuses the pass instead of failing the run. The compiled CLI opens and locks through a small C wrapper (native/flock.c) that captures errno immediately. Each provider's canonical refusal retains the operation, exact lock path, actual host errno and OS cause: only contention is busy; any other failure refuses the pass. The hook only reports outcomes and never fails the run it follows: whatever escapes a pass is logged as a warning, and the next workspace is still tried. The pass itself never runs on Nx's thread: Nx awaits postTasksExecution, inside its daemon when one serves the run, and a pass unlinks synchronously, so one freeing 80 trashed generations on a loaded APFS volume held a land gate's daemon, and every client waiting on it, for more than thirty minutes. The hook answers only the gate there (not due, the retention lock busy or unattemptable, each reported as above), lets go of the retention lock and starts the due pass as the package's Node CLI, smoo-nx-cargo-retain --apply --automatic, detached (startRetentionPass, launch-node.ts). Its standard streams are /dev/null, and it inherits no descriptor of the run's, so neither the run's exit nor a reader of its output waits for it, and the daemon goes on serving. That process goes through the gate again, takes every lock itself and records its outcome in the receipt; while it frees its trash it holds the retention lock, so the next run's gate reports busy and starts no second pass. It moves each reclaimed generation into the profile's .smoo-retention-trash/ under that hold, recording its inode first, and unlinks it after letting go. Each proven generation gets a synchronous, non-symlink-following inode census and one native recursive removal, not an awaited lstat/unlink for every artifact. The census counts internal hardlinks once; the pass ledger drops an inode only when its last link is removed, so bytes freed and bytes still linked outside the removed generations remain distinct. The manifest's root identity is rechecked immediately before removal. A failed removal is an I/O failure, not a successful reclaim or a new completed maintenance receipt: the receipt says failed with the reason, as it does for anything else that fails an applied pass past its gate. It writes its last report to <target>/.smoo-cargo-retention.json. Cargo's root-relative fingerprint paths distinguish cross profiles from nested target roots under the lock; mixed, foreign or unprovable cross-profile roots refuse the pass rather than silently dropping their dependency edges. That scan finds a nested target root (target/cargo-lint, target/cargo-lint-cross) by locking and reading its depth-two profile like the target's own, so a pass held or refused there reaches no nested root. Each nested root then gets the same pass over its own profiles, host and cross, under only their Cargo locks and the target's one retention lock, one root after another: units never depend across roots, so a nested root that is held (a lint running into it) or refused is skipped and reported in the receipt's nestedRoots without holding up the others, and the receipt schedules the next automatic pass only when every root reclaimed. Only generations are reclaimed, never a root or a profile, and a nested root holding no unit of a workspace member is retained whole as foreign-root. A pre-created target need not carry Cargo's optional backup tag; a present but invalid tag still refuses the pass. Every pass still reads the current fingerprint graph. The locked scan reuses directory-entry types and reads its small metadata files synchronously, avoiding duplicate stats and one asynchronous dispatch per tiny file; it does not walk retained artifact contents or reuse an earlier graph that could miss in-place recompiles. That package's smoo-nx-cargo-retain [cargo-workspace-dir] previews the same selection, nested roots included, without changing anything. It is a tool run by hand, not an Nx target: its verdict is about this machine's disk, not the commit, and no input could key it. Each profile's incremental/<crate>-<id>/ directories go in the same pass: rustc keeps one per build variant and never removes the one a feature, profile or flag change superseded. One is kept while its newest finished session started inside the compile window (invoked.timestamp to the hash file's last write) of a kept unit of the same crate, while it started within the recent-use window, or while the directory holds an unfinished or unreadable session. The split-debuginfo objects rustc keeps inside current generations (rust-lang/rust#161824) are not reclaimed, so a target directory is not bounded by them. Empty fingerprint hashes are kept as unfinished compilations. The preservation proof covers Cargo's current unit and dependency records, not every debug reference in copied binaries or other checkouts. In particular, a binary copied up before a failed link can outlive the generation its debug map names; this pass does not establish that machine-wide debug closure.
  • Scheduling: inference never throttles Nx. It sets no parallelism: false or parallel: false, no cap, and no edge whose only purpose is keeping two cargo writers off one target/: Cargo owns that lock, and a second process waits inside cargo. An edge states a real consumer — fetch before frozen cargo, the test build before the runners, napi-debug before the tests that load its addon. A producer and its consumer inside one task are one && command (mkdir then the test build, cargo build then wasm-bindgen); independent commands (cargo fmt and clippy, per-manifest cargo fetch, Biome and ESLint) stay in a commands list that Nx runs side by side.
  • Overrides: normal Nx merging applies. Explicit nx.targets fields replace inferred fields, while omitted fields retain their inferred base. Use "dependsOn": ["...", "extra"] to preserve inferred prerequisites when adding an edge; replacing the array makes its author responsible for fetching before frozen Cargo commands and producing the test build before any runner consumes it. This repository also declares the test build explicitly in both APFS runners' owning project targets, so a cold checkout reaches it before their sweep runs its executable.

Member and exclude patterns may use * and ? in any path segment (for example, packages/*/crates/*). Unsupported patterns and member patterns that match no directory fail graph construction instead of silently omitting crates.

Cargo profiles

The default configuration of every cargo target compiles cargo's dev profile: every local build and test, CI validation and nx build share one target/ and one set of units, so cargo recompiles only what changed. The clippy gate and the cross check keep their own target/cargo-lint and target/cargo-lint-cross (see Cargo Workspace Layouts). The production configuration adds --release to the targets that build shipped artifacts (cargo-napi, napi-<arch>-<os>, cargo-wasm) and is selected for shipping: the generated publish workflow's build and platform-leg steps, the smoo release commands it runs, and the build and deploy runs of a production smoo github-ci nx-deploy. The land gate also builds the host production CLI and addon before its shipping-byte guard; ordinary build and test graphs never reach that seam. Test targets have no production configuration, so a release build never compiles release test binaries. A package-local target that builds a shipped artifact follows the same rule: its default options compile dev and its production configuration compiles a shipping profile. A shipping profile other than release is only for an artifact family no other build compiles, such as a size-optimised wasm32 lane; a profile that compiles the same crates for the same target as dev or release compiles every dependency a second time.

Native profiles have disjoint output trees: development CLIs live under .cache/bin/<platform>/ and development platform addons under .cache/native-dev/<platform>/; production retains the shipping dist/bin/ and dist/native/ layout. Declarations interpolate the same options the commands use, so their outputs and restored input receipts move together. The host-resolved cli-native and napi-native aggregates delegate to the existing platform producers and declare no outputs of their own. Their production configuration forwards that same selection to the host producer. A test that executes the CLI or loads the ordinary platform addon names those producers; tests using the explicit test addon still name napi-debug. The root gate target depends on cowshed:cli-native and cowshed:napi-native in their production configuration (a dependsOn entry's configuration, a hunk of the carried Nx patch), so the land gate's one Nx graph builds them beside its other tasks and replays them when their inputs did not move. The land host then runs the dependency-free cowshed:release-check, cached on a runtime digest of the shipping bytes it inspects, never on a development output.

After the fast-forward, main's output restoration explicitly requests build, cli-native, and napi-native. The normal build remains JS/dependency-only; this post-land seam restores the source-selected host CLI/addon and their input receipts for linked consumers. Each concrete producer remains cacheable and recompiles only changed inputs.

Artifact loaders select the profile from package source authority, never from which artifact happens to exist. A checkout contains a tracked source entry that the package never publishes and loads the development artifact, even when its JavaScript runs from dist/ts. A published package or sealed snapshot omits that authority entry and loads the shipping dist/ artifact. A missing selected development artifact fails loudly with its cached Nx producer remedy; it never falls back to an older shipping artifact. Developer-link input-receipt checks run once at shell entry and cover the selected development outputs, not once per CLI execution. Snapshots without that authority entry request the producer's production configuration so their artifact bytes and public paths agree.

Rust LMAO's opt-in source-revision contract is defined in 01f. Its native and Wasm targets declare { "env": "LMAO_GIT_REVISION" } as an input. Default dev/test runs leave it unset; publishing jobs set it to the dispatch commit, so those builds include the explicit revision in their key without making dev runs commit-keyed. The production configuration selects the shipping profile; it does not discover a Git revision. The root workspace's cached test build hashes that variable too, because it compiles the LMAO test binaries.

Cargo cache boundaries

Every inferred cargo target that runs work is cached, cargo-fetch included. Its download lands in CARGO_HOME, outside Nx outputs, so its key names the registry's state instead: the manifests, the lock and the Cargo configuration it reads, and a runtime input that is fetched when an offline cargo fetch --locked of each manifest succeeds. A registry missing a locked crate gives a value of its own run, which no cached run matches, so the fetch runs; a complete one makes an unchanged lock a cache hit. cargo-test-build is the one cached cargo build, and it is the step that compiles the test binaries ahead of the bounded runners; there is no separate cargo test --no-run warm-up, since it compiled the same units the test build does. Cargo's own incremental cache avoids recompiling unchanged code. Nx never restores or collects shared Cargo build directories; lint and test results, and dedicated Wasm/N-API outputs, remain cacheable. mutation and bench key on the workspace's crates and toolchain like cargo-lint, mutation also on .cargo/mutants.toml and cargo mutants --version with mutants.out as its output: an unchanged tree measures nothing new. A cross-triple test runner keys on its archive's own bytes, the binaries it runs, since the machine that runs it did not build them.

Validation inputs include the crate's files, every file its sources compile in through a literal include_str! or include_bytes! path outside the crate's directory (a shipped skill or a shared corpus beside the crates), the crates Cargo builds for its tests, governing manifests and configuration, the declared toolchain identity, and the toolchain's own reported versions. The identity has two halves. The pin is devenv.lock, hashed as an ordinary file input at both {workspaceRoot}/devenv.lock and {workspaceRoot}/tooling/direnv/devenv.lock — devenv resolves it into the rustc, cargo, linker, C toolchain and SDK every cargo command inherits, so bumping it invalidates every cached cargo target. The lock names inputs, not what the shell builds from them: a patch to the linker moves no byte of the lock and no line of rustc -vV, yet it changes what links every unit. The other half is a runtime input, smoo-nx-cargo-toolchain, that names the Nix store object of each program the shell declares for Cargo to compile and link with: the linker of each target (CARGO_TARGET_<TRIPLE>_LINKER, which Cargo hashes into every unit) and the programs cc-rs runs for build scripts (CC, CXX, AR and RANLIB: generic, per target and in their HOST_ and TARGET_ forms, each declared rerun-if-env-changed). The wrapper devenv builds for a Linux linker names its clang and its lld, so one entry covers all three. The object moves when the toolchain moves, once, and nothing else in the shell moves it. The shell's Nix sources are not hashed instead: the managed module alone changed 43 times in the 30 days to 2026-10-26, and each edit would have rekeyed every cargo verdict; evaluated at those commits on Linux x86_64, the observed store objects did not change. rustc and cargo are not in the list on purpose. Cargo keys units on the output of rustc's version command, which is hashed beside this input, never on rustc's path. On a non-Linux host the default shell and its linux-cross profile carry different rust-std sets, so that path would split one target's key between the two shells. Only a path in the Nix store counts; a path elsewhere is ignored. A linker named only by Cargo configuration is outside this environment probe: a target that runs one must name the script itself as an input. Workspace test runners additionally hash the manifest of every package the workspace build compiles inside the Nx workspace (a member's, and a path package's that reaches the build only as some crate's dev-dependency), because those affect unified features, and the nextest executable/configuration. While the build compiles a path package outside the Nx workspace, every runner also names externalRustCrates, which then must be declared: a runner's key does not follow the test build's outputs.

Nothing machine-local takes part. The ambient cargo environment as a whole and the global $CARGO_HOME/config.toml are deliberately not hashed: their values carry per-checkout state directories (CARGO_INSTALL_ROOT moves under the active profile) and the home directory's configuration, which would make every cache entry private to the machine that wrote it and make one target hash differently inside a devenv profile than outside it — enough to make a pre-push cross-compile probe unsatisfiable. The one thing read from the environment is the Nix store objects of the shell's declared programs, above: an object agrees on every machine of a platform, and a path outside the store is ignored. Keep explicit inputs for arbitrary build-script reads or custom environment variables that Cargo metadata cannot name.

Cargo itself keys a build script's run on every variable the script names with rerun-if-env-changed (cc-rs names CC_<target> and its kin), so an ambient value Nx does not hash still reruns the script and rebuilds everything that depends on it. The @smoothbricks/nx-plugin/cargo-build-script-env export reads those records: buildScriptEnvironmentDrift(profile, sees) returns each variable whose recorded value differs from what Cargo would see now, with both values and the packages whose scripts read it; cargoSees(files, environment) is that view over the configuration chain cargoConfigFiles(directory, cargoHome) lists (the deepest [env] entry decides: forced, else the environment's value, else the entry's), and runBuildScriptEnvironmentDrift([checkout]) prints the drift of <checkout>/target/debug. Variables Cargo sets for the processes it runs (CARGO_*, OUT_DIR and the other documented build-script names) are not compared: a run that recorded one was started by a cargo a Cargo-run process spawned. tooling/land-host.sh calls it through tooling/land-cargo-env.ts before each gate (cowshed specs/cowshed/16_build_volumes.md, "One Cargo environment on every path").

A target whose inputs a repository DECLARES replaces the inferred list, toolchain identity included. Name cargoToolchain in that declaration, or in the named input it uses: inference defines it, in every project that has inferred or declared cargo targets, as the whole identity. A restated lock is not enough, because the identity also holds the runtime input no fileset can state, and smoo monorepo check refuses a cached cargo target that does not reach the name. The replaced list also held the target's externalDependencies entries described below; restate them ([] for a target that runs only cargo, and the lock-scope node of its crates), or every lockfile change re-keys the target.

Every cached cargo target names the npm packages its command runs as externalDependencies: none for cargo itself, and @smoothbricks/nx-plugin for a target that runs its executor or reads its installed files (the nextest tool config, the test-binaries helper). Without that input Nx keys a task whose executor is not an @nx/ one on every package in the lockfile, so a lockfile-only commit re-ran every cargo verdict. Inside this repository the plugin is a workspace project, which Nx refuses as an external dependency, so the list is empty here. The plugin tells the two apart by the workspace's own node_modules/@smoothbricks/nx-plugin: one that resolves inside the workspace and outside every node_modules is a workspace project, whatever path the running process loaded the plugin's code through (a daemon of this repository once loaded it through one, and the land gate refused the graph). A second entry names the Cargo lock-scope node of the crates the target builds (cargoClosure carries it; a target that names neither gets the node of the directory it runs in). An entry naming only cargo-lock: nodes does not count as naming npm packages.

No input declaration removes a project's own configuration from a task's key. Nx 24.0.3 adds ProjectConfiguration to every task (hash_planner.rs gather_self_inputs), and hash_project_config.rs hashes the executor, outputs, options, configurations and parallelism of every target in the project, plus its tags and named inputs (not inputs, dependsOn, cache or //). An edit to one target's command therefore re-keys every target of that project, including each inferred cargo-test-* runner. Only the project a target lives in decides that sensitivity.

Workspace policy defaults non-production tasks to ["{projectRoot}/**/*", "!{projectRoot}/**/*.md"], without a forced CI workflow input. It migrates the old CI-only sharedGlobals reference out of default and never re-injects that definition. Custom non-CI named globals and explicit workflow-target inputs remain repository-owned and unchanged; workflow checks can still name their own CI inputs.

Policy keeps Rust crate sources out of production: smoo monorepo validate refuses a positive entry beneath {projectRoot}/crates, ending in .rs or naming a Cargo.toml, and smoo monorepo update removes it. TypeScript targets see Rust only as the producer target's outputs, through dependsOn and dependentTasksOutputFiles; a target that reads crate source as text names it in its own inputs; cargo targets are keyed by cargoClosure. Update removes the entry rather than keeping it, as it rewrites a broad production to the default: whatever reads crate source declares it on its own target.

Policy excludes incidental Markdown through !{projectRoot}/**/*.md in default and production. A task that consumes Markdown declares it through an input set without a matching negative: Nx exclusions win over positive paths in the same fileset, so a later explicit path cannot re-include it. Inferred Cargo package filesets remain broad for arbitrary compiled data, headers, schemas and build-script reads; incidental Markdown beneath a crate can therefore still invalidate those tasks conservatively. Nx 24.0.3 hashes a cached target that declares no inputs over every file under its project root and ignores the default named input, so smoo monorepo check refuses such a target: declare its inputs, or give its target name a targetDefaults entry with "inputs": ["default", "^default"].

TypeScript toolchain inputs exclude Markdown beneath workspace-root patches/, but retain every other patch byte and pin, including extensionless PIN and VERSION files. Native content-hash regressions distinguish those inputs from README and unrelated-source edits. A task that consumes Markdown declares it through an input set without a matching negative: Nx exclusions win over positive paths in the same fileset, so a later explicit path cannot re-include it. Inference adds that fileset only to the targets it infers. A declared cached target that runs an installed patched package (a bun or node command, the patched compiler binary, a bundled or site build over a patched dependency) names the same fileset in its own inputs: patches are applied to node_modules at install, which no fileset reaches, and the lockfile records the patch path, not its bytes, so without the fileset a patch edit replays the previous verdict or output from cache. Inferred Cargo package filesets remain broad for arbitrary compiled data, headers, schemas and build-script reads; incidental Markdown beneath a crate can therefore still invalidate those tasks conservatively.

Cargo keeps the caller's CARGO_HOME. Moving configuration into an isolated home can change relative paths or lose source replacement, credentials, and toolchain settings; forwarding it with --config changes precedence. Registry access may consequently wait on Cargo's package-cache lock, which Cargo arbitrates itself; Nx adds no ordering for it.

Graph inference persists locked, offline Cargo resolution in Nx's workspace-data directory. Its content key includes the canonical workspace and manifest paths, the complete indexed manifest set, local path-dependency and governing manifests, lockfiles, Cargo configuration (including ancestor and CARGO_HOME configuration), toolchain pins, the resolved Cargo executable, and explicit rustup toolchain selection. Changing only Rust sources, other files, or file timestamps reuses the resolved closure without starting Cargo. The configuration identity here is a local resolution-cache key, not a machine-specific task input.

Concurrent graph computations share one in-flight resolution per workspace. A native Nx file lock serializes workers; after acquiring it, a worker checks the persisted result again before starting Cargo. A caller with changed inputs waits for the current flight and resolves the new content rather than accepting the old closure. Successful cache writes use atomic rename; failed resolutions are never cached. A corrupt cache is reported and resolved again. Inputs that change during resolution are retried at most three times, then refused with the cause intact. A WASM-only Nx host cannot provide the native lock and receives a typed refusal rather than an unserialized fallback.

A cache-publication refusal retains the original filesystem cause, logical directory/lock/temporary paths, and parent device/inode observations immediately before lock construction, at the temporary open, and after failure (or the exact stat error). This distinguishes a missing namespace from a rebound one without recreating directories, retrying publication or changing warm-cache behavior.

A persisted warm cache is checked before waiting for the cold-fill lock. Each Cargo resolution child and the asynchronous wait for a foreign resolution lock have a 300-second bound. A refusal names the child or lock, the workspace and elapsed bound; a timed-out child and its owned process group are killed. These bounds do not change the native synchronous lock acquisition or Cargo's own package-cache arbitration.

A plugin worker's normal exit, host disconnect, or handled termination kills Cargo children it still owns; on POSIX it kills the whole process group, including wrapper descendants. Standalone callers also forward unhandled termination signals to their owned groups. An uncatchable SIGKILL cannot run an exit hook; the kernel still releases its file lock.

The @smoothbricks/nx-plugin:cargo-resolve diagnostics channel reports { phase: "join", manifest } when a caller joins an existing flight. With no subscribers, inference neither allocates nor publishes an event. The cache regressions use this readiness signal and Cargo control-channel events to prove concurrency and child teardown without sleep-based timing assumptions; their outer hang guards report still-open test spans and process/gate state. The counted Cargo fixture delegates to the command selected before its shim was installed, with that original search path, so nothing it starts rediscovers the counted shim and recursively enters the broker. The real-producer regression covers both counted metadata and forwarded workspace discovery, with one metadata child.

Keep workspace-owned runtime state outside Nx's source index. The root .nxignore ignores every dot path with .* and re-includes each tracked dot input by its dot-named prefix, such as !/.github or !/packages/lmao/.cargo: a dot-named directory must be re-included to reach its tracked children. Nx's watcher reads only the root .gitignore and .nxignore, not nested ignore files or Git's .git/info/exclude. tooling:lint-dot-inputs is an additive lint dependency, keyed on the tracked file inventory as well as ignore-file contents. It fails when a tracked dot path lacks its !/ line or Nx's ignore checker excludes it, so a newly tracked dot input cannot silently disappear into .*. nx-dot-input-policy.test.ts guards native index and file-hash parity for existing tracked inputs, unchanged project graphs across the policy cutover, and actual daemon graph-pass silence for .icm* stores and hidden Bun staging. The .nxignore file itself intentionally has new bytes. A tracked dot edit still reaches the daemon's watcher and graph recomputation. Watching daemon logs, plugin sockets or job records instead turns each graph computation into another file event and another graph computation.

Runtime inputs

Every runtime input, inferred or declared, starts with sh node_modules/@smoothbricks/nx-plugin/runtime-input.sh. Command-specific environment assignments follow the wrapper as env NAME=value command ..., so setup failures and command failures use the same fail-closed path. For example:

{ "runtime": "sh node_modules/@smoothbricks/nx-plugin/runtime-input.sh bun --version" }

Nx 24.0.3 hashes a runtime input's stdout and stderr and ignores its exit status, so a failed input becomes a valid key: its error text, one string for every history, toolchain or file it could not read, and a result cached under it is replayed for all of them. The script keys on the command's stdout alone and turns a nonzero exit into an Nx error: it ends stdout with the byte 0xFF, and Nx refuses output that is not UTF-8 (invalid utf-8 sequence of 1 bytes from index N, exit 1). That native message names neither the task nor the input; the carried Nx patch's hasher rethrows it as Could not hash <tasks>: invalid utf-8 sequence … and lists each runtime input of those tasks with the tasks that hash it (patches/README.md, "A hash failure names its tasks and runtime inputs"). It names the candidates, not the one that failed. Nx drops a runtime input's stderr, so a failure that only wrote to stderr would be invisible. The wrapper retains each failure under the checkout's .runtime-input-failures/failure.XXXXXXXXXX/, outside Nx's state and the temporary directory. These private records survive nx reset, task-cache cleanup and temporary-directory retirement; they remain until the operator removes them. The reliable diagnostic after a daemon or headless gate's decoder error is:

cat .runtime-input-failures/*/report

smoo github-ci nx-smart, nx-run-many and nx-deploy print every report retained during a failed Nx run (runtimeInputFailureReports(root, sinceMs), @smoothbricks/nx-plugin/runtime-input-policy), so a CI log names the command and its cause beside Nx's error, which names only the candidates.

Each readable report names the command, its indexed arguments, working directory, exit status and original stderr. argv retains the exact NUL-delimited argument vector (including empty arguments), stderr retains the original bytes (including trailing newlines), and exit-status retains the decimal status. The wrapper also attempts a stderr summary naming the command and report path, and displays the report on /dev/tty when it can open a controlling terminal. Nx's daemon has none (ENXIO) and cowshed's sandbox denies it (EPERM); having no terminal is no failure of the input, so the shell's refusal of the open joins the stderr summary and the record gains nothing. Neither display is guaranteed through Nx's daemon or a headless harness: rely on the retained report, not on Nx forwarding stderr. A write that fails on a terminal that did open is retained in the record's terminal-error. Records can contain arguments or stderr with credentials; keep them private and inspect before sharing. Evidence-storage errors name the problem and the surviving stderr capture on stderr; they never remove the refusal byte or replace the command's status.

Successful commands still emit only their original stdout bytes, discard stderr and retain no failure record: no evidence path, timestamp or diagnostic enters a successful key. runtime-input.test.ts compares that byte stream and its hash with the previous success path, and plants a failure through real Nx (daemon and daemonless) to prove both refusal and retained evidence.

A runtime input runs only programs the workspace declares: devenv's Nix profile, the checkout's node_modules/.bin and its own scripts. Nx hashes on a developer's Mac, on a NixOS CI runner and inside cowshed's sandbox, and only those are the same in all three. A program the host's operating system ships is not: shasum is a Perl script that macOS ships in /usr/bin, not coreutils, and no devenv profile provides it, so an input that named it hashed on every Mac and exited 127 on a NixOS runner. Hash a file with Bun, as the plugin's own file inputs do (Bun.CryptoHasher); devenv's coreutils provide sha256sum where a pipeline needs one. The only host paths admitted are /bin/sh and /usr/bin/env, which NixOS keeps too. @smoothbricks/nx-plugin/runtime-input-policy exports hostPrograms(command, { path, root, home }): each program the command runs (through the wrapper, env, exec, command, sh -c and command substitutions; a script file it runs is that script's own concern) that PATH finds outside /nix, the checkout and the home, that nothing on PATH provides, or that only a link in the checkout's node_modules/.bin to a build output of one of its own packages provides. Bun links a workspace package's bin only when its target exists at install time, and a fresh checkout installs before it builds, so such a name is absent on a fresh runner and present in every checkout that built first; run the file instead (node node_modules/@smoothbricks/nx-plugin/dist/bin/smoo-nx-cargo-hash.js, as the plugin's own Cargo input does). A consumer's installed package ships its build, so its bins are not refused. Each refusal is a tagged HostProgram, and hostProgramReason says it. runtime-input.test.ts judges this repository's declared inputs on tooling/direnv/repo-path --print, the PATH entries the devenv shell and CI give Nx (which hands a runtime input its own environment): the devenv profile and the checkout's directories, with no /usr/bin or /bin, so a program only the host provides fails on macOS as it does on the NixOS runner. A consumer judges its own declarations with the same export.

An input that runs inside a cowshed workspace's sandboxed daemon gets the client's environment, a host shell's HOME included, but not the client's filesystem view (cowshed 04_sandbox.md). An input that reads the operator's home fails there, so a runtime input reads only the checkout and its declared environment. A git command in one sets GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1: the key is a function of the repository, not of the operator's git configuration. Git reads the global ignore file (~/.config/git/ignore) even then, so smoo-nx-cargo-hash, whose git queries decide which package sources the key covers, also passes -c core.excludesFile=/dev/null.

Cargo source runtime inputs

For inferred Cargo targets in a repository-root Cargo workspace, declare the external-source input once:

{
  "namedInputs": {
    "externalRustCrates": [{ "runtime": "sh node_modules/@smoothbricks/nx-plugin/runtime-input.sh smoo-nx-cargo-hash" }]
  }
}

For a package-root Cargo workspace, pass its manifest relative to the Nx root: smoo-nx-cargo-hash packages/example/Cargo.toml. A shared input covering several Cargo workspaces includes one runtime entry per manifest.

By default, the command uses the content-keyed cargo metadata --locked --offline resolution and hashes external path packages, including transitive dependencies, Rust sources, target source files, governing Cargo manifests, and Cargo configuration. It also covers path packages under node_modules, which Nx's normal file map ignores. Packages inside both the Nx and Cargo workspaces are left to the plugin's inferred file inputs. Adding or moving a dependency does not require maintaining a second list of source roots. The command refuses missing dependencies or an unavailable locked dependency cache instead of emitting a partial digest.

Each Cargo child and each acquisition of the cross-process resolve lock has a five-minute CARGO_RESOLVE_BOUND_MS deadline. A warm content-keyed cache read takes no lock and spawns no child. A cold flight acquires the pinned Nx binding's blocking kernel lock in one owned child; its parent kills and reaps that child if acquisition exceeds the deadline. The named CargoResolveLockError reports the lock path, elapsed wait, bound and waiter pid (the holder pid is unknown). A successful flight holds the lease until closing the child's stdin releases the lock and the child is reaped. A timed-out Cargo command is killed, and resolution refuses instead of hanging graph construction or returning a stale closure.

Resolution expands shared ancestors once and reads current file bytes directly, without per-file asynchronous dispatch or canonical-path probes for missing files. Standalone calls retain a fresh native manifest census; graph callers use the graph's existing index. Canonical paths and contents still key metadata, including ignored and external packages; source bytes are read on every hash call, not kept in a runtime-result cache.

Cargo closure input

Custom Cargo targets that replace inferred inputs need the source closure of the crates they build. The plugin infers it as the cargoClosure named input of every package.json project with a member of a Cargo workspace under its root, where that Cargo workspace's root is itself a project (the root package.json, for a repository-root workspace). Name it in each custom Cargo target's inputs, alongside cargoToolchain, the target's own scripts and non-Rust inputs:

{
  "nx": {
    "targets": {
      "build": {
        "inputs": ["{projectRoot}/scripts/build.sh", "cargoToolchain", "cargoClosure"]
      }
    }
  }
}

Cargo decides the closure. Graph inference checks the content-keyed locked offline resolution of the local packages under the project's root and every mutable local package Cargo builds for them: their normal, build and dev dependencies, and below them only normal and build dependencies, since Cargo builds a dependency for its library and build script alone and never for its own tests. An edit to a workspace member outside that closure, a dependency's test helper included, leaves the task key alone. The members Nx's file index holds become filesets: each package's .rs files, Cargo.toml files and .cargo/config[.toml] below its directory (skipping target/ and the other directories the hash command skips), target sources outside it, its governing workspace manifest, the Cargo.toml and .cargo/config[.toml] of every directory from the package up to the Nx root, and the lock-scope node for the Cargo.lock entries the closure reaches (see below). Nx hashes those from the file hashes and node hashes it already keeps, so hashing these inputs starts no process. A graph with unchanged resolution inputs starts no Cargo process either.

Fileset reduction works from the set of package directories: a path is covered exactly when it or one of its ancestors is a member. Nested packages therefore need no extra subtree pattern, and shared ancestors are visited once. Sibling names accumulate into the same brace pattern without copying each growing prefix. The ordered filesets are unchanged; no new cache or retained index is introduced. For P packages and F distinct source paths of bounded depth, with L loose files, reduction costs O(P log P + F + L log L) rather than enumerating package pairs and rebuilding sibling prefixes.

bun packages/nx-plugin/benchmarks/cargo-closure-input.bench.ts measures the real async infere