Skip to content

fix(bundle): read package.json as Node does; remapping errors never quote; green main after #178 - #201

Merged
ChALkeR merged 3 commits into
mainfrom
claude/magical-davinci-bs3pob
Oct 2, 2026
Merged

ChALkeR merged 3 commits into
mainfrom
claude/magical-davinci-bs3pob

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Follow-up to #178. It makes two decided changes and fixes a test that fails on main because of how #178 and #197 combine.

A package.json that's there but can't be read is refused, as Node refuses it

host.stat returns null for any failure. That's right for its ~30 callers, which are existence checks during module resolution, forge's lib discovery and the Solidity walk. Node's resolver and Rust's Path::exists also treat any failure as "not there", so stat stays as it is.

Every package.json reader, though, also took a link loop, or a file behind a directory that may not be searched, for no package.json, and walked past it:

  • the strict Solidity reader, which bucketed a forge lib's files as the project's own (proj@1.0.0);
  • the submodule bucket, whose version silently fell back to the branch;
  • the resolver the Vfs host uses, and the Vfs host's findPackageJSON;
  • State's walk up to a bucket's name, and its root discovery;
  • readModuleManifest.

Node counts only "nothing there" as no package.json, a link to nothing included. Anything else that can't be read it refuses with ERR_INVALID_PACKAGE_CONFIG. Real stat draws the same line.

So when a reader's stat fails, a read now says why:

  • statStrict names the file from the project root: lib/dep/package.json: can't be read (ELOOP). It serves configs and the strict package.json read. readRegularFileOrNull uses it too, instead of rethrowing the raw fs error with its absolute path.
  • packageJSONStat throws Node's ERR_INVALID_PACKAGE_CONFIG. It serves the resolver, the Vfs host's findPackageJSON and State.

Lenient lookups (Bash, Rust, stasis add, --package-json) still walk past a malformed or unreadable one, as before.

Invalid-remapping errors never quote the remapping

An invalid remapping's error quoted it. The text is normally a remapping the project wrote. But a file named as a mapping by mistake can hold anything: --mapping=token.txt printed token.txt:1: invalid remapping "ghp_…" to stderr, and so into CI logs. A token pasted into FOUNDRY_REMAPPINGS did the same.

Errors now give the location and the expected form:

  • remappings.txt:2: invalid remapping, expected [context:]prefix=target
  • foundry.toml: `remappings` entry 2: invalid remapping, expected [context:]prefix=target
  • `remappings` entry 1 is not a string

This was the last error of stasis's own that quoted file content.

main was red after #178

buildGitHubBundle and suggestedEntries check their options against an empty Vfs before anything is fetched (#197). #178's directoryEntryError then found every extensionless entry missing from that empty tree. So src with pnpm was refused as "no such file or directory" rather than "only JS bundles are built with pnpm", and buildGitHubBundle reads nothing from disk fails on main.

The pre-fetch checks now pass fetched: false, which skips the missing-entry errors. buildVfsBundle still checks entries against the project's own tree.

Checks

  • A differential against Node's require.resolve and findPackageJSON agrees on every case: missing, a link to nothing, a directory named package.json, malformed, a self-loop, a loop through directories, and (run unprivileged) a link into an unsearchable directory.
  • The three new package.json tests fail on main. The remapping test also checks that a secret named as a mapping isn't echoed.
  • node --run test (Node 24): 2191 pass, 0 fail, 3 skipped, all 82 suites. node --run lint: clean.
  • Forge oracle: the scenario, profile, legacy-extends and test-case suites all match, as do 400 new fuzzed projects.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C6oBS5QX4oqZcd2d3STiGA


Generated by Claude Code

claude added 3 commits October 2, 2026 08:59
buildGitHubBundle and suggestedEntries check their options against an
empty Vfs, before anything is fetched: "what the tree is never decides
them" (#197). #178's directoryEntryError then found every extensionless
entry missing from that empty tree. So `src` with pnpm was refused as "no
such file or directory" rather than "only JS bundles are built with pnpm",
and main's "buildGitHubBundle reads nothing from disk" test fails.

The pre-fetch checks now pass `fetched: false`, which skips the
missing-entry errors. buildVfsBundle still checks entries against the
project's own tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6oBS5QX4oqZcd2d3STiGA
…, as Node refuses it

host.stat returns null for any failure, as Node's module lookup does. So
every package.json reader took a link loop, or a file behind a directory
that may not be searched, for no package.json and walked past it:

- the strict Solidity reader, which bucketed a forge lib's files as the
  project's own;
- the resolver the Vfs host uses, and the Vfs host's findPackageJSON;
- State's walk up to a bucket's name, and its root discovery;
- readModuleManifest.

Node refuses such a package.json with ERR_INVALID_PACKAGE_CONFIG, and
counts only "nothing there" as none, a link to nothing included. Real stat
tells the two apart and host.stat doesn't. That's right for the module and
lib probes, which treat any failure as absent as Node and Rust do, so stat
stays as it is. When a package.json reader's stat fails, a read now says
why:

- statStrict, for configs and the strict package.json read, names the file
  from the project root (`lib/dep/package.json: can't be read (ELOOP)`).
  readRegularFileOrNull uses it too, instead of rethrowing the raw fs
  error with its absolute path.
- packageJSONStat throws Node's ERR_INVALID_PACKAGE_CONFIG, for the
  resolver, the Vfs host's findPackageJSON and State.

Lenient lookups (Bash, Rust, `stasis add`, --package-json) still walk
past a malformed or unreadable one. A differential against Node's
require.resolve and findPackageJSON now agrees on every case: missing,
dangling, a directory, malformed, a self-loop, a loop through
directories, and (unprivileged) a link into an unsearchable directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6oBS5QX4oqZcd2d3STiGA
An invalid remapping's error quoted it. The text is normally a remapping
the project wrote, but a file named as a mapping by mistake can hold
anything: `--mapping=token.txt` printed `token.txt:1: invalid remapping
"ghp_…"` to stderr, and into CI logs with it. A token pasted into
FOUNDRY_REMAPPINGS did the same.

Errors now give the location and the form the entry should have:
`remappings.txt:2: invalid remapping, expected [context:]prefix=target`.
A foundry.toml entry is named by its position (`` `remappings` entry 2 ``,
`` `remappings` entry 1 is not a string ``). This is the last error of
stasis's own that quoted file content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6oBS5QX4oqZcd2d3STiGA
@ChALkeR
ChALkeR merged commit 25e52f2 into main Oct 2, 2026
3 of 5 checks passed
ChALkeR pushed a commit that referenced this pull request Oct 2, 2026
…4.21+ and 26.8+ alone (#202)

The differential against Node's own resolver (#201) expected Node to
refuse a package.json it can't read (a link loop, a loop through
directories) with ERR_INVALID_PACKAGE_CONFIG. Node does so since 24.21.0
and 26.8.0 (nodejs/node#65223, "module: report unreadable
package.json"); before, it took one for none and resolved past it. CI
runs the engines floor, 24.14.0, and 26.0.0 too, where the test failed.

The test now checks Node's answer against its two known behaviors by
version, keeps the differential where every Node agrees (a link to
nothing is none), and asserts ours refuses on every Node, as before.


Claude-Session: https://claude.ai/code/session_01GGr7zsXxPPC6Mrmtn7qyJG

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants