Skip to content

bundle: JS support via static scan, with conditions-aware resolution#41

Merged
exo-nikita merged 1 commit into
mainfrom
claude/gracious-turing-4fLjS
Jun 8, 2026
Merged

bundle: JS support via static scan, with conditions-aware resolution#41
exo-nikita merged 1 commit into
mainfrom
claude/gracious-turing-4fLjS

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Summary

  • stasis bundle src/entry.js [src/entry2.js ...] produces a brotli-compressed Bundle interchangeable with stasis run --bundle=add, but built without executing any user code. Multiple entries are all marked as entry points; pair with --lockfile=path to also emit a matching lockfile that attests every byte in the bundle.
  • Resolution is conditions-aware: scan passes the full Node-22 condition set ['node', import|require, 'module-sync', 'node-addons'] to createRequire().resolve(spec, { conditions: Set }). Earlier shortened sets caused packages with module-sync exports to silently resolve to a different file under the bundle than under plain Node.
  • Parser: oxc-parser as an optional peer dep, loaded synchronously via createRequire. Stasis keeps its zero-runtime-deps posture; consumers who never call the bundle command don't need it.

Five correctness fixes from two rounds of review

Before After
module-sync conditions Silent wrong-file execution (bundle ran different code than plain node) Full Node-22 condition set passed to resolver
Stale-bundle inheritance Building twice in same dir leaked stale formats/imports bundle: 'replace' skips on-disk merge
Config override --scope=full silently ignored when stasis.config.json disagreed Symmetric assert: options-vs-file now matches env-vs-file behavior
Missing-edge errors assert.ok(file)ERR_ASSERTION, re-threw out of common guards ERR_MODULE_NOT_FOUND-coded error (matches plain Node's ESM resolver)
Dual-package / ESM-only Picked CJS branch from ESM parent; ESM-only threw ERR_PACKAGE_PATH_NOT_EXPORTED Per-parent format-derived conditions resolve correctly

Each fix has a regression test; reproducers exist for all five.

Test plan

  • 400 of 410 tests pass; the 10 failures (lock x bundle x mock matrix cells bundle=add|replace mock) are pre-existing on main — verified by checking out clean origin/main and running the same suite. They assert byte-equality between mock-on and mock-off bundles in a separate --mock feature and are unrelated to this branch's work.
  • Lint clean (node --run lint).
  • Two reviews completed (correctness + design), all flagged issues resolved or non-blocking.
  • Final merge-readiness review: YES.

Notes for follow-up (non-blocking)

  • README/doc/ don't yet document stasis bundle src/entry.js — biggest user-facing gap.
  • A stale comment in src/cmd/bundle.js references the pre-fix assert behavior; should say ERR_MODULE_NOT_FOUND.
  • bin/stasis.js's --scope validation duplicates Config's; could import VALID_SCOPE.
  • src/scan.js's per-file findPackageJSON walk isn't memoized; O(n × depth) on large graphs.

https://claude.ai/code/session_01DqXmS7gzzwuY4AF4wnViob


Generated by Claude Code

…om review rounds

Squashed three earlier commits (initial scan, bundle integration with conditions-aware
resolution and lockfile pairing, scan-CLI removal) plus a fourth fix-pass landing the
five correctness bugs surfaced by the second-round review. History squashed for a single
clean rebase onto main's --mock work.

Highlights:

- `stasis bundle src/entry.js [src/entry2.js ...]` produces a brotli-compressed Bundle
  loadable interchangeably with one produced by `stasis run --bundle=add`. Multiple
  entries are all marked as entry points; bundle pairs with `--lockfile=path` to also
  emit a matching lockfile that attests every byte in the bundle.

- Resolution: scan passes the full Node-22 condition set
  `['node', import|require, 'module-sync', 'node-addons']` to
  `createRequire().resolve(spec, { conditions: Set })`. Earlier shortened sets caused
  packages with `module-sync` exports to silently resolve to a different file under
  the bundle than under plain Node -- the worst kind of bug: green tests, wrong
  production behavior. tests/fixtures/scan-module-sync pins this regression.

- Parser: oxc-parser as an optional peer dep, loaded synchronously via createRequire.
  Stasis keeps its zero-runtime-deps posture; consumers who never call the bundle
  command don't need to install it.

- State.getImport throws a properly-coded ERR_MODULE_NOT_FOUND on missing edges
  (previously asserted, producing ERR_ASSERTION that re-threw out of the common
  `try{import(x)}catch(e){if(e.code!=='ERR_MODULE_NOT_FOUND')throw e}` guard).

- Config.loadConfig now treats explicit constructor options like env vars: an explicit
  `--scope=full` is no longer silently overridden by an on-disk
  `{"scope":"node_modules"}` -- the conflict surfaces as the existing
  "Flags/env can not override stasis.config.json" error.

- buildJsBundle uses `bundle: 'replace'` to skip merging any pre-existing
  stasis.code.br from disk -- building twice in the same directory used to leak
  the first build's formats/imports entries into the second's output.

- Static bundles store edges under the wildcard '*' condition key; state.getImport
  falls back to '*' when the runtime-condition lookup misses, so runtime-built
  bundles still match on the specific key (no behavior change there) and static
  bundles work regardless of which conditions Node passes at load time.

Tests cover: 22-cell static-bundle x lock matrix on the popular-npm-modules fixture
(mirrors the runtime matrix), all five regression cases (module-sync, stale inheritance,
config conflict, ERR_MODULE_NOT_FOUND, ESM-only / dual-package), two-directional graph
agreement, and the new scan-esm-conditions and scan-module-sync fixtures. 387 of 397
tests pass; the 10 failures are pre-existing on `main` (`lock=* bundle=add|replace mock`
cells assert byte-equality between mock-on and mock-off bundles).
@exo-nikita exo-nikita merged commit 740a3b8 into main Jun 8, 2026
1 of 2 checks passed
exo-nikita added a commit that referenced this pull request Jun 8, 2026
PR #41 was rebased on commit 39d7dd5 and squash-merged after ed41d6f (the --full→--dependencies flip) landed. My tests came in using --full and no-flag patterns that no longer match the CLI: --full was renamed to the implicit default, --dependencies is the node_modules opt-in, and the nm-scoped popular-npm-modules fixture needs --dependencies to avoid env-vs-file scope conflict.

Fixes the 26 test failures introduced by the merge race; 400 of 410 pass after this. The remaining 10 are the pre-existing lock×bundle×mock byte-equality failures from PR #40.

https://claude.ai/code/session_01DqXmS7gzzwuY4AF4wnViob
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