Skip to content

feat(vfs-bundle): suggestedEntries; github-bundle defaults to them and to the default branch's head - #199

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

suggestedEntries

@exodus/stasis/vfs-bundle exports suggestedEntries. It returns the JS entry points a package.json names, as paths from its directory. They are resolved the way the JS build resolves them under the same conditions, mainFields, metro, platforms, jsx and typescript, which are checked the way the build checks them. In order:

  1. The package's own entry, resolved as ./ resolves there. That is main as Node takes it, or, with mainFields or metro, the build's field resolver once per platform (Metro's main fields, platform suffixes and browser maps). For example, main: "index" gives index.ios.js / index.android.js.
  2. Each exports subpath, resolved through the package's name for both require() and import, with the conditions the build adds: the React Native ones under metro, plus browser on web. Patterns (./x/*) and folder mappings (./x/) are skipped.
  3. Each bin.

Under typescript, an entry point that resolution misses is mapped to its TS source the way tsc maps it, as the build's fallback does. Such an entry is one named by its compiled output; for example, main: "lib/index.js" gives lib/index.ts. A compiled .js that exists in the tree still wins over its source.

Only files inside that directory count, and only when the package.json names them from inside it. A main field or bin that points above the directory is left out, so a directory downloaded on its own gives the same answer as the whole repo. Without a package.json the result is empty.

await suggestedEntries({ vfs, cwd: '/packages/p', metro: true, platforms: ['ios', 'android'] })   // a project held in a Vfs
await suggestedEntries({ github: 'owner/name', sha, directory, client, typescript: true })        // a GitHub repo at a commit

For a repo, sha defaults to the head of the default branch. The directory is downloaded on its own when it is a plain directory in git with no symlink out of it; otherwise the whole repo is downloaded. Either way the bytes come from GitHub or from the cache, as buildGitHubBundle fetches them.

buildGitHubBundle / stasis github-bundle

sha / --sha and entries are now optional:

  • Without sha: builds the head commit of the default branch, as GitHub reports it (getRepoHead). That commit becomes the bundle's repo.commit, and the CLI summary prints it.
  • Without entries, for JS bundles (pnpm, yarn1): uses the entries suggestedEntries returns for the tree already fetched, under the build's own --conditions, --mainFields, --metro --platforms and --typescript. If there are none, the build is refused before anything is installed. The options are still checked before anything is fetched, against a JS entry, since every suggested entry is one.
  • Solidity bundles (soldeer): these still need their entries listed, and fail with an error before anything is fetched.

Shared code

Two pieces are pulled out so the default entries use exactly what the build uses. Both are pure extractions: stasis bundle and buildVfsBundle behave the same.

  • fieldResolverFor (cmd/bundle.js): the build's per-platform field-resolver setup.
  • typescriptExportsTarget (resolve-typescript.js): the fallback's mapping of exports targets. The fallback finds a package only under node_modules, so it can't map a package's own exports; this helper takes the package's directory instead.

The new module is stasis/src/vfs-bundle/entries.js, which is added to the package's files. The README row and the CLI usage text are updated.

Tests

tests/vfs-bundle-github.test.js covers the following. The fake client gains getRepoHead, and its getRepoTreeId now refuses a path that is not a directory, as upstream does.

  • Repo vs. Vfs: suggestedEntries gives the same answer for both, with and without conditions, at the default branch's head, and empty with no package.json.

  • Metro, platforms and mainFields: checked on a React Native package whose own entry and exports differ by field, platform and condition:

    • Node's resolution;
    • Node with browser;
    • metro on ios and android;
    • metro on web;
    • mainFields;
    • an index per platform.

    Each case is checked three ways: over a repo, over a Vfs, and as buildGitHubBundle's default.

  • typescript:

    • main and exports (including an import-only .mts) named by compiled outputs, with only the TS sources present;
    • an extensionless main, completed (a .d.ts is never picked);
    • a compiled .js winning over its source;
    • the same through the field resolver under metro.
  • Option refusals: the build's own messages, e.g. --metro without --platforms, or --conditions combined with --metro.

  • Directory handling:

    • a directory downloaded on its own;
    • one with a symlink out of it, which falls back to the whole repo;
    • a directory that is itself a symlink.

    A main field or bin written ../x.js is not suggested; this is also checked through the Metro main fields.

  • Argument checks for suggestedEntries, before anything is fetched.

  • buildGitHubBundle:

    • the default commit;
    • default entries;
    • what is skipped;
    • the refusals;
    • option checks without entries;
    • soldeer requiring entries.
  • CLI: runs with neither --sha nor entries.

  • No disk reads: that test also builds from default entries, and from default entries under --metro.

node --run lint is clean and node --run test passes: 2144 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 c95d9d1 to ae35586 Compare October 2, 2026 02:00
@exo-nikita exo-nikita changed the title feat(github-bundle): default to the package.json's entry points and the default branch's head feat(vfs-bundle): suggestedEntries; github-bundle defaults to them and to the default branch's head Oct 2, 2026
@exo-nikita
exo-nikita force-pushed the claude/affectionate-ritchie-1j9umy branch 2 times, most recently from 78e7d43 to 3c3a274 Compare October 2, 2026 03:21
…d to the default branch's head

`@exodus/stasis/vfs-bundle` exports suggestedEntries: the JS entry points a package.json names,
as paths from its directory, resolved the way the JS build resolves them under the same
`conditions`, `mainFields`, `metro`, `platforms`, `jsx` and `typescript`, which are checked the way
the build checks them. In order:

- The package's own entry, resolved as `./` resolves there. That is `main` as Node takes it, or,
  with `mainFields` or `metro`, the build's field resolver (Metro's main fields, platform
  suffixes and browser maps) once per platform.
- Each `exports` subpath, except patterns and folder mappings, resolved through the package's
  name for require() and for import, with the conditions the build adds (the React Native ones
  under `metro`, plus browser on web).
- Each `bin`.

Under `typescript`, an entry point that resolution misses (one named by its compiled output) is
mapped to its TS source the way tsc maps it, as the build's fallback does. For `exports`, the
fallback's mapping is now typescriptExportsTarget in resolve-typescript.js.

Only files inside that directory count, and only when the package.json names them from within
it. A main field or `bin` pointing above the directory is left out, so a directory downloaded on
its own gives the same answer as the whole repo. With no package.json the result is empty.

suggestedEntries takes either a project held in a Vfs (`{ vfs, cwd }`) or a GitHub repo at a
commit (`{ github, sha, directory, client }`). For a repo, the directory is downloaded on its own
when it is a plain directory in git with no symlink out of it; otherwise the whole repo is.

buildGitHubBundle and `stasis github-bundle` now take `sha` and `entries` as optional:

- Without `sha`, they build the head of the default branch, as GitHub names it (getRepoHead).
  That commit becomes the bundle's `repo.commit`, and the CLI summary prints it.
- Without `entries`, a JS bundle (pnpm, yarn1) takes the entries suggestedEntries suggests for the
  tree already fetched, under the build's own options (`--typescript` included). If there are none, the build is refused
  before anything is installed. The options are still checked before anything is fetched,
  against a JS entry, since every suggested entry is one.
- A Solidity bundle (soldeer) still needs its entries.

The legacy-field build's per-platform resolver setup becomes fieldResolverFor in cmd/bundle.js,
which the build and the default entries both use; the build itself is unchanged.

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 3c3a274 to c30593f Compare October 2, 2026 05:45
@ChALkeR
ChALkeR merged commit 4169924 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