From 68eff44d2d0736d687e29c894d8f5ad854e1e306 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 21 Aug 2026 11:49:55 +0200 Subject: [PATCH 1/3] fix(frontend): pin one vite copy so start serves in dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite lists `lightningcss` as an optional peer and `@tanstack/start-plugin-core` depends on it, so bun installed a second copy of Vite for the plugin. TanStack Start gates its SSR dev middleware on `isRunnableDevEnvironment()`, an `instanceof` check, which cannot match across two copies — the plugin returned without installing anything and left `vite dev` answering every route with a connect 404. That broke `bun dev` and the nightly Playwright journeys, whose webServer probe only accepts a status below 404 and whose `/index.html` fallback disappeared with the SSR migration. --- apps/frontend/CLAUDE.md | 9 ++++++ apps/frontend/package.json | 1 + .../src/tests/vite-single-instance.test.ts | 29 +++++++++++++++++++ bun.lock | 1 + 4 files changed, 40 insertions(+) create mode 100644 apps/frontend/src/tests/vite-single-instance.test.ts diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md index 4266a391b..ae3bc2dcf 100644 --- a/apps/frontend/CLAUDE.md +++ b/apps/frontend/CLAUDE.md @@ -34,6 +34,15 @@ Anything rendered by a marketing route is prerendered in a DOM-less environment, `src/tests/ssr-safety.test.tsx` guards these; keep it in the default `node` environment. +### One Vite copy + +`lightningcss` is a direct devDependency even though nothing imports it. Vite lists it as an +*optional peer*, and `@tanstack/start-plugin-core` depends on it, so without it bun installs a +second copy of Vite for the plugin. Start decides whether to install its SSR dev middleware with +an `instanceof` check against its own Vite, so two copies make that check fail silently: the dev +server starts fine and then 404s every route. Keep the dependency; `src/tests/vite-single-instance.test.ts` +guards it. + ### XState v5 - Use `setup({ ... }).createMachine(...)` — not `createMachine` directly. diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 61a8b0b17..f971523e8 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -111,6 +111,7 @@ "eslint-plugin-storybook": "^9.1.4", "husky": ">=6", "jsdom": "26", + "lightningcss": "^1.32.0", "lint-staged": ">=10", "msw": "^2.14.6", "prettier": "catalog:", diff --git a/apps/frontend/src/tests/vite-single-instance.test.ts b/apps/frontend/src/tests/vite-single-instance.test.ts new file mode 100644 index 000000000..9e1d04bc8 --- /dev/null +++ b/apps/frontend/src/tests/vite-single-instance.test.ts @@ -0,0 +1,29 @@ +import { realpathSync } from "node:fs"; +import { createRequire } from "node:module"; +import { describe, expect, it } from "vitest"; + +// TanStack Start decides whether to install its SSR dev middleware with +// `isRunnableDevEnvironment(env)`, which is an `instanceof` check. If the plugin resolves a +// different copy of Vite than the one that created the dev server, that check silently fails, +// the middleware is never installed and every dev route falls through to a 404 — the app is +// unreachable locally and the Playwright webServer never becomes ready. +// +// Bun installs a separate copy of Vite per resolved optional-peer set, so this breaks as soon +// as the app and the plugin disagree on one of them (`lightningcss` is why the frontend depends +// on it directly). Compare the resolved module paths rather than trusting the version alone. +const require = createRequire(import.meta.url); + +function resolveViteFrom(requirer: NodeRequire): string { + return realpathSync(requirer.resolve("vite")); +} + +describe("Vite module identity", () => { + it("resolves the same Vite copy for the app and for the TanStack Start plugin", () => { + const appVite = resolveViteFrom(require); + + const reactStart = createRequire(require.resolve("@tanstack/react-start/package.json")); + const startPlugin = createRequire(reactStart.resolve("@tanstack/start-plugin-core/package.json")); + + expect(resolveViteFrom(startPlugin)).toBe(appVite); + }); +}); diff --git a/bun.lock b/bun.lock index 7f8145c57..283250bb7 100644 --- a/bun.lock +++ b/bun.lock @@ -260,6 +260,7 @@ "eslint-plugin-storybook": "^9.1.4", "husky": ">=6", "jsdom": "26", + "lightningcss": "^1.32.0", "lint-staged": ">=10", "msw": "^2.14.6", "prettier": "catalog:", From 2f913567771904fec3a254a3386d3a9aa4130908 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 21 Aug 2026 11:50:03 +0200 Subject: [PATCH 2/3] ci(repo): write playwright html reports in CI The e2e workflow uploads `playwright-report/` on failure, but the CI reporter list produced only terminal output, so both upload steps found nothing and a red nightly run left no artifact to debug from. --- apps/dashboard/playwright.config.ts | 4 +++- apps/frontend/playwright.config.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/playwright.config.ts b/apps/dashboard/playwright.config.ts index f7c0d9055..39186a1fc 100644 --- a/apps/dashboard/playwright.config.ts +++ b/apps/dashboard/playwright.config.ts @@ -7,7 +7,9 @@ export default defineConfig({ forbidOnly: !!process.env.CI, fullyParallel: true, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - reporter: process.env.CI ? [["list"], ["github"]] : [["list"]], + // "html" writes playwright-report/, which the e2e workflow uploads as an artifact on + // failure — without it a red nightly run leaves nothing to debug from. + reporter: process.env.CI ? [["list"], ["github"], ["html", { open: "never" }]] : [["list"]], retries: process.env.CI ? 2 : 0, testDir: "./e2e", timeout: 60_000, diff --git a/apps/frontend/playwright.config.ts b/apps/frontend/playwright.config.ts index bb4142e79..881ab9ce1 100644 --- a/apps/frontend/playwright.config.ts +++ b/apps/frontend/playwright.config.ts @@ -7,7 +7,9 @@ export default defineConfig({ forbidOnly: !!process.env.CI, fullyParallel: true, projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - reporter: process.env.CI ? [["list"], ["github"]] : [["list"]], + // "html" writes playwright-report/, which the e2e workflow uploads as an artifact on + // failure — without it a red nightly run leaves nothing to debug from. + reporter: process.env.CI ? [["list"], ["github"], ["html", { open: "never" }]] : [["list"]], retries: process.env.CI ? 2 : 0, testDir: "./e2e", timeout: 60_000, From 6323aa0bee86d340f8cda1d77a75750fd2d5ff63 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 21 Aug 2026 11:58:43 +0200 Subject: [PATCH 3/3] docs(frontend): drop the vite copy note The rationale lives in `src/tests/vite-single-instance.test.ts`, next to the assertion that enforces it. --- apps/frontend/CLAUDE.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/apps/frontend/CLAUDE.md b/apps/frontend/CLAUDE.md index ae3bc2dcf..4266a391b 100644 --- a/apps/frontend/CLAUDE.md +++ b/apps/frontend/CLAUDE.md @@ -34,15 +34,6 @@ Anything rendered by a marketing route is prerendered in a DOM-less environment, `src/tests/ssr-safety.test.tsx` guards these; keep it in the default `node` environment. -### One Vite copy - -`lightningcss` is a direct devDependency even though nothing imports it. Vite lists it as an -*optional peer*, and `@tanstack/start-plugin-core` depends on it, so without it bun installs a -second copy of Vite for the plugin. Start decides whether to install its SSR dev middleware with -an `instanceof` check against its own Vite, so two copies make that check fail silently: the dev -server starts fine and then 404s every route. Keep the dependency; `src/tests/vite-single-instance.test.ts` -guards it. - ### XState v5 - Use `setup({ ... }).createMachine(...)` — not `createMachine` directly.