Skip to content

Try bundling base realm - #5653

Draft
backspace wants to merge 9 commits into
mainfrom
base-bundling-prototype
Draft

Try bundling base realm#5653
backspace wants to merge 9 commits into
mainfrom
base-bundling-prototype

Conversation

@backspace

Copy link
Copy Markdown
Contributor

No description provided.

Base-realm modules are compiled into the host bundle (eager
import.meta.glob over packages/base) and registered as loader shims at
loader creation, so host imports of @cardstack/base/* resolve to
compiled-in modules instead of fetching realm-server-transpiled source
per loader generation.

Supporting changes the bundling surfaces:

- base modules imported several type-only names as value imports
  (babel's TS transform elides them; rolldown resolves them and fails
  on the missing runtime exports) — converted to `import type` /
  inline `type` in 15 base files
- color.gts imported siblings via @cardstack/base/* specifiers;
  converted to relative imports like the rest of the package
- date-fns is a real dependency of base source, declared it (it was
  previously satisfied only by the host's runtime shim)
- vite resolve.alias maps @cardstack/boxel-host/{tools,commands}/* to
  app/tools/* — the same 1:1 mapping shimHostTools provides at runtime
- https:// imports (currency.gts's esm.run dependency) are externalized
  so the browser fetches them at chunk load, as the loader-served
  module does

Known semantic shifts, to evaluate on this branch: base modules become
singletons shared across loader generations (loader resets no longer
re-evaluate base module state), and editing base source in a running
realm no longer affects the host runtime without a rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±  0      1 suites  ±0   1h 26m 14s ⏱️ - 5m 48s
3 588 tests  - 140  3 564 ✅  - 118  13 💤  - 1   2 ❌ + 1  9 🔥  - 22 
3 607 runs   - 140  3 574 ✅  -  96  13 💤  - 1  11 ❌  - 21  9 🔥  - 22 

Results for commit 54f2640. ± Comparison against earlier commit b654ec1.

For more details on these errors, see this check.

Realm Server Test Results

    1 files  ±  0      1 suites  ±0   10m 0s ⏱️ - 2m 10s
1 717 tests  - 315  1 715 ✅  - 315  0 💤 ±0  2 ❌ ±0 
1 791 runs   - 320  1 789 ✅  - 320  0 💤 ±0  2 ❌ ±0 

Results for commit 54f2640. ± Comparison against earlier commit b654ec1.

For more details on these errors, see this check.

backspace and others added 8 commits July 30, 2026 15:45
Base modules discover their loader via import.meta.loader, which only
exists when a Loader evaluates the module source. Modules compiled into
the host bundle are evaluated by the platform's module system, so every
myLoader() there returned undefined and loadCardDef threw
"Cannot read properties of undefined (reading 'getVirtualNetwork')" on
each base-realm instance deserialize. The prerender renders with the
host dist, so base indexing failed, realms never reached readiness, and
every stack-booting CI job timed out.

- Loader gains setForBundledModules/forBundledModules statics; the host
  loader-service publishes each loader that becomes its active one
  (initial, clones, resets)
- the seven myLoader() copies in base fall back to
  Loader.forBundledModules() when import.meta.loader is absent, and
  throw a descriptive error when neither is available
- workspace.gts derived its own module URL from import.meta.url, which
  a bundler reports as the compiled chunk's URL; it now states its
  canonical base-realm URL directly
- the bundled-base header no longer claims prerender/indexing are
  unaffected — they render with the host dist, so they see the bundled
  base too

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundled base modules were registered per loader by the loader service,
so loaders constructed anywhere else — the test-realm adapter's loader,
in-browser indexing loaders — fetched base modules from the realm server
and evaluated a second copy of card-api. Card instances built by one
copy fail field lookups performed by the other ("the card Object does
not have a field ..."), which is what most of the host test failures
reduced to.

Registration now happens once, on the virtual network (network service,
right after the @cardstack/base realm mapping it resolves against). The
loader's module-fetch path consults the network's shim registry after
its own per-loader shims, so every loader sharing the network serves
the same compiled-in modules. The lookup lives in the module-fetch path
rather than the network's fetch handlers deliberately: base-realm URLs
also serve card-instance documents at extensionless URLs, and only the
module path knows a request is for a module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
base declared ^0.7.0 while the host uses ^0.8.0, so pnpm installed both
versions and the bundled host build resolved base's imports to its own
0.7.1 copy. Two copies mean two context registries: providers rendered
from base components registered with the copy whose glimmer integration
the app never booted, so consumers found no provided value and fell
back to defaults. Concretely, DefaultFormatsConsumer read 'isolated'
instead of the provided child format, and an unset linksTo then
rendered DefaultCardDefTemplate with an undefined @model, crashing
every indexing render of a card with links ("Cannot read properties of
undefined (reading 'constructor')").

Under loader-served operation the mismatch was invisible — the runtime
shim always supplied the host's copy regardless of what base declared.
Bundling makes build-time resolution the binding one, so base's
declared range now matches what the shim provides at runtime. With the
ranges aligned pnpm dedupes to a single 0.8.0 install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
card-serialization listed Loader in its import-type block, so the
transpiled module had no runtime binding for it and myLoader's
Loader.forBundledModules() fallback threw "ReferenceError: Loader is
not defined" — every bundled deserialize through cardClassFromResource
failed, which broke indexing of any card with linked fixtures (the
operator-mode links/basics/ui failures). base has no type-check step,
so nothing caught the type-only import being used as a value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base card components fire a _types fetch against the mock test realm
as soon as they render; with base compiled into the host bundle that
render can precede the test-realm service worker's interception, so
the fetch escapes to the real network and fails the shard as
"Global error: Uncaught TypeError: Failed to fetch". Match that form
in the shard retry pattern — the race is rare per shard, so the second
pass lands. The durable fix is queuing test-realm fetches in the
harness until realm registration completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orms

Two fixes for the remaining bundled-base test failures:

The test fetch wrapper now answers requests to registered test realms
via realm.maybeHandle instead of dispatching them to the network. The
service-worker relay that otherwise serves these URLs only intercepts
once the worker controls the page and its per-module activation is
acked; a card component that renders before that window closes (which
bundled base modules do — no module-fetch latency) fired fetches that
escaped to the real network and failed shards as "Global error:
Uncaught TypeError: Failed to fetch". The registry is populated at
realm construction, so the in-page path has no such window.

Bundled base modules now register under the canonical
https://cardstack.com/base/ form as well as the RRI form. resolveImport
passes URL-form identifiers through unchanged (URL-to-URL mapping
happens at the network's fetch boundary, which shim lookup precedes),
so canonical-form imports missed the shim and fell through to a network
fetch that evaluated a second copy of the module, splitting def
identity — the serializeFileDef adoptsFrom mismatch and skill-state
failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registering each bundled module under the canonical
https://cardstack.com/base/ form as well made a def's identified module
URL depend on import order: loaders capture export identities under
whichever identifier form they fetched first, so flows that compare or
serialize refs (skill and command attachment, file choosers) saw
mismatched identities. The ai-assistant-panel, mini-file-chooser, and
card-basics failures bisect to exactly that dual registration.

Canonical-form imports therefore fall through to a network fetch that
evaluates a second module copy — a known, narrower gap (a handful of
commands/* imports) documented at the registration site. The durable
fix is normalizing identifiers through the network's URL mappings in
the loader's module-fetch path so both forms converge on one module
state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three fixes for the def-identity failures that remained with base
compiled into the host bundle:

- The loader folds virtual-alias URL identifiers (e.g.
  https://cardstack.com/base/…) onto their mapped real URL when
  resolving imports, so both spellings of a module converge on one
  module-state entry, one shim lookup, and one captured export identity.
  resolveImport alone passes full URLs through, so a virtual-alias
  import previously keyed separate module state — and, since virtual
  forms miss the shim registry, fetched and evaluated a second copy.
  Non-URL identifiers pass through untouched (mapURL constructs a URL).

- Loaders replay the virtual network's sync-shim inventory through
  identity capture, in registration order, before any individual shim's
  own capture. Loader-evaluated modules got declaring-module-first
  identity from dependency-first evaluation; shims carry no dependency
  chain, so a loader whose first shim load was a re-exporter (file-api
  re-exports FileDef from card-api) captured identities under the
  re-exporter. Registration order stands in for dependency order:
  externals (runtime-common, boxel-ui) register before base, and within
  base card-api and cards-grid — the declaring modules for the def
  classes serialization identifies — register ahead of the rest.

- code-ref.gts validated code refs with a bare dynamic import(module).
  Inside loader-evaluated modules the AMD transpile rewrites that to a
  loader-mediated import; compiled-in modules must route through the
  loader explicitly, since the specifier is a runtime realm URL only a
  Loader can resolve.

Verified locally against the full service stack: serializeFileDef 7/7,
update-room-skills 9/9, card-basics 101/101, host command schema
generation 92/92, ai-assistant-panel skills clean, markdown skill
search 3/3; Store, file-attachment, and serialization stay green.
Remaining known residue is the dependency/reference-shape class in
realm-indexing assertions (shimmed modules carry no consumed-module
chains).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant