Skip to content

feat(vfs-bundle): detect the package manager from its lockfile; .sol default entries for Soldeer - #200

Merged
ChALkeR merged 1 commit into
mainfrom
claude/affectionate-ritchie-1j9umy
Oct 2, 2026
Merged

ChALkeR merged 1 commit into
mainfrom
claude/affectionate-ritchie-1j9umy

Conversation

@exo-nikita

@exo-nikita exo-nikita commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

packageManager / --package-manager is now optional for:

  • buildVfsBundle;
  • buildGitHubBundle;
  • suggestedEntries;
  • stasis github-bundle;
  • loadNodeModules.

Default entries now follow the package manager's kind; for Soldeer they are the project's .sol entry points. Nothing in these builds follows the environment.

Detection

Without a package manager given, each supported one is checked for a lockfile in the directory it would install cwd from. That directory is found the same way lockfileRoot finds it:

package manager installs from
pnpm the workspace root, else the nearest pnpm-lock.yaml
yarn 1 the root that declares cwd's workspace, else the nearest yarn.lock
Soldeer the nearest foundry.toml / soldeer.toml, never above the git root

If exactly one package manager's lockfile is there, it is taken. If none or more than one is, the call is refused, and the error names what was looked for or found:

buildVfsBundle: no packageManager given, and none of pnpm-lock.yaml, yarn.lock, soldeer.lock installs /
buildVfsBundle: no packageManager given, and more than one lockfile installs /apps/p: /apps/p/pnpm-lock.yaml (pnpm), /yarn.lock (yarn1)
  • buildVfsBundle: chooses among all three, and returns the packageManager it built with.
  • loadNodeModules: chooses between pnpm and yarn 1, resolving a symlinked cwd first.
  • A package manager that is given is always taken. A packageManagerVersion without one is refused.

For a GitHub repo (buildGitHubBundle, suggestedEntries), the repo listings are read first: the directory's and those above it, each listed once.

  • No supported lockfile listed: refused before anything is downloaded.
  • One listed: that package manager is taken. The directory is still downloaded on its own where it stands alone.
  • More than one listed, or a directory no listing covers (a symlink): the whole repo is downloaded and checked.
  • A failed root listing: reported, not taken for a symlink's.
  • A directory's tree id: taken from the listing above it, which upstream holds to that directory's own id. This saves a walk from the commit for each subtree.

Option checks run before anything is fetched:

  • for the package manager's kind, as soon as it is known;
  • with neither entries nor a package manager, an option no kind takes is refused (e.g. --tsconfig without --typescript).

Default entries

  • pnpm, yarn 1: the package.json's entry points, as before.

  • Soldeer: .sol entry points by name and layout, following preventive/triage:

    • .sol files directly in the directory;
    • .sol files under contracts/;
    • .sol files under its source directory: the src of foundry.toml's [profile.default], else src/.

    Left out:

    • tests, scripts and mocks, by directory (test(s), script(s), mock(s)) and by *.t.sol, *.s.sol, *.test.sol, *.spec.sol;
    • what a project depends on or builds: lib/, node_modules/, dependencies/, out/, cache/, artifacts/, build/;
    • the directories no stasis walk descends into (isAutoExcludedDir): VCS metadata, examples, test scaffolding.

    A link to a directory is not followed. Triage takes src/ only beside a foundry.toml or hardhat.config.*, to tell a Solidity src/ from a JS one. Here the package manager being Soldeer already says that, and Hardhat projects are pnpm or yarn ones.

suggestedEntries takes packageManager and suggests exactly the build's default entries. Without a package manager, it detects one as the build does. For a repo, it downloads the tree the way buildGitHubBundle does, through the same code.

Environment

buildVfsBundle (and so buildGitHubBundle) no longer takes an env. A Solidity bundle is built with foundry.toml's default profile, whatever FOUNDRY_PROFILE, FOUNDRY_REMAPPINGS or DAPP_REMAPPINGS say.

stasis github-bundle reads only GITHUB_TOKEN. It hands --lockfile to the build's option checks, so --lockfile is refused for a Solidity bundle as soon as the kind is known:

  • from .sol entries, before anything is fetched;
  • once the listings or the tree tell Soldeer.

Other changes

  • repoTree: does the shared GitHub setup (repo checks, client, head commit, option checks, download, detection) for buildGitHubBundle and suggestedEntries.
  • checkAhead and KINDS: one up-front check, and a per-kind table holding the stand-in entry, the default entries and the "none found" message.
  • checkKind: refuses a kind no package manager builds. checkVfsOptions just returns the kind.
  • packageManagerFor: the given-or-detected package manager, shared by buildVfsBundle and loadNodeModules.
  • foundry.js: gains foundrySourceDir.
  • Docs: the usage text and the README are updated.

Tests

  • tests/vfs-bundle-detect.test.js (new):
    • each package manager detected;
    • another's lockfile outside where that package manager installs from, which is ignored;
    • a given package manager overriding detection;
    • none or several: at the root, nested, a pnpm workspace root without a lockfile, and pnpm beside Soldeer;
    • loadNodeModules choosing between pnpm and yarn 1, and through a symlinked cwd;
    • packageManagerVersion without packageManager.
  • tests/vfs-bundle-soldeer.test.js:
    • Soldeer detected from the fixture, byte-identical to the bundle built with it given, and refused beside a yarn.lock;
    • the suggested .sol entries building the bundle src/ does;
    • env ignored: FOUNDRY_PROFILE, FOUNDRY_REMAPPINGS and DAPP_REMAPPINGS change nothing.
  • tests/vfs-bundle-github.test.js:
    • the listing-first flow, with exact client calls: refusals from listings alone, several lockfiles settled by the tree, a symlinked directory, a failed root listing;
    • Soldeer .sol entries: what is included, what is skipped, the foundry.toml src, a source directory outside the project, a linked contracts/, and no entries refused;
    • suggestedEntries with a given or detected package manager, matching buildGitHubBundle's defaults;
    • early option checks with neither entries nor a package manager;
    • --lockfile with Soldeer;
    • the shell's FOUNDRY_PROFILE not reaching stasis github-bundle.

Rebased onto main (#202). node --run lint is clean and node --run test passes: 2204 passed, 3 skipped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KPSLumcb6Rdo4xXL5s5CpE

@exo-nikita
exo-nikita force-pushed the claude/affectionate-ritchie-1j9umy branch from dd9e5f3 to 5962d2b Compare October 2, 2026 06:36
@exo-nikita exo-nikita changed the title feat(vfs-bundle): detect the package manager from the lockfile that installs the project feat(vfs-bundle): detect the package manager from its lockfile; .sol default entries for Soldeer Oct 2, 2026
@exo-nikita
exo-nikita force-pushed the claude/affectionate-ritchie-1j9umy branch 2 times, most recently from 44a6ea1 to 1aa6769 Compare October 2, 2026 08:40
@exo-nikita
exo-nikita force-pushed the claude/affectionate-ritchie-1j9umy branch 4 times, most recently from 52ccacf to bd34faf Compare October 2, 2026 09:23
…default entries for Soldeer

buildVfsBundle, buildGitHubBundle, suggestedEntries and `stasis github-bundle` take
`packageManager` / `--package-manager` as optional, and so does loadNodeModules.

Without one, each supported package manager is checked for a lockfile in the directory it would
install cwd from, as lockfileRoot finds that directory:

- pnpm: the workspace root, else the nearest pnpm-lock.yaml.
- yarn 1: the root that declares cwd's workspace, else the nearest yarn.lock.
- Soldeer: the nearest foundry.toml or soldeer.toml, never above the git root.

If exactly one package manager's lockfile is there, it is taken. If none or more than one is,
the call is refused, and the error names the lockfiles that were looked for, or the ones found
with their package managers. buildVfsBundle chooses among all three. loadNodeModules chooses
between pnpm and yarn 1, so a soldeer.lock beside them is not counted. A package manager that is
given is always taken, whatever lockfiles are present.

buildGitHubBundle (and suggestedEntries for a repo) reads the repo listings first: the
directory's and those above it, listed once and shared with the subtree check.

- No supported lockfile listed: refused before anything is downloaded.
- One listed: that package manager is the only one that can install the directory, so it is
  taken. The directory is still downloaded on its own where it stands alone.
- More than one listed, or a directory no listing covers (a symlink): the whole repo is
  downloaded and checked.

In every case the choice is confirmed against the tree that was downloaded. Option checks that
need the package manager's kind run as soon as it is known; before that, given entries are
checked to be of a kind some supported package manager builds. buildVfsBundle returns the
`packageManager` it built with.

Default entries follow the package manager's kind. With pnpm and yarn 1 they remain the
package.json's entry points. With Soldeer they are the project's .sol entry points, picked by name
and layout as preventive/triage picks them:

- .sol files directly in the directory;
- .sol files under contracts/;
- .sol files under its source directory: the `src` of foundry.toml's [profile.default], else
  src/ (forge's default, or contracts/ when only that exists).

Triage takes src/ only beside a foundry.toml or a hardhat.config.*, to tell a Solidity project's
src/ from a JS one's. Here the package manager is Soldeer, which already says that, and Hardhat
projects are pnpm or yarn ones, so src/ is always taken. No profile but the default one is read,
and nothing from the environment.

Tests, scripts and mocks are left out, by directory name and by *.t.sol, *.s.sol, *.test.sol and
*.spec.sol. So is anything a project depends on or builds: lib/, node_modules/, dependencies/,
out/, cache/, artifacts/ and build/. Directories no stasis walk descends into (isAutoExcludedDir:
VCS metadata, examples, test scaffolding) are skipped too. A link to a directory is not followed.
Soldeer no longer needs its entries given.

suggestedEntries takes `packageManager`, and suggests exactly the build's default entries.
Without a package manager, it detects one as the build does, so it needs the lockfile the build
needs. For a repo, it downloads the tree the way buildGitHubBundle does, through the same code.

Options are checked before anything is fetched:

- Once the package manager is known, its kind's checks run.
- With neither entries nor a package manager, an option no kind takes is refused (e.g.
  --tsconfig without --typescript).
- A `packageManagerVersion` without a `packageManager` is refused.
- `stasis github-bundle` hands `--lockfile` to these checks, so it is refused for a Solidity
  bundle as soon as the kind is known: from `.sol` entries before anything is fetched, or once
  the listings or the tree tell Soldeer.

Listing details:

- A directory's tree id is taken from the listing above it, which upstream holds to that
  directory's own id. This saves a walk from the commit for each subtree.
- A failed root listing is reported, not taken for a symlink's.

Nothing in these builds follows the environment. buildVfsBundle (and so buildGitHubBundle) no
longer takes an `env`: a Solidity bundle is built with foundry.toml's default profile, whatever
FOUNDRY_PROFILE, FOUNDRY_REMAPPINGS or DAPP_REMAPPINGS say. `stasis github-bundle` reads only
GITHUB_TOKEN.

Detection resolves a symlinked cwd first in loadNodeModules, as buildVfsBundle does. foundry.js
gains foundrySourceDir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KPSLumcb6Rdo4xXL5s5CpE
@exo-nikita
exo-nikita force-pushed the claude/affectionate-ritchie-1j9umy branch from bd34faf to d02acff Compare October 2, 2026 09:43
@ChALkeR
ChALkeR merged commit c382e79 into main Oct 2, 2026
5 checks passed
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.

2 participants