Skip to content

Try bundling base realm - #5653

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

Try bundling base realm#5653
backspace wants to merge 19 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      1 suites   57m 13s ⏱️
4 327 tests 4 303 ✅ 14 💤  0 ❌ 10 🔥
4 346 runs  4 312 ✅ 14 💤 10 ❌ 10 🔥

Results for commit 7f03806.

For more details on these errors, see this check.

Realm Server Test Results

    1 files      1 suites   11m 46s ⏱️
2 192 tests 2 189 ✅ 0 💤 3 ❌
2 275 runs  2 272 ✅ 0 💤 3 ❌

Results for commit 7f03806.

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>
backspace and others added 10 commits August 7, 2026 18:56
The Code-patches timeouts on this branch report two `sqlite running` and
two `fetcher-body:text` waiters still open at the 60s mark, with nothing
to say which statement or which request stalled — and the harness'
in-flight-fetch list reads `<none>` because an in-page test realm answers
a request without it ever reaching the global fetch, so the waiter label
is the only record left.

Label them: the fetcher waiter with the request's method and URL, its
body waiters with the response URL, and the sqlite waiter with the worker
command plus the statement. Also log the full stack behind a global error
or unhandled rejection, so the recurring `Failed to fetch` points at a
call site instead of a line in a bundled chunk.

Records, too, why the three `Integration | realm indexing` failures are
structural: a shim carries no dependency chain, so an indexed instance's
deps lose their closure through base.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bundled build resolves base's imports statically, so a value-import
of a type-only export is a hard error: `[MISSING_EXPORT]
"BaseDefComponent" is not exported by "../base/card-api.gts"`, which
failed this branch's test-asset build. The fetched path never noticed
because the realm-server transpile strips types per-module without
checking them against the source module's exports.

Every other base file that names BaseDefComponent already marks it
`type` — this file arrived after the last merge from main, so it missed
the pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new global-error stack named the recurring `TypeError: Failed to
fetch` on this branch: the grid's `loadFilterList` task, fetching
`${realm}_types` with a native fetch. The only failing request in the
shard is `GET https://cardstack.com/base/_types` — a virtual alias that
just names a host the browser can't reach. The virtual network maps that
alias to the URL the base realm is really served from, but a native fetch
never consults it, and the task has no catch, so the rejection surfaced
as an uncaught global error and failed the shard.

Route both `_types` fetches (cards-grid and workspace) through the
loader's fetch, which resolves the alias and carries realm auth rather
than depending on the auth service worker to inject it — a base module
compiled into the host bundle falls back to the loader the host publishes
for bundled modules, since the platform evaluated it and left no
`import.meta.loader`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`@ts-ignore` suppresses the next line only, and prettier wrapped the
annotated declaration so `import.meta` landed on the line after it —
tripping TS1470 ("not allowed in files which will build into CommonJS
output", which base files hit because realm-server type-checks them).
Dropping the type annotation fits the initializer on the ignored line,
matching how the other base modules that reach for a loader write it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`FileAudio falls back to the canonical source when the blob fetch fails`
asserts right after `await render(...)`, and the modifier decides the
element's src in an async IIFE that no test waiter covers — so whether the
fallback has run by assertion time is a race that this branch happens to
lose (the harness logs the intended `GET .../take.mp3` rejection, then the
element is found with no src at all).

Wrap the IIFE in `waitForPromise` so the swap is visible to the harness.
Outside tests the shim passes the promise through untouched, so this is
inert in the app. The race is latent on main too — the test passes there
by luck of microtask ordering, not by synchronization.

Co-Authored-By: Claude Opus 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