Skip to content

Implement bundle command for Solidity files#37

Merged
exo-nikita merged 8 commits into
masterfrom
claude/dreamy-hypatia-NZPSQ
May 25, 2026
Merged

Implement bundle command for Solidity files#37
exo-nikita merged 8 commits into
masterfrom
claude/dreamy-hypatia-NZPSQ

Conversation

@exo-nikita
Copy link
Copy Markdown
Collaborator

@exo-nikita exo-nikita commented May 24, 2026

Summary

  • Adds a stasis bundle CLI command that accepts a list of .sol entry files and an optional --mapping=foundry.toml|remappings.txt, then produces a stasis Bundle written to stdout (default) or -o path (brotli when the path ends in .br, JSON otherwise).
  • Adds src/loaders/solidity.js, adapted from DeepView's loaders/solidity.js. Trimmed to produce only { sources, resolutions }. The mapping file is parsed for remappings but not included in sources.
  • Adds src/cmd/bundle.js which builds a Bundle (workspace bucket ".") holding every loaded .sol file, tags each with format: 'solidity', and stores resolutions under the wildcard "*" condition key in imports.

Usage

stasis bundle [--mapping=path/to/remappings(.txt|.toml)] [-o path/to/out(.json|.br)] path/to/file.sol ...

Test plan

  • node --run test — 279 tests pass (40 new + 239 existing)
  • node --run lint — clean
  • CLI smoke: stasis bundle --mapping=remappings.txt src/A.sol writes a parseable Bundle JSON to stdout
  • CLI smoke: stasis bundle with no args prints usage and exits 1
  • Round-trips through Bundle.parseCode (both .json and .br outputs)
  • Mapping files (foundry.toml, remappings.txt) are NOT included in sources
  • Shared imports are loaded exactly once across multiple entries

https://claude.ai/code/session_01YFi2BPkgGhtb2vXKZKL2it

Adds a `stasis bundle` CLI command that takes a list of .sol entries
plus an optional --mapping=foundry.toml|remappings.txt and produces a
stasis Bundle (workspace bucket "."). The mapping file is parsed for
remappings but is not itself included in the bundle. The loader is
adapted from DeepView's solidity loader (with a link in the file
header), trimmed to produce only `{ sources, resolutions }`.
claude added 2 commits May 24, 2026 10:12
The bundle CLI now always brotli-compresses its output (matching the
on-disk format of stasis.code.br) — both for `-o path` and stdout.

The Solidity loader and bundle command modules are no longer in the
package's exports map; the only public surface is `stasis bundle`.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class Solidity bundling support to Stasis by introducing a stasis bundle CLI command plus a Solidity loader that discovers and resolves Solidity imports (including Foundry remappings) and emits a Stasis Bundle (JSON or brotli-compressed JSON).

Changes:

  • Added stasis bundle CLI flow that builds a synthetic workspace bundle from one or more .sol entry files, optionally using foundry.toml/remappings.txt remappings.
  • Added src/loaders/solidity.js to parse imports/remappings, walk the filesystem, and produce { sources, resolutions }.
  • Added comprehensive tests and fixtures covering remappings, missing imports, multiple entries, stdout/file output, and .br output.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
bin/stasis.js Adds bundle command parsing and updates CLI usage text.
package.json Exposes new ./loaders/solidity and ./cmd/bundle entrypoints and includes them in published files.
src/cmd/bundle.js Implements buildSolidityBundle and bundleCommand (JSON/brotli output).
src/loaders/solidity.js Introduces Solidity import parsing, remappings parsing, disk walking, and resolution mapping.
tests/bundle-cmd.test.js Adds unit + integration tests for bundle building and CLI behavior including .br output.
tests/solidity-loader.test.js Adds tests for Solidity loader behaviors (imports, remappings, missing imports, listings).
tests/fixtures/solidity-bundle/basic/src/A.sol Fixture: basic relative import entry.
tests/fixtures/solidity-bundle/basic/src/B.sol Fixture: basic imported library.
tests/fixtures/solidity-bundle/listing-empty/list.sol.txt Fixture: empty listing file.
tests/fixtures/solidity-bundle/listing-nonsol/list.sol.txt Fixture: listing containing a non-.sol line.
tests/fixtures/solidity-bundle/listing-nonsol/src/A.sol Fixture: minimal Solidity source for nonsol-listing test.
tests/fixtures/solidity-bundle/listing-toml/foundry.toml Fixture: TOML remappings for listing mode.
tests/fixtures/solidity-bundle/listing-toml/lib/oz/X.sol Fixture: remapped library source for listing TOML mode.
tests/fixtures/solidity-bundle/listing-toml/list.sol.txt Fixture: listing file with foundry.toml header.
tests/fixtures/solidity-bundle/listing-toml/src/A.sol Fixture: entry importing remapped @oz/....
tests/fixtures/solidity-bundle/listing-txt/lib/oz/X.sol Fixture: remapped library source for listing TXT mode.
tests/fixtures/solidity-bundle/listing-txt/list.sol.txt Fixture: listing file with remappings.txt header.
tests/fixtures/solidity-bundle/listing-txt/remappings.txt Fixture: remappings file for listing TXT mode.
tests/fixtures/solidity-bundle/listing-txt/src/A.sol Fixture: entry importing remapped @oz/....
tests/fixtures/solidity-bundle/missing/src/A.sol Fixture: missing import specifier case.
tests/fixtures/solidity-bundle/non-relative-entry/src/A.sol Fixture: entry used for non-relative import edge case.
tests/fixtures/solidity-bundle/non-relative-entry/src/B.sol Fixture: imports src/A.sol non-relatively.
tests/fixtures/solidity-bundle/shared/src/A.sol Fixture: entry importing shared file.
tests/fixtures/solidity-bundle/shared/src/B.sol Fixture: second entry importing shared file.
tests/fixtures/solidity-bundle/shared/src/Shared.sol Fixture: shared imported library.
tests/fixtures/solidity-bundle/with-foundry-toml/foundry.toml Fixture: Foundry TOML remappings.
tests/fixtures/solidity-bundle/with-foundry-toml/lib/openzeppelin-contracts/contracts/utils/Math.sol Fixture: remapped OpenZeppelin-like library source.
tests/fixtures/solidity-bundle/with-foundry-toml/src/A.sol Fixture: entry importing OpenZeppelin path.
tests/fixtures/solidity-bundle/with-remappings-txt/lib/openzeppelin-contracts/contracts/utils/Math.sol Fixture: remapped OpenZeppelin-like library source.
tests/fixtures/solidity-bundle/with-remappings-txt/remappings.txt Fixture: remappings.txt remapping definition.
tests/fixtures/solidity-bundle/with-remappings-txt/src/A.sol Fixture: entry importing OpenZeppelin path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cmd/bundle.js Outdated
Comment on lines +20 to +26
function normaliseEntries(entries, cwd) {
const baseDir = resolve(cwd)
return entries.map((e) => {
const abs = resolve(cwd, e)
const rel = relative(baseDir, abs).split(/[\\/]/u).join('/')
if (rel.startsWith('..')) throw new Error(`Entry escapes baseDir: ${e}`)
return rel.replace(/^\.\//u, '')
Comment thread src/cmd/bundle.js Outdated
Comment on lines +20 to +27
function normaliseEntries(entries, cwd) {
const baseDir = resolve(cwd)
return entries.map((e) => {
const abs = resolve(cwd, e)
const rel = relative(baseDir, abs).split(/[\\/]/u).join('/')
if (rel.startsWith('..')) throw new Error(`Entry escapes baseDir: ${e}`)
return rel.replace(/^\.\//u, '')
})
Comment thread src/loaders/solidity.js Outdated
Comment on lines +123 to +126
toLoad.map(async (relPath) => [relPath, await readFile(join(baseDir, relPath), 'utf8')])
)
const next = []
for (const [relPath, content] of reads) {
Comment thread src/loaders/solidity.js
Comment on lines +55 to +67
if (specifier.startsWith('.')) {
const fromDir = fromFile.includes('/') ? fromFile.slice(0, fromFile.lastIndexOf('/')) : ''
const parts = [...(fromDir ? fromDir.split('/') : []), ...specifier.split('/')]
const resolved = []
for (const part of parts) {
if (part === '.' || part === '') continue
if (part === '..') {
if (resolved.length > 0) resolved.pop()
} else {
resolved.push(part)
}
}
return resolved.join('/')
Comment thread src/loaders/solidity.js
claude added 5 commits May 24, 2026 10:23
Adds a stderr summary line after writing the bundle, showing the file
count, the outermost common parent directory of every bundled file
(expressed relative to cwd, which may yield ".." if a remapping pulls
files above cwd), and the output destination ("<stdout>" when no -o).
- normalize spelling (normaliseEntries → normalizeEntries) and reject
  paths whose path.relative() result is absolute, which catches the
  Windows-different-drive escape that doesn't show up as ".."
- resolveSolImport returns null when a relative import's .. segments
  would traverse above the project root instead of silently clamping
  to root (which would change the imported file)
- collectSolidityFilesFromDisk warns + skips ENOENT for a resolved
  path instead of aborting the entire walk; other read errors still
  propagate
- loadSolidity rejects absolute paths and ..-escaping paths in a
  .sol.txt listing so a sloppy or hostile listing can't read
  arbitrary files outside its own directory
buildSolidityTree now returns a `missing` list alongside sources and
resolutions. buildSolidityBundle aggregates that list (plus any
entries that didn't load from disk) and throws before serializing, so
the output file is never created when a bundle would be incomplete.

The loader-level warn-and-skip behavior is preserved for diagnostic
output; only the bundle command treats unresolved imports as fatal.
buildSolidityBundle now walks up from every loaded .sol file looking
for the nearest package.json with name+version, and stamps the file
into a Bundle bucket keyed by that package's directory. Files inside
node_modules land in node_modules/<pkg> buckets (or deeper, when a
nested package.json claims the file); workspace files land in their
nearest workspace package.json's dir, falling back to "." with
solidity-bundle@0.0.0 when no package.json is found.

A node_modules file with no resolvable package.json fails the bundle
loudly rather than silently leaking into the workspace bucket.

The imports map's condition key is now "solidity" instead of "*" so
solidity bundles don't collide with the wildcard used by JS bundles.

Also: relax tests/fixtures glob in .gitignore so deeper fixture
node_modules trees can be tracked.
The summary computed `files` from `bundle.modules.get('.').files`, which
only saw the workspace bucket and ignored every node_modules bucket
populated by the per-package bucketing. Use `bundle.sources` (which
flattens every bucket to project-relative paths) so the file count and
the outermost dir both reflect the whole bundle.
@exo-nikita exo-nikita merged commit 5c5820c into master May 25, 2026
2 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.

3 participants