Skip to content

vfs-bundle: add buildGitHubBundle (a GitHub repo at a commit, via @preventive/upstream) - #196

Merged
ChALkeR merged 4 commits into
mainfrom
claude/sharp-edison-cmebue
Oct 1, 2026
Merged

ChALkeR merged 4 commits into
mainfrom
claude/sharp-edison-cmebue

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Adds buildGitHubBundle to @exodus/stasis/vfs-bundle. It builds a JS bundle of a GitHub repo at a commit, optionally rooted at a directory, from the repo's lockfile alone. Nothing is read from disk.

const { bundle, lockfile, stats } = await buildGitHubBundle({
  github: 'owner/name', sha: '<full sha>', directory: 'packages/app', // directory optional
  packageManager: 'pnpm', entries: ['src/index.js'],                  // ...buildVfsBundle options
  client,                                                             // optional @preventive/upstream/github.js client; anonymous by default
})

How it works

  1. Validation first: github, sha and directory are checked as the bundle's repo block checks them (GitHub owner/name, full lowercase sha, URL-safe directory), plus packageManager, before anything is fetched.
  2. Download, through @preventive/upstream 1.0.0-alpha.3. Every download is checked against git's tree id.
    • With a directory: listRepoDir checks whether it holds the package manager's lockfile (pnpm-lock.yaml / yarn.lock). If it does, only that subtree is downloaded (getRepoTreeId + getRepoTreeTarball) and built from its root.
    • Otherwise: the whole repo is downloaded (getRepoTarball) and built from /<directory>, so a lockfile at the repo root covers a workspace package.
  3. Unpack: the tarball is gunzipped and unpacked with @preventive/archive, which is new here. GitHub's top directory is dropped, and only files, directories and links are accepted. A fifo or device is refused, with its path in the error.
  4. Build: the result goes into a Vfs and is handed to buildVfsBundle, with repo: { github, directory | root, commit } stamped from the request.

Dependencies

  • @preventive/archive 1.0.0-beta.3 (latest) is added to @exodus/stasis and, for the tests, to the workspace root devDependencies.

Tests

  • New tests/vfs-bundle-github.test.js runs against a fake in-memory client, so nothing is fetched. It covers:
    • a repo-root build
    • a directory with its own lockfile (subtree only)
    • a directory under a root lockfile (whole repo)
    • argument checks before any call
    • a refused fifo entry
  • buildGitHubBundle is added to tests/public-exports.test.js.
  • Full suite on Node 24.14.0: 2112 passed, 0 failed. oxlint is clean.
  • I couldn't run a live check against GitHub: the CI container's shared IP is over GitHub's anonymous API rate limit (403).

🤖 Generated with Claude Code

https://claude.ai/code/session_01C4wWtS6P5ji71GhZ56NZeh


Generated by Claude Code

claude added 4 commits October 1, 2026 20:24
… @preventive/upstream)

buildGitHubBundle({ github, sha, directory?, client?, packageManager, ...buildVfsBundle options })
downloads the commit's tree through a @preventive/upstream/github.js client
(anonymous by default), every download checked against git's tree id:
- with a `directory` holding the package manager's lockfile
  (pnpm-lock.yaml / yarn.lock, via listRepoDir), only that subtree
  (getRepoTreeId + getRepoTreeTarball), built from its root;
- otherwise the whole repo (getRepoTarball), built from /<directory>,
  for a lockfile above it.
The tarball is gunzipped and unpacked with @preventive/archive (its top
directory dropped; only files, dirs and links accepted) into a Vfs and
handed to buildVfsBundle, with `repo: { github, directory | root, commit }`
stamped from the request (validated before anything is fetched).

Adds @preventive/archive 1.0.0-beta.3 (latest) as a dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4wWtS6P5ji71GhZ56NZeh
- Move it to vfs-bundle/github.js; vfs-bundle.js stays a facade
  (re-export; header mentions GitHub).
- Lockfile names come from tree.js (lockfileOf) instead of a second map.
- Tar entries go to vfsFromEntries as unpack returns them (top dir
  dropped); only file/directory/symlink, as GitHub's verified trees hold.
- Unpacked size capped at upstream's own 1 GiB.
- Check github/sha directly; const flow; shorter comments.
- Simpler fake client in the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4wWtS6P5ji71GhZ56NZeh
- A symlink pointing outside the downloaded tree is refused in
  whole-repo mode instead of silently resolving to a file inside it.
- The subtree is used only when it stands alone; otherwise the whole
  repo is downloaded: a symlink out of it (or one unpack refuses), a
  lockfile link:/file:/directory: path above it, or a [jt]sconfig path
  above it.
- A `directory` that is no plain tree in git (a symlink, or under one)
  falls back to the whole repo, which resolves it.
- `repo` now names where the lockfile is (the real path), which the
  bundle's paths are relative to -- not the requested directory, which
  was wrong under a root lockfile or through a symlink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4wWtS6P5ji71GhZ56NZeh
Validate github/sha/directory with isValidRepoField directly instead of a
throwaway Bundle (clearer errors too); run the fallback test cases with
Promise.all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C4wWtS6P5ji71GhZ56NZeh
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