Skip to content

Switch host build to vite#4019

Draft
ef4 wants to merge 66 commits intomainfrom
vite
Draft

Switch host build to vite#4019
ef4 wants to merge 66 commits intomainfrom
vite

Conversation

@ef4
Copy link
Copy Markdown
Contributor

@ef4 ef4 commented Feb 18, 2026

No description provided.

ef4 added 21 commits December 10, 2025 18:19
These imports in app.ts were skipping a level in the package dependency graph. ember-power-select and ember-power-calendar are not dependencies of host, they're dependencies of boxel-ui.

Trying to import from your dep's deps is unreliable and makes the vite depscan complain.

I pushed them down into the boxel-ui components that actually pull the corresponding ember-power-* components into the project.
Under vite, we don't use an AMD loader so we won't conflict with the one in monaco.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 18, 2026

Host Test Results

2 439 tests  +2   2 424 ✅ +2   2h 35m 13s ⏱️ - 21m 50s
    1 suites ±0      15 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit 6a5b694. ± Comparison against base commit 0c7ede1.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 19, 2026

Preview deployments

@ef4
Copy link
Copy Markdown
Contributor Author

ef4 commented Apr 16, 2026

The leading edge of this work that I'm investigating is why postcss is now broken in our builds.

I have created a minimal reproduction that's ready to be debugged further. The presence of the embroider resolver inside rolldown breaks the behavior of how postcss sees its stubbed-for-the-browser dependencies. If you take that one line out of the vite config, postcss works.

@ef4
Copy link
Copy Markdown
Contributor Author

ef4 commented Apr 16, 2026

lukemelia and others added 26 commits April 20, 2026 15:30
The embroider resolver that @embroider/vite pushes into
optimizeDeps.rolldownOptions.plugins doesn't do extension resolution
for relative requires, so postcss's `require('./terminal-highlight')`
fails during dep optimization. Handle that one case in a pre-embroider
rolldown plugin instead of patching postcss.

Upstream: embroider-build/embroider#2703

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rolldown's chunk splitting can extract modules with module-scope
logger() calls into separate shared chunks that evaluate before the
host app's entry body runs setup-globals. The previous throw assumed
source-order import evaluation, which the new bundler violates.

Loosen logger() to no-op when _logDefinitions isn't installed yet,
and add reapplyLogLevels() so setup-globals can retroactively set
levels on loggers created during the early chunk phase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	packages/host/package.json
#	packages/runtime-common/virtual-network.ts
#	pnpm-lock.yaml
Rolldown evaluates CommonJS `require()` calls through a runtime wrapper
that throws if `require` isn't present, so any Node built-in that's
reachable from a static import chain crashes the browser at module load.
Several runtime-common entry points were pulling in `source-map-js`,
`util`, and `fs` transitively:

- realm.ts: defer `transpile` (→ glimmer-scoped-css/ast-transform →
  postcss → source-map-js) behind a dynamic import; only the realm
  server ever calls transpileJS.
- scoped-css.ts: inline `isScopedCSSRequest` and
  `decodeScopedCSSRequest` so nothing in runtime-common imports from
  glimmer-scoped-css.
- index-query-engine.ts, realm-index-query-engine.ts: use the new local
  scoped-css helpers.
- worker.ts: change the `stream` import to `import type` and lazy-load
  `Readable` inside the Node-only nodeStream branch. stream-browserify's
  module body calls `require('util')`, which fails in the browser.

Host-side consumers that previously imported `isScopedCSSRequest`
directly from glimmer-scoped-css now use the runtime-common re-export.

vite.config.mjs: alias `fs` to a tiny stub (lib/empty-fs.js) and add a
matching rolldown resolver plugin to optimizeDeps. recast's main.js
eagerly `require`s fs for a CLI helper we never invoke, and its
`"browser": { "fs": false }` field isn't honored by rolldown. The
optimizeDeps resolver is needed because resolve.alias doesn't apply
during dep pre-bundling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Node's ESM resolver (called from dynamic import() at runtime) does not
do TypeScript extension resolution, so `await import('./transpile')`
throws ERR_MODULE_NOT_FOUND on the realm server. Static imports were
fine because ts-node transforms them at compile time, but the dynamic
import I added in 6694960 bypasses that path.

