Skip to content

feat(stasis-plugins): add Rollup plugin (StasisRollup) - #167

Merged
ChALkeR merged 2 commits into
mainfrom
claude/rollup-plugin-sojzqv
Aug 21, 2026
Merged

feat(stasis-plugins): add Rollup plugin (StasisRollup)#167
ChALkeR merged 2 commits into
mainfrom
claude/rollup-plugin-sojzqv

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Adds a Rollup bundler plugin alongside the existing esbuild, webpack, and Metro ones, exported as @exodus/stasis-plugins/rollup and re-exported as @exodus/stasis/rollup.

Design

StasisRollup mirrors StasisEsbuild's shape: same options handling via resolvePluginState (preload reuse, sidecars, rules 0/1/7), same #seen-keyed capture dedupe, same watch/rebuild refusal, same fail-closed load-mode gates. Hooks are per-instance functions rather than prototype methods because rollup binds this to its PluginContext (whose .resolve the capture path needs); an inert plugin assigns no hooks at all, so rollup sees a name-only plugin.

  • CaptureresolveId wraps the remaining resolver chain (ctx.resolve with skipSelf) to observe the final resolution, records the as-written edge via addImport, and passes the resolution through unchanged. The load hook attests raw bytes via addFile and returns them so the build consumes exactly what was recorded; resources are attested and then deferred to the user's asset plugin. Standalone/sidecar States write on a clean buildEnd.
  • bundle=loadresolveId answers from the bundle's import map (the file needn't be on disk), deferring built-ins and unattested externals back to rollup; the load hook serves any in-scope path through getFile's hash-verified, fail-closed gate — never a silent disk fallback. A clean dir holding only the bundle plus a minimal package.json rebuilds byte-identically.
  • Import attributeswith { type: 'json' } edges round-trip through addImport/getImport keyed the same way as the esbuild plugin's.
  • Resource limitation — rollup asset plugins read their files from disk inside their own load hook, and load hooks don't chain, so there is no seam to hand them attested bytes (unlike esbuild's loader replay or webpack's inputFileSystem wrap). bundle=load therefore refuses resource imports loudly instead of letting them read unattested (or missing) disk bytes; capture/frozen modes fully support resources.
  • Ordering — like the other plugins' resolver hooks, StasisRollup should be first in plugins so it sees edges before sibling resolvers; file bytes still fail closed at the load hook regardless.

Tests

tests/rollup.test.js drives real rollup builds (new rollup workspace devDependency) through the same spawned-helper harness as the esbuild suite: all lock/bundle modes, tamper and fail-closed cases (dropped bytes, dropped edges), a clean-dir byte-identical round-trip, built-in/external deferral, import-attribute round-trip, node_modules scope including a load with node_modules deleted, plugin↔preload coordination rules, options validation, resources allowlisting, and the load-mode resource refusal. The helper ships minimal stand-ins for @rollup/plugin-node-resolve/@rollup/plugin-json/asset plugins (rollup core resolves only relative/absolute paths and parses only JS), which also exercises the sibling-plugin chain. The rebuild-refusal and public-exports suites cover the new plugin too.

node --run test: 1823 pass, 0 fail (+42 over baseline, ~+2s wall). node --run lint: clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LHeFB5fiezsMo1CKGPMafQ


Generated by Claude Code

claude added 2 commits August 20, 2026 16:57
Add a Rollup bundler plugin alongside the esbuild, webpack, and Metro ones,
exported as @exodus/stasis-plugins/rollup and re-exported as
@exodus/stasis/rollup.

Capture mode wraps the remaining resolveId chain (ctx.resolve with skipSelf)
to record the observed as-written edges, and serves attested raw bytes from
its load hook so the build consumes exactly what was recorded; resources are
attested and then deferred to the user's asset plugin. bundle=load resolves
every specifier from the bundle's import map and serves bytes through
getFile's fail-closed gate, deferring built-ins and unattested externals back
to rollup. Import attributes (with { type: 'json' }) round-trip through
addImport/getImport like the esbuild plugin's.

Because rollup asset plugins read their files from disk inside their own load
hook (no seam to hand them attested bytes, unlike esbuild's loader replay or
webpack's inputFileSystem), bundle=load refuses resource imports loudly
instead of letting them read unattested disk bytes.

Watch/rebuild capture is refused from the second buildStart (same
path-keyed-dedupe staleness hazard as the sibling plugins); load mode allows
rebuilds. Standalone/sidecar States are written on a clean buildEnd.

tests/rollup.test.js drives real rollup builds through the same spawned-helper
harness as the esbuild suite (lock/bundle modes, tamper + fail-closed cases, a
clean-dir byte-identical round-trip, nm scope with node_modules deleted,
plugin<->preload coordination rules, resources); the rebuild-refusal and
public-exports suites cover the new plugin too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHeFB5fiezsMo1CKGPMafQ
Correctness fixes to the StasisRollup plugin, each with a regression
test in the new tests/rollup-plugin.test.js:

- Refuse '?'/'#'-suffixed variants of on-disk module ids at capture
  instead of silently skipping them: the underlying bytes feed the
  build, so a silent skip left a build input invisible to frozen
  verification (the esbuild plugin refuses its suffix analogue).
- Strip a warm `cache:` via the options hook (with a warning): cached
  modules bypass resolveId/load, so a warm-cache capture recorded no
  import edges and a warm-cache load served unverified code.
- Attest entries from the completed graph in buildEnd instead of
  trusting resolveId's opts.isEntry, which rollup defaults to
  !importer: a sibling's bare this.resolve() probe widened the
  attested set of runnable roots at capture and aborted clean-bundle
  loads. A resource entry now fails loudly ("a resource can't be an
  entry"), matching StasisEsbuild, instead of silently writing an
  entries list that omits the build's real entry.
- Anchor load-mode entry resolution against the importer when one is
  given (emitFile'd chunks), falling back to cwd; assert entries in
  the load hook from the graph's authoritative isEntry.
- Guard the load-mode resolver against an undefined importer
  (sibling this.resolve(spec, undefined, { isEntry: false })) --
  previously a TypeError failed the whole build.
- Gate the load-mode resource refusal on serving scope, so an
  attested workspace resource under node_modules scope round-trips
  (it is served from disk, like every workspace file in that scope).
- Move the import-attribute assert to the edge record/lookup sites,
  so an attributed EXTERNAL passes through as it does under esbuild.
- Refuse out-of-root resolutions with a contextual error naming the
  specifier/importer instead of State's bare path-less assertion,
  and refuse a second constructor argument instead of silently
  ignoring an esbuild-style { transform } bag.
- Document the load-mode moduleSideEffects KNOWN GAP (sibling
  resolution metadata is not persisted in the bundle).

Also: dedupe the classify-throw block, destructure rollup 4's
guaranteed resolveId options, reuse getImport's format instead of a
second getFormat pass, drop the redundant existsSync stat before
every capture read, restore posixPathEscapes' allocation-free '..'
prefilter, and rebuild the artifact-graph builtin pin on oxc's AST
records (static + re-export + dynamic) so quoting or indentation
can't hide an edge from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHeFB5fiezsMo1CKGPMafQ
@ChALkeR
ChALkeR merged commit 09d8b3c into main Aug 21, 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.

3 participants