Use `./transpile.ts` explicitly — matches the existing convention in
realm-server/tests/module-syntax-test.ts and is compatible with
allowImportingTsExtensions in tsconfig. Vite's dev server and Rolldown
both handle .ts extensions in dynamic imports.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the dynamic-import hack in realm.ts with the package.json
"imports" pattern already used for #fetch and #lint-task. The browser
entry is a stub that throws; the node entry is the real transpile
module. This keeps glimmer-scoped-css/ast-transform + postcss out of
the browser graph without crossing the ts-node CJS/ESM boundary that
broke dynamic import() at runtime on the realm server.

Supersedes the approaches in 6694960 and 08952f1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Only ci-host.yaml's 20-shard host suite should run on PR #4019 while
the vite migration is in progress. Revert before merging to main.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Same rationale as ci.yaml — only ci-host.yaml should run on PR #4019
during the vite migration. Revert before merging to main.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Host tests execute .gts transpilation in-browser via TestRealmAdapter,
so transpile.ts (and its postcss/glimmer-scoped-css transitive deps)
must bundle cleanly in the browser — earlier stub just pushed the
error elsewhere.

- Revert realm.ts back to a plain `./transpile` import; drop the
  #transpile conditional and transpile-browser.ts stub.
- Resolve bare `source-map-js` to postcss's own install (pnpm keeps
  it nested, so the host root can't find it).
- Stub Node's `url` module (alongside `fs`) with empty-fs.js; postcss
  guards pathToFileURL/fileURLToPath with truthy checks, so an empty
  module is safe.

Supersedes 6694960, 08952f1, c4ea349 (approach, not each
individual diff).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The vite branch's 9cd7b02 deleted packages/host/tests/serve.json and
switched serve:dist to `vite preview --port 4200`. A later main→vite
merge brought in Buck's Traefik wrapper (86301ac), which reintroduced
a `serve --config ../tests/serve.json` invocation inside
scripts/serve-dist.js. CI's start:host-dist has been failing with ENOENT
on the deleted serve.json ever since.

Rewrite the Traefik-aware wrapper to spawn `vite preview` instead, and
move the CORS + Cache-Control: no-store headers that serve.json provided
into vite.config.mjs under `preview`.
Boxel's card runtime relies on `Class.name` for user-visible text —
validation errors ("references unknown path X on Person"),
displayName fallbacks, query-field-schema diagnostics. esbuild's
minifier was mangling those to `e`/`i`/`r`, which produced garbled
errors in production and broke several integration/unit tests that
assert on the class name inside the error string:

  Unit | query field schema: referencing a missing field...
  Unit | query field schema: referencing nested fields...
  Integration | realm: 500 error for POST polymorphic incompatible type
  Integration | realm: 500 error for PATCH polymorphic incompatible type

Setting esbuild.keepNames globally applies to both the dev transform
and the build-time minifier.
Babel's internal path.resolve uses process.cwd() to convert a relative
filename into an absolute moduleName on compiled templates. In the
ember-cli/embroider browser build process.cwd() returned "/", so
`path.resolve('.', 'dir/person.gts')` yielded "/dir/person.gts". Under
vite the browser process shim differs and the leading slash was lost,
which broke the two realm-test assertions that check the moduleName
field in the compiled template.

Prepend a leading slash before calling transpileJS so the moduleName is
deterministic across node and browser. Update the realm-server card
source endpoints test to match the new convention ("/person.gts")
instead of the workspace-absolute filesystem path it happened to
receive before.
esbuild.keepNames only preserves identifiers that esbuild touches (dep
optimizer, JSX transform, etc.). Vite 8's production minifier is
oxc-minifier, invoked by Rolldown, which mangles classes declared
inside function bodies — breaking runtime Class.name introspection for
test fixtures like `class TestField1 extends FieldDef {}`.

Set build.rolldownOptions.output.keepNames so oxc-minifier emits the
__name helper that restores .name at runtime, matching the behavior
ember-cli/webpack provided before.
…tled

Every other skill-menu click in commands-test.gts waits for
[data-room-settled] after opening the assistant. This one didn't, so
the skill menu pill wasn't rendered when the click fired. The ember-cli
build happened to settle in time; vite's timing surfaces the race.
…lds)

@embroider/core@4's exports map only exposes `./virtual`, not
`./types/virtual` — the old subpath was valid under v3. Updating
tsconfig types to the supported spelling so ember-tsc can resolve it.

Also remove two duplicate fields in app/config/environment.ts that a
recent main merge introduced: `publishedRealmDomainOverrides` and
`defaultSystemCardId`. The optional form of defaultSystemCardId is
kept because consumers (room.gts, matrix-service) already guard it as
possibly undefined.

These were hidden behind the TS2688 early-exit from the missing
type def; resolving the subpath re-surfaces them.
…nsform

esbuild:keepNames routes .ts files through esbuild before rollup's babel
plugin, and esbuild converts class fields (x = foo()) into constructor
__publicField(this, "x", foo()) calls. That breaks ember-concurrency's
async-arrow-task-transform, which only matches ClassProperty nodes -- the
CallExpression ends up parented by AssignmentExpression after esbuild.

Name preservation is still handled by rolldownOptions.output.keepNames
through Vite 8's oxc-minifier. Letting babel do all TypeScript handling
keeps class fields intact through the async-arrow transform.

Verified: rebuilt host dist now emits
  loginTask = b(() => ({ context: this, generator: f... }))
instead of the untransformed
  loginTask = i(async () => { ... })
commit f53c665 normalized the debugFilename passed to transpileJS to
start with a leading slash so babel's internal path.resolve produces a
consistent moduleName across node and browser. But the same filename is
forwarded to content-tag's Preprocessor and shows up in user-facing
"Parse Error at ..." messages, where the leading slash looks wrong
(users don't expect absolute paths there).

Strip the leading slash just for content-tag. Babel still gets the
absolute path so moduleName resolution stays deterministic. Fixes three
prerender/error tests: shards 16, 18, and 20 all expected
"Parse Error at broken.gts:..." but were getting
"Parse Error at /broken.gts:...".
Under ember-cli-babel, an uninitialized class field like
    static [primitive]: number;
would be emitted as an empty class field (equivalent to
`static [primitive] = undefined`), so `primitive in NewField` was true
at runtime. The ai-function-generation "skips over fields that can't be
recognised" test relied on that: it needs the primitive symbol to be a
runtime property so the skip path in generateJsonSchemaForContainsFields
kicks in.

Under the vite build's babel config (onlyRemoveTypeImports + decorator
transforms), the uninitialized field is stripped from the emitted class,
so `primitive in NewField` is false, the non-primitive branch runs, and
an extra `skipField: { type: "object", properties: {} }` leaks into the
schema.

Use an explicit `undefined` initializer so the class property exists at
runtime regardless of which babel pipeline compiled it.
Previously testem stringified the thrown object as `[object Object]`,
hiding the underlying error. Concatenate stack/message into the log
line so the actual failure is visible in CI output.
Under Vite's ESM build, `import * as QUnit from 'qunit'` yields a frozen
module namespace, so assigning `QUnit.module = ...` throws
"Cannot set property module of #<Object> which has only a getter".
Track newly registered modules by diffing `QUnit.config.modules` around
each `runTests()` call instead.
Host suite is green on the vite branch (run 24704642390 after rerunning
one Monaco dynamic-import flake). Re-enable the full CI suites on PRs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tests/serve.json was removed when host moved to vite, but the
software-factory harness still spawned `npx serve --config tests/serve.json`
and crashed with ENOENT in CI. Invoke `vite preview` (the same tool
scripts/serve-dist.js uses) on a chosen free port instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ember-source's defaultId hashes template source via node's crypto
module, reached through `module.require` or `globalThis.require`. The
vite branch switched transpile.ts to import the ESM source entry
(`ember-source/ember-template-compiler/index.js`) where neither handle
exists, so defaultId falls back to `() => null`. The emitted template
JSON then contains the literal `"id": null` (unquoted), which breaks
realm-server card-source-endpoints-test.ts assertion 8 — the test's
`/"id":\s"[^"]+"/` normalization regex only matches quoted id strings.

Wrap compiler.precompile so we always pass an `id` option backed by
super-fast-md5. md5 is already a runtime-common dep, works identically
in node and the browser, and produces a stable 8-char token that the
test regex replaces with `"<id>"` before comparing against the
compiled-card fixture.
…nder patches

The vite host build has no window.requirejs.entries / _eak_seen module
registry to walk, so the page-side lookups silently failed: the render
route patch threw "render route module not found for injection" and the
RealmServerService bypass patch returned early, leaving
assertOwnRealmServer intact so cross-origin query-fallback searches never
dispatched `_federated-search` and RuntimeRealm.search was never invoked.

Reach the classes through the Ember ApplicationInstance that the
export-application-global instance-initializer exposes on
window['@cardstack/host'] and call factoryFor('service:realm-server') /
factoryFor('route:render') instead.

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

2 participants