From 87738a8a45365f65d705c8d7db02e803aaa61b1e Mon Sep 17 00:00:00 2001 From: "Lisa (Cedar Assistant)" Date: Sun, 17 May 2026 16:16:05 +0200 Subject: [PATCH 01/48] chore(vite): move index.html from web/src to web Standard Vite convention is index.html at the project root (web/), not inside the source directory (web/src/). Cedar inherited the web/src/index.html location from Redwood, which forced getMergedConfig to set root: web/src so Vite could find it. Moving index.html to web/ lets Vite find it by default (configFile is in web/, so root defaults to web/). getMergedConfig no longer needs to set root at all, and the UD API server build workaround that overrode root back to web/ is no longer needed either. Changes: - packages/project-config/src/paths.ts: web.html now points to web/index.html - packages/vite/src/lib/getMergedConfig.ts: remove root: web/src override - packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts: compute entry script path relative to web/ instead of web/src/ (gives src="/src/entry.client.tsx" instead of src="/entry.client.tsx") - Move index.html from web/src/ to web/ in all fixtures and templates - Update entry.client error messages and docs referencing the old path Co-Authored-By: Claude Sonnet 4.6 --- __fixtures__/empty-project/web/{src => }/index.html | 0 .../web/{src => }/index.html | 0 __fixtures__/example-todo-main/web/{src => }/index.html | 0 __fixtures__/rsc-caching/web/src/entry.client.tsx | 2 +- __fixtures__/test-project-esm/web/{src => }/index.html | 0 __fixtures__/test-project-esm/web/src/entry.client.tsx | 2 +- __fixtures__/test-project-live/web/{src => }/index.html | 0 __fixtures__/test-project-live/web/src/entry.client.tsx | 2 +- __fixtures__/test-project-rsa/web/src/entry.client.tsx | 2 +- .../web/src/entry.client.tsx | 2 +- __fixtures__/test-project/web/{src => }/index.html | 0 __fixtures__/test-project/web/src/entry.client.tsx | 2 +- docs/docs/deploy/baremetal.md | 2 +- local-testing-project-live/web/{src => }/index.html | 0 local-testing-project-live/web/src/entry.client.tsx | 2 +- local-testing-project/web/{src => }/index.html | 0 local-testing-project/web/src/entry.client.tsx | 2 +- .../templates/esm-js/web/{src => }/index.html | 0 .../templates/esm-js/web/src/entry.client.jsx | 2 +- .../templates/esm-ts/web/{src => }/index.html | 0 .../templates/esm-ts/web/src/entry.client.tsx | 2 +- .../templates/js/web/{src => }/index.html | 0 .../templates/js/web/src/entry.client.jsx | 2 +- .../templates/ts/web/{src => }/index.html | 0 .../templates/ts/web/src/entry.client.tsx | 2 +- packages/create-cedar-app/tests/templates.test.ts | 8 ++++---- .../__fixtures__/nestedPages/web/{src => }/index.html | 0 packages/project-config/src/__tests__/paths.test.ts | 2 +- packages/project-config/src/paths.ts | 2 +- packages/vite/src/lib/getMergedConfig.ts | 1 - .../vite/src/plugins/vite-plugin-cedar-entry-injection.ts | 2 +- .../ud-tests/fixtures/cedar-app/web/{src => }/index.html | 0 32 files changed, 20 insertions(+), 21 deletions(-) rename __fixtures__/empty-project/web/{src => }/index.html (100%) rename __fixtures__/example-todo-main-with-errors/web/{src => }/index.html (100%) rename __fixtures__/example-todo-main/web/{src => }/index.html (100%) rename __fixtures__/test-project-esm/web/{src => }/index.html (100%) rename __fixtures__/test-project-live/web/{src => }/index.html (100%) rename __fixtures__/test-project/web/{src => }/index.html (100%) rename local-testing-project-live/web/{src => }/index.html (100%) rename local-testing-project/web/{src => }/index.html (100%) rename packages/create-cedar-app/templates/esm-js/web/{src => }/index.html (100%) rename packages/create-cedar-app/templates/esm-ts/web/{src => }/index.html (100%) rename packages/create-cedar-app/templates/js/web/{src => }/index.html (100%) rename packages/create-cedar-app/templates/ts/web/{src => }/index.html (100%) rename packages/internal/src/__tests__/__fixtures__/nestedPages/web/{src => }/index.html (100%) rename tasks/ud-tests/fixtures/cedar-app/web/{src => }/index.html (100%) diff --git a/__fixtures__/empty-project/web/src/index.html b/__fixtures__/empty-project/web/index.html similarity index 100% rename from __fixtures__/empty-project/web/src/index.html rename to __fixtures__/empty-project/web/index.html diff --git a/__fixtures__/example-todo-main-with-errors/web/src/index.html b/__fixtures__/example-todo-main-with-errors/web/index.html similarity index 100% rename from __fixtures__/example-todo-main-with-errors/web/src/index.html rename to __fixtures__/example-todo-main-with-errors/web/index.html diff --git a/__fixtures__/example-todo-main/web/src/index.html b/__fixtures__/example-todo-main/web/index.html similarity index 100% rename from __fixtures__/example-todo-main/web/src/index.html rename to __fixtures__/example-todo-main/web/index.html diff --git a/__fixtures__/rsc-caching/web/src/entry.client.tsx b/__fixtures__/rsc-caching/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/__fixtures__/rsc-caching/web/src/entry.client.tsx +++ b/__fixtures__/rsc-caching/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/__fixtures__/test-project-esm/web/src/index.html b/__fixtures__/test-project-esm/web/index.html similarity index 100% rename from __fixtures__/test-project-esm/web/src/index.html rename to __fixtures__/test-project-esm/web/index.html diff --git a/__fixtures__/test-project-esm/web/src/entry.client.tsx b/__fixtures__/test-project-esm/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/__fixtures__/test-project-esm/web/src/entry.client.tsx +++ b/__fixtures__/test-project-esm/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/__fixtures__/test-project-live/web/src/index.html b/__fixtures__/test-project-live/web/index.html similarity index 100% rename from __fixtures__/test-project-live/web/src/index.html rename to __fixtures__/test-project-live/web/index.html diff --git a/__fixtures__/test-project-live/web/src/entry.client.tsx b/__fixtures__/test-project-live/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/__fixtures__/test-project-live/web/src/entry.client.tsx +++ b/__fixtures__/test-project-live/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/__fixtures__/test-project-rsa/web/src/entry.client.tsx b/__fixtures__/test-project-rsa/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/__fixtures__/test-project-rsa/web/src/entry.client.tsx +++ b/__fixtures__/test-project-rsa/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/__fixtures__/test-project-rsc-kitchen-sink/web/src/entry.client.tsx b/__fixtures__/test-project-rsc-kitchen-sink/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/__fixtures__/test-project-rsc-kitchen-sink/web/src/entry.client.tsx +++ b/__fixtures__/test-project-rsc-kitchen-sink/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/__fixtures__/test-project/web/src/index.html b/__fixtures__/test-project/web/index.html similarity index 100% rename from __fixtures__/test-project/web/src/index.html rename to __fixtures__/test-project/web/index.html diff --git a/__fixtures__/test-project/web/src/entry.client.tsx b/__fixtures__/test-project/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/__fixtures__/test-project/web/src/entry.client.tsx +++ b/__fixtures__/test-project/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/docs/docs/deploy/baremetal.md b/docs/docs/deploy/baremetal.md index 5fb8b0b66d..1e5c82896a 100644 --- a/docs/docs/deploy/baremetal.md +++ b/docs/docs/deploy/baremetal.md @@ -356,7 +356,7 @@ curl http://localhost:8910 wget http://localhost:8910 ``` -If you don't see the content of your `web/src/index.html` file then something isn't working. You'll need to fix those issues before you can deploy. Verify the api side is responding: +If you don't see the content of your `web/index.html` file then something isn't working. You'll need to fix those issues before you can deploy. Verify the api side is responding: ```bash curl http://localhost:8910/.api/functions/graphql?query={redwood{version}} diff --git a/local-testing-project-live/web/src/index.html b/local-testing-project-live/web/index.html similarity index 100% rename from local-testing-project-live/web/src/index.html rename to local-testing-project-live/web/index.html diff --git a/local-testing-project-live/web/src/entry.client.tsx b/local-testing-project-live/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/local-testing-project-live/web/src/entry.client.tsx +++ b/local-testing-project-live/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/local-testing-project/web/src/index.html b/local-testing-project/web/index.html similarity index 100% rename from local-testing-project/web/src/index.html rename to local-testing-project/web/index.html diff --git a/local-testing-project/web/src/entry.client.tsx b/local-testing-project/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/local-testing-project/web/src/entry.client.tsx +++ b/local-testing-project/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/packages/create-cedar-app/templates/esm-js/web/src/index.html b/packages/create-cedar-app/templates/esm-js/web/index.html similarity index 100% rename from packages/create-cedar-app/templates/esm-js/web/src/index.html rename to packages/create-cedar-app/templates/esm-js/web/index.html diff --git a/packages/create-cedar-app/templates/esm-js/web/src/entry.client.jsx b/packages/create-cedar-app/templates/esm-js/web/src/entry.client.jsx index 915c14d76d..1761de1c81 100644 --- a/packages/create-cedar-app/templates/esm-js/web/src/entry.client.jsx +++ b/packages/create-cedar-app/templates/esm-js/web/src/entry.client.jsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/packages/create-cedar-app/templates/esm-ts/web/src/index.html b/packages/create-cedar-app/templates/esm-ts/web/index.html similarity index 100% rename from packages/create-cedar-app/templates/esm-ts/web/src/index.html rename to packages/create-cedar-app/templates/esm-ts/web/index.html diff --git a/packages/create-cedar-app/templates/esm-ts/web/src/entry.client.tsx b/packages/create-cedar-app/templates/esm-ts/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/packages/create-cedar-app/templates/esm-ts/web/src/entry.client.tsx +++ b/packages/create-cedar-app/templates/esm-ts/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/packages/create-cedar-app/templates/js/web/src/index.html b/packages/create-cedar-app/templates/js/web/index.html similarity index 100% rename from packages/create-cedar-app/templates/js/web/src/index.html rename to packages/create-cedar-app/templates/js/web/index.html diff --git a/packages/create-cedar-app/templates/js/web/src/entry.client.jsx b/packages/create-cedar-app/templates/js/web/src/entry.client.jsx index 915c14d76d..1761de1c81 100644 --- a/packages/create-cedar-app/templates/js/web/src/entry.client.jsx +++ b/packages/create-cedar-app/templates/js/web/src/entry.client.jsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/packages/create-cedar-app/templates/ts/web/src/index.html b/packages/create-cedar-app/templates/ts/web/index.html similarity index 100% rename from packages/create-cedar-app/templates/ts/web/src/index.html rename to packages/create-cedar-app/templates/ts/web/index.html diff --git a/packages/create-cedar-app/templates/ts/web/src/entry.client.tsx b/packages/create-cedar-app/templates/ts/web/src/entry.client.tsx index 915c14d76d..1761de1c81 100644 --- a/packages/create-cedar-app/templates/ts/web/src/entry.client.tsx +++ b/packages/create-cedar-app/templates/ts/web/src/entry.client.tsx @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/packages/create-cedar-app/tests/templates.test.ts b/packages/create-cedar-app/tests/templates.test.ts index fbba56270f..04191b4afd 100644 --- a/packages/create-cedar-app/tests/templates.test.ts +++ b/packages/create-cedar-app/tests/templates.test.ts @@ -72,7 +72,7 @@ test('files should not have changed unintentionally in TS template', () => { "/web/src/components/.keep", "/web/src/entry.client.tsx", "/web/src/index.css", - "/web/src/index.html", + "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", @@ -150,7 +150,7 @@ test('files should not have changed unintentionally in JS template', () => { "/web/src/components/.keep", "/web/src/entry.client.jsx", "/web/src/index.css", - "/web/src/index.html", + "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", @@ -225,7 +225,7 @@ test('files should not have changed unintentionally in esm TS template', () => { "/web/src/components/.keep", "/web/src/entry.client.tsx", "/web/src/index.css", - "/web/src/index.html", + "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", @@ -303,7 +303,7 @@ test('files should not have changed unintentionally in esm JS template', () => { "/web/src/components/.keep", "/web/src/entry.client.jsx", "/web/src/index.css", - "/web/src/index.html", + "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", diff --git a/packages/internal/src/__tests__/__fixtures__/nestedPages/web/src/index.html b/packages/internal/src/__tests__/__fixtures__/nestedPages/web/index.html similarity index 100% rename from packages/internal/src/__tests__/__fixtures__/nestedPages/web/src/index.html rename to packages/internal/src/__tests__/__fixtures__/nestedPages/web/index.html diff --git a/packages/project-config/src/__tests__/paths.test.ts b/packages/project-config/src/__tests__/paths.test.ts index c3e379abfc..39be1bf56d 100644 --- a/packages/project-config/src/__tests__/paths.test.ts +++ b/packages/project-config/src/__tests__/paths.test.ts @@ -65,7 +65,7 @@ const DEFAULT_PATHS = { generators: ['web', 'generators'], app: ['web', 'src', 'App.tsx'], document: ['web', 'src', 'Document'], - html: ['web', 'src', 'index.html'], + html: ['web', 'index.html'], config: ['web', 'config'], viteConfig: ['web', 'vite.config.ts'], postcss: ['web', 'config', 'postcss.config.cjs'], diff --git a/packages/project-config/src/paths.ts b/packages/project-config/src/paths.ts index bd4823fa82..ce9d45816f 100644 --- a/packages/project-config/src/paths.ts +++ b/packages/project-config/src/paths.ts @@ -202,7 +202,7 @@ export function getPaths(BASE_DIR: string = getBaseDir()) { generators: path.join(BASE_DIR, 'web/generators'), app: resolveFile(path.join(BASE_DIR, 'web/src/App')) as string, document: resolveFile(path.join(BASE_DIR, 'web/src/Document')) as string, - html: path.join(BASE_DIR, 'web/src/index.html'), + html: path.join(BASE_DIR, 'web/index.html'), config: path.join(BASE_DIR, 'web/config'), viteConfig, postcss: path.join(BASE_DIR, 'web/config/postcss.config.cjs'), diff --git a/packages/vite/src/lib/getMergedConfig.ts b/packages/vite/src/lib/getMergedConfig.ts index 27f0760574..8757e7ec91 100644 --- a/packages/vite/src/lib/getMergedConfig.ts +++ b/packages/vite/src/lib/getMergedConfig.ts @@ -48,7 +48,6 @@ export function getMergedConfig(cedarConfig: Config, cedarPaths: Paths) { } const defaultCedarViteConfig: ViteUserConfig = { - root: cedarPaths.web.src, resolve: { alias: workspaceAliases, }, diff --git a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts index 5d8ab9275c..1e0704bfa2 100644 --- a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts +++ b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts @@ -18,7 +18,7 @@ export function cedarEntryInjectionPlugin(): Plugin { ) } - const relativeEntryPath = path.relative(rwPaths.web.src, clientEntryPath) + const relativeEntryPath = path.relative(rwPaths.web.base, clientEntryPath) return { name: 'cedar-entry-injection', diff --git a/tasks/ud-tests/fixtures/cedar-app/web/src/index.html b/tasks/ud-tests/fixtures/cedar-app/web/index.html similarity index 100% rename from tasks/ud-tests/fixtures/cedar-app/web/src/index.html rename to tasks/ud-tests/fixtures/cedar-app/web/index.html From 40cebd80f01f40a58082d86103d435222928f0d9 Mon Sep 17 00:00:00 2001 From: "Lisa (Cedar Assistant)" Date: Sun, 17 May 2026 21:05:58 +0200 Subject: [PATCH 02/48] fix(vite): set root to web/ in getMergedConfig Vite defaults root to process.cwd(), not the config file directory. Since cedar-unified-dev and other callers don't chdir to web/ first, Vite was looking for index.html in the project root instead of web/. Set root explicitly to cedarPaths.web.base (web/) so Vite finds index.html correctly regardless of CWD. --- packages/vite/src/lib/getMergedConfig.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vite/src/lib/getMergedConfig.ts b/packages/vite/src/lib/getMergedConfig.ts index 8757e7ec91..a643a8e3e1 100644 --- a/packages/vite/src/lib/getMergedConfig.ts +++ b/packages/vite/src/lib/getMergedConfig.ts @@ -48,6 +48,7 @@ export function getMergedConfig(cedarConfig: Config, cedarPaths: Paths) { } const defaultCedarViteConfig: ViteUserConfig = { + root: cedarPaths.web.base, resolve: { alias: workspaceAliases, }, From eb7ff94794b10c4cd8b1df813cd3d0a9ad566357 Mon Sep 17 00:00:00 2001 From: "Lisa (Cedar Assistant)" Date: Sun, 17 May 2026 21:06:21 +0200 Subject: [PATCH 03/48] fix(vite): set root to web/ and normalize Windows paths Two fixes for the index.html move: 1. Set root: cedarPaths.web.base in getMergedConfig so Vite finds index.html at web/index.html. Without this, Vite defaults root to process.cwd() (the project root), causing 404 in cedar dev --ud where process.cwd() is not changed to web/ before starting. 2. Wrap path.relative with normalizePath in the entry injection plugin so the injected script src uses forward slashes on Windows (src/entry.client.tsx not src\entry.client.tsx). --- .../vite/src/plugins/vite-plugin-cedar-entry-injection.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts index 1e0704bfa2..5af11d635a 100644 --- a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts +++ b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts @@ -18,7 +18,9 @@ export function cedarEntryInjectionPlugin(): Plugin { ) } - const relativeEntryPath = path.relative(rwPaths.web.base, clientEntryPath) + const relativeEntryPath = normalizePath( + path.relative(rwPaths.web.base, clientEntryPath), + ) return { name: 'cedar-entry-injection', From dee159777901d445da86b6d17374bb157f57c58f Mon Sep 17 00:00:00 2001 From: Lisa Date: Sun, 17 May 2026 22:17:05 +0200 Subject: [PATCH 04/48] fix(create-cedar-app): fix index.html position in template snapshots The inline snapshots use .sort(), so index.html should appear alphabetically after /web and before /web/jest.config.js etc., not between src/index.css and src/layouts. --- packages/create-cedar-app/tests/templates.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/create-cedar-app/tests/templates.test.ts b/packages/create-cedar-app/tests/templates.test.ts index 04191b4afd..ee2bc35deb 100644 --- a/packages/create-cedar-app/tests/templates.test.ts +++ b/packages/create-cedar-app/tests/templates.test.ts @@ -59,6 +59,7 @@ test('files should not have changed unintentionally in TS template', () => { "/scripts/seed.ts", "/scripts/tsconfig.json", "/web", + "/web/index.html", "/web/jest.config.js", "/web/package.json", "/web/public", @@ -72,7 +73,6 @@ test('files should not have changed unintentionally in TS template', () => { "/web/src/components/.keep", "/web/src/entry.client.tsx", "/web/src/index.css", - "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", @@ -136,6 +136,7 @@ test('files should not have changed unintentionally in JS template', () => { "/scripts/jsconfig.json", "/scripts/seed.js", "/web", + "/web/index.html", "/web/jest.config.js", "/web/jsconfig.json", "/web/package.json", @@ -150,7 +151,6 @@ test('files should not have changed unintentionally in JS template', () => { "/web/src/components/.keep", "/web/src/entry.client.jsx", "/web/src/index.css", - "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", @@ -213,6 +213,7 @@ test('files should not have changed unintentionally in esm TS template', () => { "/scripts/tsconfig.json", "/vitest.config.ts", "/web", + "/web/index.html", "/web/package.json", "/web/public", "/web/public/README.md", @@ -225,7 +226,6 @@ test('files should not have changed unintentionally in esm TS template', () => { "/web/src/components/.keep", "/web/src/entry.client.tsx", "/web/src/index.css", - "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", @@ -290,6 +290,7 @@ test('files should not have changed unintentionally in esm JS template', () => { "/scripts/seed.js", "/vitest.config.mjs", "/web", + "/web/index.html", "/web/jsconfig.json", "/web/package.json", "/web/public", @@ -303,7 +304,6 @@ test('files should not have changed unintentionally in esm JS template', () => { "/web/src/components/.keep", "/web/src/entry.client.jsx", "/web/src/index.css", - "/web/index.html", "/web/src/layouts", "/web/src/layouts/.keep", "/web/src/pages", From ec2892e085ea66d6f20be7afc07f679cf8f16d80 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 06:48:26 +0200 Subject: [PATCH 05/48] fix experimental setup template files --- .../experimental/templates/rsc/entry.client.tsx.template | 2 +- .../templates/streamingSsr/entry.client.tsx.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template index 8247c96a55..a2b9a68ad2 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/entry.client.tsx.template @@ -14,7 +14,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } diff --git a/packages/cli/src/commands/experimental/templates/streamingSsr/entry.client.tsx.template b/packages/cli/src/commands/experimental/templates/streamingSsr/entry.client.tsx.template index 46849cf211..750ea2c871 100644 --- a/packages/cli/src/commands/experimental/templates/streamingSsr/entry.client.tsx.template +++ b/packages/cli/src/commands/experimental/templates/streamingSsr/entry.client.tsx.template @@ -15,7 +15,7 @@ const redwoodAppElement = document.getElementById('redwood-app') if (!redwoodAppElement) { throw new Error( "Could not find an element with ID 'redwood-app'. Please ensure it " + - "exists in your 'web/src/index.html' file." + "exists in your 'web/index.html' file." ) } From 7570168bc1f2c5783399fba202f02d52eb3244a5 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 06:58:54 +0200 Subject: [PATCH 06/48] adjust for web/src vs /src --- packages/prerender/src/runPrerender.tsx | 2 +- packages/prerender/src/runPrerenderEsm.tsx | 2 +- packages/vite/src/runFeServer.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/prerender/src/runPrerender.tsx b/packages/prerender/src/runPrerender.tsx index 1043bf5c72..8f09d9e186 100644 --- a/packages/prerender/src/runPrerender.tsx +++ b/packages/prerender/src/runPrerender.tsx @@ -198,7 +198,7 @@ function insertChunkLoadingScript( if (route?.filePath) { const pagesIndex = - route.filePath.indexOf(path.join('web', 'src', 'pages')) + 8 + route.filePath.indexOf(path.join('web', 'src', 'pages')) + 4 const pagePath = ensurePosixPath(route.filePath.slice(pagesIndex)) const pageChunkPath = buildManifest[pagePath]?.file diff --git a/packages/prerender/src/runPrerenderEsm.tsx b/packages/prerender/src/runPrerenderEsm.tsx index e0e970e35b..a4d4b5efee 100644 --- a/packages/prerender/src/runPrerenderEsm.tsx +++ b/packages/prerender/src/runPrerenderEsm.tsx @@ -198,7 +198,7 @@ function insertChunkLoadingScript( if (route?.filePath) { const pagesIndex = - route.filePath.indexOf(path.join('web', 'src', 'pages')) + 8 + route.filePath.indexOf(path.join('web', 'src', 'pages')) + 4 const pagePath = ensurePosixPath(route.filePath.slice(pagesIndex)) const pageChunkPath = buildManifest[pagePath]?.file diff --git a/packages/vite/src/runFeServer.ts b/packages/vite/src/runFeServer.ts index 35c5c525c1..35da859583 100644 --- a/packages/vite/src/runFeServer.ts +++ b/packages/vite/src/runFeServer.ts @@ -95,8 +95,8 @@ export async function runFeServer() { // In addition to all the above the discovered CSS files are also passed to // all middleware that have been registered const clientEntry = rscEnabled - ? clientBuildManifest['entry.client.tsx'] || - clientBuildManifest['entry.client.jsx'] + ? clientBuildManifest['src/entry.client.tsx'] || + clientBuildManifest['src/entry.client.jsx'] : Object.values(clientBuildManifest).find( (manifestItem) => manifestItem.isEntry, ) From da6aafab52c57e4d8d59b8d887fcf23c511803de Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 07:02:25 +0200 Subject: [PATCH 07/48] remove stale code and TODO --- packages/vite/src/buildUDApiServer.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/vite/src/buildUDApiServer.ts b/packages/vite/src/buildUDApiServer.ts index 227badc0c1..45d7616703 100644 --- a/packages/vite/src/buildUDApiServer.ts +++ b/packages/vite/src/buildUDApiServer.ts @@ -60,13 +60,6 @@ export async function buildUDApiServer({ // Load the user's Vite config so all plugins (Cedar's UD plugin, // provider plugins, etc.) run during the build. configFile: cedarPaths.web.viteConfig, - // Cedar's getMergedConfig (from the cedar() plugin) overrides root to - // web/src/. For the UD server build, we want root to be the config - // file directory (web/) so that plugins like @netlify/vite-plugin - // output their files relative to the project structure, not a source - // directory. - // TODO: See what it'd take to *not* have cedar() set it to web/src - root: cedarPaths.web.base, logLevel: verbose ? 'info' : 'warn', plugins: [ From 11d0a135c80c0fd3b2a0c78968a046a1ae0ab0f3 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 07:41:25 +0200 Subject: [PATCH 08/48] fix pre-transform error for entry script src --- .../src/plugins/vite-plugin-cedar-entry-injection.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts index 5af11d635a..df5ffa76ff 100644 --- a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts +++ b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts @@ -25,6 +25,18 @@ export function cedarEntryInjectionPlugin(): Plugin { return { name: 'cedar-entry-injection', + // Vite's dependency optimizer and dev server resolve the script tag + // src="/src/entry.client.{tsx,jsx}" as an absolute filesystem path + // (/src/entry.client.tsx) instead of relative to the Vite root. + // This hook maps it to the real file path so Vite can find it. + resolveId(id) { + if (id === '/' + relativeEntryPath) { + return clientEntryPath + } + + return null + }, + // ---------- Bundle injection ---------- // Used by Vite during dev, to inject the entrypoint. transformIndexHtml: { From dc192966c431c18a0c6db95d73432c3e1105429f Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 07:58:26 +0200 Subject: [PATCH 09/48] fix index transform --- .../vite-plugin-cedar-entry-injection.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts index df5ffa76ff..98e2c92571 100644 --- a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts +++ b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts @@ -7,9 +7,9 @@ import { normalizePath } from 'vite' import { getPaths } from '@cedarjs/project-config' export function cedarEntryInjectionPlugin(): Plugin { - const rwPaths = getPaths() + const cedarPaths = getPaths() - const clientEntryPath = rwPaths.web.entryClient + const clientEntryPath = cedarPaths.web.entryClient if (!clientEntryPath) { throw new Error( @@ -19,7 +19,7 @@ export function cedarEntryInjectionPlugin(): Plugin { } const relativeEntryPath = normalizePath( - path.relative(rwPaths.web.base, clientEntryPath), + path.relative(cedarPaths.web.base, clientEntryPath), ) return { @@ -41,7 +41,16 @@ export function cedarEntryInjectionPlugin(): Plugin { // Used by Vite during dev, to inject the entrypoint. transformIndexHtml: { order: 'pre', - handler: (html: string) => { + handler: (html: string, ctx) => { + // Only inject for the project's own index.html, not for other + // consumers of this plugin (e.g. Storybook). + if ( + ctx.filename && + normalizePath(ctx.filename) !== normalizePath(cedarPaths.web.html) + ) { + return html + } + // So we inject the entrypoint with the correct extension .tsx vs .jsx // And then inject the entry @@ -63,7 +72,7 @@ export function cedarEntryInjectionPlugin(): Plugin { transform: (code: string, id: string) => { if ( fs.existsSync(clientEntryPath) && - normalizePath(id) === normalizePath(rwPaths.web.html) + normalizePath(id) === normalizePath(cedarPaths.web.html) ) { return { code: code.replace( From c7e4da1bb341c573aadab2f707855f52f52b2c5b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 08:36:23 +0200 Subject: [PATCH 10/48] respect user config --- packages/vite/src/lib/getMergedConfig.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/lib/getMergedConfig.ts b/packages/vite/src/lib/getMergedConfig.ts index a643a8e3e1..45aa0c30b7 100644 --- a/packages/vite/src/lib/getMergedConfig.ts +++ b/packages/vite/src/lib/getMergedConfig.ts @@ -1,6 +1,5 @@ import path from 'node:path' -import type { InputOption } from 'rollup' import { mergeConfig } from 'vite' import type { ConfigEnv, ViteUserConfig } from 'vitest/config' @@ -105,7 +104,7 @@ export function getMergedConfig(cedarConfig: Config, cedarPaths: Paths) { // Note that sourcemap can be boolean or 'inline' sourcemap: !env.isSsrBuild && cedarConfig.web.sourceMap, rollupOptions: { - input: getRollupInput(!!env.isSsrBuild), + input: getRollupInput(userConfig, !!env.isSsrBuild), }, }, // @MARK: do not set buildSsrCjsExternalHeuristics here @@ -153,10 +152,16 @@ export function getMergedConfig(cedarConfig: Config, cedarPaths: Paths) { * @param ssr Whether to return the SSR inputs or not * @returns Rollup input Options */ -function getRollupInput(ssr: boolean): InputOption | undefined { +function getRollupInput(userConfig: ViteUserConfig, ssr: boolean) { const cedarConfig = getConfig() const cedarPaths = getPaths() + // Don't override the input if the user has already set it in their config. + // This is also needed for when this runs inside Storybook's Vite server. + if (userConfig.build?.rollupOptions?.input) { + return userConfig.build.rollupOptions.input + } + if (!cedarPaths.web.entryClient) { throw new Error('entryClient not defined') } From 7e6eb853446e6a89be45e063bcc485e1a48f33ad Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 18 May 2026 10:35:51 +0200 Subject: [PATCH 11/48] align vite config with web/ root default --- packages/storybook/src/preset.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 91683b7f05..3a2cf967a2 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -44,9 +44,6 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { plugins.unshift(nodePolyfills()) return mergeConfig(config, { - // This is necessary as it otherwise just points to the `web` directory, - // but it needs to point to `web/src` - root: cedarProjectPaths.web.src, plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { From 450a5d291cb8c3845a449e62c7e5c7a91971c556 Mon Sep 17 00:00:00 2001 From: Lisa Date: Mon, 18 May 2026 12:13:58 +0200 Subject: [PATCH 12/48] Restore root: cedarProjectPaths.web.src in storybook preset viteFinal This line is needed for Storybook to correctly resolve modules from web/src when using the mockRouter plugin and route helpers. Without it, routes.blogPost is undefined in stories because the module ID transform (id.includes('src')) doesn't fire correctly. This reverts the unintentional removal from commit 7e6eb853. --- packages/storybook/src/preset.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 3a2cf967a2..91683b7f05 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -44,6 +44,9 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { plugins.unshift(nodePolyfills()) return mergeConfig(config, { + // This is necessary as it otherwise just points to the `web` directory, + // but it needs to point to `web/src` + root: cedarProjectPaths.web.src, plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { From 81d3ca18bee3ba1b430e8c76d64221b1b5f15a1d Mon Sep 17 00:00:00 2001 From: Lisa Date: Mon, 18 May 2026 12:37:27 +0200 Subject: [PATCH 13/48] fix(vite): only inject entry script into Cedar's own index.html The transformIndexHtml guard had a logic error: when ctx.filename was falsy (e.g. Storybook's virtual HTML), the && short-circuit caused the condition to be false, so the Cedar entry script was injected into Storybook's HTML. This caused entry.client.tsx to load in the Storybook iframe, which crashed because #redwood-app doesn't exist, preventing routes from being populated. Fix: invert the guard so we only inject when ctx.filename is set AND matches Cedar's own index.html. Unknown/virtual HTML is now skipped. --- .../vite/src/plugins/vite-plugin-cedar-entry-injection.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts index 98e2c92571..024fd6316f 100644 --- a/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts +++ b/packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts @@ -43,9 +43,10 @@ export function cedarEntryInjectionPlugin(): Plugin { order: 'pre', handler: (html: string, ctx) => { // Only inject for the project's own index.html, not for other - // consumers of this plugin (e.g. Storybook). + // consumers of this plugin (e.g. Storybook). If ctx.filename is + // not set or doesn't match Cedar's index.html, skip injection. if ( - ctx.filename && + !ctx.filename || normalizePath(ctx.filename) !== normalizePath(cedarPaths.web.html) ) { return html From 8b319dd491ee52b530fd2b8d70d608650ac44d67 Mon Sep 17 00:00:00 2001 From: Lisa Date: Mon, 18 May 2026 12:37:34 +0200 Subject: [PATCH 14/48] revert: remove root: web/src override from storybook preset The root change was intentional (aligning with Cedar's default web/ root). The actual storybook issue was caused by a buggy transformIndexHtml guard in cedarEntryInjectionPlugin, not by the root setting. --- packages/storybook/src/preset.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 91683b7f05..3a2cf967a2 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -44,9 +44,6 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { plugins.unshift(nodePolyfills()) return mergeConfig(config, { - // This is necessary as it otherwise just points to the `web` directory, - // but it needs to point to `web/src` - root: cedarProjectPaths.web.src, plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { From a194a6e7e8400b561a7b6d7e21f2704e5792388f Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 19 May 2026 01:33:07 +0200 Subject: [PATCH 15/48] fix(storybook): filter cedar-entry-injection from Storybook's Vite config With web/ as Vite root, Storybook's Vite processes web/index.html whose absolute path matches cedarPaths.web.html, causing cedar-entry-injection to inject Cedar's entry script. This broke dep scanning and left routes un-mocked, producing "routes.blogPost is not a function" in smoke tests. Co-Authored-By: Claude Sonnet 4.6 --- packages/storybook/src/preset.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 3a2cf967a2..1f64f66dcd 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -37,13 +37,18 @@ export const previewAnnotations: StorybookConfig['previewAnnotations'] = ( const cedarProjectPaths = getPaths() export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { - const { plugins = [] } = config + // Filter out cedar-entry-injection: with web/ as Vite root, Storybook's + // Vite processes web/index.html whose path matches cedarPaths.web.html, + // so the plugin would inject Cedar's entry script and break dep scanning. + const plugins = (config.plugins ?? []).filter( + (p) => !p || Array.isArray(p) || (p as { name?: string }).name !== 'cedar-entry-injection', + ) // Needs to run before the react plugin, so add to the front plugins.unshift(await reactDocgen()) plugins.unshift(nodePolyfills()) - return mergeConfig(config, { + return mergeConfig({ ...config, plugins }, { plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { From cf0ce5573d3f45cded450e5ec24ab82f0957bf98 Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 19 May 2026 14:34:15 +0200 Subject: [PATCH 16/48] chore: fix prettier formatting in preset.ts --- packages/storybook/src/preset.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 1f64f66dcd..e421702775 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -41,7 +41,10 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // Vite processes web/index.html whose path matches cedarPaths.web.html, // so the plugin would inject Cedar's entry script and break dep scanning. const plugins = (config.plugins ?? []).filter( - (p) => !p || Array.isArray(p) || (p as { name?: string }).name !== 'cedar-entry-injection', + (p) => + !p || + Array.isArray(p) || + (p as { name?: string }).name !== 'cedar-entry-injection', ) // Needs to run before the react plugin, so add to the front From 247d035e6c0aaf1612fcae9d1d4756fb29f48623 Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 19 May 2026 14:39:55 +0200 Subject: [PATCH 17/48] chore: fix prettier formatting (mergeConfig args) --- packages/storybook/src/preset.ts | 37 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index e421702775..ac20b340dc 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -51,23 +51,26 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { plugins.unshift(await reactDocgen()) plugins.unshift(nodePolyfills()) - return mergeConfig({ ...config, plugins }, { - plugins: [mockRouter(), mockAuth(), autoImports], - resolve: { - alias: { - '~__REDWOOD__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, - '~__REDWOOD__USER_WEB_SRC': cedarProjectPaths.web.src, + return mergeConfig( + { ...config, plugins }, + { + plugins: [mockRouter(), mockAuth(), autoImports], + resolve: { + alias: { + '~__REDWOOD__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, + '~__REDWOOD__USER_WEB_SRC': cedarProjectPaths.web.src, + }, + }, + optimizeDeps: { + // Without this, on first run, Vite throws: `The file does not exist at + // "{project path}/web/node_modules/.cache/sb-vite/deps/DocsRenderer-NNNQARDV-DEXCJJZJ.js?v=c640a8fa" + // which is in the optimize deps directory.` + // This refers to @storybook/addon-docs, which is included as part of @storybook/addon-essentials. + // the docs addon then includes itself here: https://github.com/storybookjs/storybook/blob/a496ec48c708eed753a5251d55fa07947a869e62/code/addons/docs/src/preset.ts#L198C3-L198C27 + // which I believe gets included by the builder here: https://github.com/storybookjs/storybook/blob/a496ec48c708eed753a5251d55fa07947a869e62/code/builders/builder-vite/src/optimizeDeps.ts#L117 + // TODO: Figure out why this error is being thrown so that this can be removed. + exclude: ['@storybook/addon-docs'], }, }, - optimizeDeps: { - // Without this, on first run, Vite throws: `The file does not exist at - // "{project path}/web/node_modules/.cache/sb-vite/deps/DocsRenderer-NNNQARDV-DEXCJJZJ.js?v=c640a8fa" - // which is in the optimize deps directory.` - // This refers to @storybook/addon-docs, which is included as part of @storybook/addon-essentials. - // the docs addon then includes itself here: https://github.com/storybookjs/storybook/blob/a496ec48c708eed753a5251d55fa07947a869e62/code/addons/docs/src/preset.ts#L198C3-L198C27 - // which I believe gets included by the builder here: https://github.com/storybookjs/storybook/blob/a496ec48c708eed753a5251d55fa07947a869e62/code/builders/builder-vite/src/optimizeDeps.ts#L117 - // TODO: Figure out why this error is being thrown so that this can be removed. - exclude: ['@storybook/addon-docs'], - }, - }) + ) } From dd2be92bf786e4ff85a94606a9a1282793a1894d Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 19 May 2026 15:05:10 +0200 Subject: [PATCH 18/48] fix(storybook): restore root: web/src to fix routes dep scan Without root: web/src, Vite's esbuild dep scan picks up web/index.html and follows imports into web/src/ Cell files. Cell files have no default export (Cedar's Cell plugin doesn't run during esbuild dep scan), causing 'No matching export' errors. This breaks pre-bundling of storybook-framework-cedarjs, so require() in MockProviders.js fails at runtime (no __require shim in the bundle), the catch block fires, UserRoutes is set to an empty component, and routes never get populated -- hence 'routes.blogPost is not a function' in the smoke tests. Setting root: web/src avoids scanning web/index.html. Since index.html no longer exists at web/src/ after this PR's move, cedar-entry-injection won't fire there anyway, but we keep the filter as belt-and-suspenders. --- packages/storybook/src/preset.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index ac20b340dc..ceb4aa9d21 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -54,6 +54,14 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { return mergeConfig( { ...config, plugins }, { + // With web/ as Vite root, esbuild's dep scan picks up web/index.html and + // follows imports into web/src/ Cell files, which have no default export + // (Cedar's Cell plugin isn't run during esbuild dep scan). This breaks + // pre-bundling of storybook-framework-cedarjs, causing require() in + // MockProviders.js to fail at runtime and leaving routes unpopulated. + // Setting root to web/src avoids scanning web/index.html and keeps dep + // optimization working exactly as it did before the index.html move. + root: cedarProjectPaths.web.src, plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { From 338f4470d2b10bc09a99d538a802fb23494c0a98 Mon Sep 17 00:00:00 2001 From: Lisa Date: Tue, 19 May 2026 20:34:42 +0200 Subject: [PATCH 19/48] fix(storybook): replace require() with static ESM import in MockProviders The require() call in MockProviders.js failed in browser ESM context when Vite's dep optimizer didn't pre-bundle storybook-framework-cedarjs (which happens when web/src/index.html no longer exists as the Vite root entry). Replace the CJS require() / try-catch pattern with a static ESM import so Vite resolves the ~__REDWOOD__USER_ROUTES_FOR_MOCK alias correctly at build time, regardless of dep optimization behavior. Also add an ambient type declaration and tsconfig paths entry so TypeScript is happy with the virtual module alias. Co-Authored-By: Claude Sonnet 4.6 --- packages/storybook/src/ambient.d.ts | 7 +++++++ packages/storybook/src/mocks/MockProviders.tsx | 11 +++-------- packages/storybook/tsconfig.json | 5 ++++- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 packages/storybook/src/ambient.d.ts diff --git a/packages/storybook/src/ambient.d.ts b/packages/storybook/src/ambient.d.ts new file mode 100644 index 0000000000..8ff9eb40c5 --- /dev/null +++ b/packages/storybook/src/ambient.d.ts @@ -0,0 +1,7 @@ +import type React from 'react' + +// Virtual module alias resolved by Vite to the user's Routes file +declare module '~__REDWOOD__USER_ROUTES_FOR_MOCK' { + const Routes: React.FC + export default Routes +} diff --git a/packages/storybook/src/mocks/MockProviders.tsx b/packages/storybook/src/mocks/MockProviders.tsx index ccb6cd69cf..13569c42ef 100644 --- a/packages/storybook/src/mocks/MockProviders.tsx +++ b/packages/storybook/src/mocks/MockProviders.tsx @@ -14,14 +14,9 @@ import { MockParamsProvider } from './MockParamsProvider.js' // Import the user's Routes from `./web/src/Routes.{tsx,jsx}`, // we pass the `children` from the user's Routes to `./MockRouter.Router` // so that we can populate the `routes object` in Storybook and tests. -let UserRoutes: React.FC - -try { - const userRoutesModule = require('~__REDWOOD__USER_ROUTES_FOR_MOCK') - UserRoutes = userRoutesModule.default -} catch { - UserRoutes = () => <> -} +// Use a static ESM import so Vite resolves the alias correctly without +// relying on CJS `require()`, which fails in browser ESM context. +import UserRoutes from '~__REDWOOD__USER_ROUTES_FOR_MOCK' // TODO(pc): see if there are props we want to allow to be passed into our mock provider (e.g. AuthProviderProps) export const MockProviders: React.FunctionComponent<{ diff --git a/packages/storybook/tsconfig.json b/packages/storybook/tsconfig.json index ad1154b4cc..511f5f72b8 100644 --- a/packages/storybook/tsconfig.json +++ b/packages/storybook/tsconfig.json @@ -6,7 +6,10 @@ "moduleResolution": "nodenext", "outDir": "./dist", "rootDir": "./src", - "types": ["node", "vite/client"] + "types": ["node", "vite/client"], + "paths": { + "~__REDWOOD__USER_ROUTES_FOR_MOCK": ["./src/ambient.d.ts"] + } }, "include": ["src/**/*"] } From e82e52849cf9f2f190caad089933bb9572381ebb Mon Sep 17 00:00:00 2001 From: Lisa Date: Wed, 20 May 2026 01:19:37 +0200 Subject: [PATCH 20/48] fix(storybook): remove root override from viteFinal The root: web/src override was added as a shortcut to avoid Cell dep scan errors, but it's not needed. Both web/src/index.html (main) and web/index.html (this PR) have no static script tags, so esbuild dep scan finds nothing to follow from the HTML entry. The cedar-entry-injection plugin is already filtered out to prevent dynamic injection during scan. Co-Authored-By: Tobbe Lundberg --- packages/storybook/src/preset.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index ceb4aa9d21..ac20b340dc 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -54,14 +54,6 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { return mergeConfig( { ...config, plugins }, { - // With web/ as Vite root, esbuild's dep scan picks up web/index.html and - // follows imports into web/src/ Cell files, which have no default export - // (Cedar's Cell plugin isn't run during esbuild dep scan). This breaks - // pre-bundling of storybook-framework-cedarjs, causing require() in - // MockProviders.js to fail at runtime and leaving routes unpopulated. - // Setting root to web/src avoids scanning web/index.html and keeps dep - // optimization working exactly as it did before the index.html move. - root: cedarProjectPaths.web.src, plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { From da03110d711ddf835080c9f32e6ab5000e654af6 Mon Sep 17 00:00:00 2001 From: Lisa Date: Wed, 20 May 2026 01:47:07 +0200 Subject: [PATCH 21/48] fix(storybook): exclude storybook-framework-cedarjs from dep pre-bundling MockProviders now uses a static ESM import of ~__REDWOOD__USER_ROUTES_FOR_MOCK. When esbuild pre-bundles storybook-framework-cedarjs, it resolves that alias to Routes.tsx, then tries to bundle Cell files which have no default export (Cedar's Cell transform plugin doesn't run during esbuild dep scan). This causes pre-bundling to fail entirely, so the browser can't fetch the module. Excluding the package from pre-bundling lets Vite serve it directly through its normal transform pipeline, which does run the Cell plugin, so Cells get their default exports and aliases resolve correctly at request time. --- packages/storybook/src/preset.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index ac20b340dc..39e3f6e060 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -69,7 +69,16 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // the docs addon then includes itself here: https://github.com/storybookjs/storybook/blob/a496ec48c708eed753a5251d55fa07947a869e62/code/addons/docs/src/preset.ts#L198C3-L198C27 // which I believe gets included by the builder here: https://github.com/storybookjs/storybook/blob/a496ec48c708eed753a5251d55fa07947a869e62/code/builders/builder-vite/src/optimizeDeps.ts#L117 // TODO: Figure out why this error is being thrown so that this can be removed. - exclude: ['@storybook/addon-docs'], + // + // Exclude storybook-framework-cedarjs from pre-bundling. Its + // MockProviders module has a static import of the + // ~__REDWOOD__USER_ROUTES_FOR_MOCK alias, which esbuild would try to + // resolve during pre-bundling. That leads esbuild into user Cell files + // which have no default export (Cedar's Cell transform doesn't run + // during esbuild dep scan), causing pre-bundling to fail entirely. + // When excluded, Vite serves the package directly through its normal + // transform pipeline, which does run the Cell plugin correctly. + exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], }, }, ) From 499a5aed776ba6dd9b3c7a29917e40aa99f6596d Mon Sep 17 00:00:00 2001 From: Lisa Date: Wed, 20 May 2026 06:48:31 +0200 Subject: [PATCH 22/48] fix(storybook): move React import inside declare module block Top-level 'import type' turns ambient.d.ts into an ES module, making 'declare module' an augmentation rather than a global ambient declaration. Moving the import inside the block keeps the file as a script so TypeScript picks up the virtual module type automatically via 'include'. Also removes the now-redundant 'paths' entry from tsconfig.json. --- packages/storybook/src/ambient.d.ts | 3 +-- packages/storybook/tsconfig.json | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/storybook/src/ambient.d.ts b/packages/storybook/src/ambient.d.ts index 8ff9eb40c5..60608c848c 100644 --- a/packages/storybook/src/ambient.d.ts +++ b/packages/storybook/src/ambient.d.ts @@ -1,7 +1,6 @@ -import type React from 'react' - // Virtual module alias resolved by Vite to the user's Routes file declare module '~__REDWOOD__USER_ROUTES_FOR_MOCK' { + import type React from 'react' const Routes: React.FC export default Routes } diff --git a/packages/storybook/tsconfig.json b/packages/storybook/tsconfig.json index 511f5f72b8..ad1154b4cc 100644 --- a/packages/storybook/tsconfig.json +++ b/packages/storybook/tsconfig.json @@ -6,10 +6,7 @@ "moduleResolution": "nodenext", "outDir": "./dist", "rootDir": "./src", - "types": ["node", "vite/client"], - "paths": { - "~__REDWOOD__USER_ROUTES_FOR_MOCK": ["./src/ambient.d.ts"] - } + "types": ["node", "vite/client"] }, "include": ["src/**/*"] } From 50eff1f7ac870e121afa9f81d86a34306871f3d6 Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 21 May 2026 18:22:14 +0200 Subject: [PATCH 23/48] fix(storybook): force pre-bundle apollo CJS files excluded via storybook-framework-cedarjs storybook-framework-cedarjs is excluded from dep pre-bundling to prevent esbuild from scanning through user Cell files. Side effect: its transitive CJS deps (@apollo/client/cache/cache.cjs, utilities.cjs) are never scanned by esbuild either. At runtime Vite falls back to the ?import CJS interop for these files, but that mechanism cannot statically detect named exports like createFragmentRegistry, causing: SyntaxError: The requested module cache.cjs?import does not provide an export named createFragmentRegistry Fix: explicitly list the two CJS files in optimizeDeps.include to force esbuild to pre-bundle them, which correctly exposes all named exports. --- packages/storybook/src/preset.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index d4f4fd3bc1..2474ac476c 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -57,8 +57,8 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { - '~__CEDAR__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, - '~__CEDAR__USER_WEB_SRC': cedarProjectPaths.web.src, + '~__REDWOOD__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, + '~__REDWOOD__USER_WEB_SRC': cedarProjectPaths.web.src, }, }, optimizeDeps: { @@ -72,14 +72,24 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // // Exclude storybook-framework-cedarjs from pre-bundling. Its // MockProviders module has a static import of the - // ~__CEDAR__USER_ROUTES_FOR_MOCK alias, which esbuild would try to + // ~__REDWOOD__USER_ROUTES_FOR_MOCK alias, which esbuild would try to // resolve during pre-bundling. That leads esbuild into user Cell files - // which have no default export at that point (Cedar's Cell transform - // doesn't run during esbuild dep scan), causing pre-bundled Cell - // components to silently resolve as undefined. + // which have no default export (Cedar's Cell transform doesn't run + // during esbuild dep scan), causing pre-bundling to fail entirely. // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], + // Explicitly force-include the CJS files from @cedarjs/web that are + // imported by the excluded storybook-framework-cedarjs package tree. + // Because storybook-framework-cedarjs is excluded from dep + // pre-bundling, its transitive CJS deps are never scanned by esbuild. + // Without this, Vite falls back to a runtime `?import` CJS interop + // that can't statically detect named exports like `createFragmentRegistry`, + // causing a SyntaxError at runtime in the browser. + include: [ + '@apollo/client/cache/cache.cjs', + '@apollo/client/utilities/utilities.cjs', + ], }, }, ) From 8d633681df6d5b024639809b2fddfbb9f0ca4404 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 09:52:32 -0700 Subject: [PATCH 24/48] correct aliases --- packages/storybook/src/preset.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 2474ac476c..1839904be2 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -57,8 +57,8 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { plugins: [mockRouter(), mockAuth(), autoImports], resolve: { alias: { - '~__REDWOOD__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, - '~__REDWOOD__USER_WEB_SRC': cedarProjectPaths.web.src, + '~__CEDAR__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, + '~__CEDAR__USER_WEB_SRC': cedarProjectPaths.web.src, }, }, optimizeDeps: { From d31068efda71a5219e1601c53c4b276acb20bf59 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 11:04:45 -0700 Subject: [PATCH 25/48] restore comment --- packages/storybook/src/preset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 1839904be2..6fc4f3622d 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -72,7 +72,7 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // // Exclude storybook-framework-cedarjs from pre-bundling. Its // MockProviders module has a static import of the - // ~__REDWOOD__USER_ROUTES_FOR_MOCK alias, which esbuild would try to + // ~__CEDAR__USER_ROUTES_FOR_MOCK alias, which esbuild would try to // resolve during pre-bundling. That leads esbuild into user Cell files // which have no default export (Cedar's Cell transform doesn't run // during esbuild dep scan), causing pre-bundling to fail entirely. From 112a613dc28b40e51a9bf359ea69da15d58d815f Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 21 May 2026 20:06:16 +0200 Subject: [PATCH 26/48] fix(storybook): alias apollo .cjs sub-paths to ESM equivalents in viteFinal Replace the broken optimizeDeps.include approach (which caused dep scan failures) with resolve.alias entries that redirect all explicit @apollo/client/*.cjs sub-path imports to their package-root equivalents. @cedarjs/web imports Apollo Client via raw .cjs paths like: @apollo/client/cache/cache.cjs @apollo/client/link/core/core.cjs @apollo/client/react/react.cjs (and more) These work fine in Node.js / tsc builds, but Vite (browser, ESM-first) serves them via a ?import CJS interop that cannot statically detect named exports, causing SyntaxErrors like: 'cache.cjs?import' does not provide an export named 'createFragmentRegistry' 'core.cjs?import' does not provide an export named 'ApolloLink' Aliasing to @apollo/client/cache, @apollo/client/link/core, etc. lets Vite resolve via Apollo's package.json exports field (which maps to the ESM build), making all named exports visible to esbuild's dep pre-bundling. --- packages/storybook/src/preset.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 6fc4f3622d..f66c5a63ce 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -59,6 +59,23 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { alias: { '~__CEDAR__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, '~__CEDAR__USER_WEB_SRC': cedarProjectPaths.web.src, + // @cedarjs/web imports @apollo/client via explicit .cjs sub-paths + // (e.g. `@apollo/client/cache/cache.cjs`). In a Node.js / tsc build + // context these resolve fine, but Vite (ESM-first, browser target) + // falls back to a ?import CJS interop that can't statically detect + // named exports, causing runtime SyntaxErrors. Alias the .cjs paths + // to their package-root equivalents so Vite resolves them through + // Apollo's package.json `exports` field and picks up the ESM build. + '@apollo/client/cache/cache.cjs': '@apollo/client/cache', + '@apollo/client/core/core.cjs': '@apollo/client/core', + '@apollo/client/link/context/context.cjs': + '@apollo/client/link/context', + '@apollo/client/link/core/core.cjs': '@apollo/client/link/core', + '@apollo/client/link/persisted-queries/persisted-queries.cjs': + '@apollo/client/link/persisted-queries', + '@apollo/client/react/hooks/hooks.cjs': '@apollo/client/react/hooks', + '@apollo/client/react/react.cjs': '@apollo/client/react', + '@apollo/client/utilities/utilities.cjs': '@apollo/client/utilities', }, }, optimizeDeps: { @@ -79,17 +96,6 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], - // Explicitly force-include the CJS files from @cedarjs/web that are - // imported by the excluded storybook-framework-cedarjs package tree. - // Because storybook-framework-cedarjs is excluded from dep - // pre-bundling, its transitive CJS deps are never scanned by esbuild. - // Without this, Vite falls back to a runtime `?import` CJS interop - // that can't statically detect named exports like `createFragmentRegistry`, - // causing a SyntaxError at runtime in the browser. - include: [ - '@apollo/client/cache/cache.cjs', - '@apollo/client/utilities/utilities.cjs', - ], }, }, ) From 079a361c67f2533e7d9458531cd571a6da014b35 Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 21 May 2026 20:31:21 +0200 Subject: [PATCH 27/48] fix(storybook): alias graphql/language/printer.js to graphql package root graphql v16 ships CJS-only. When storybook-framework-cedarjs is excluded from optimizeDeps, transitive CJS deps like graphql/language/printer.js aren't pre-bundled and Vite's ?import interop can't detect named exports. Aliasing to the graphql package root routes through the exports field. --- packages/storybook/src/preset.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index f66c5a63ce..7d5b4f57ab 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -76,6 +76,9 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { '@apollo/client/react/hooks/hooks.cjs': '@apollo/client/react/hooks', '@apollo/client/react/react.cjs': '@apollo/client/react', '@apollo/client/utilities/utilities.cjs': '@apollo/client/utilities', + // graphql ships CJS-only; alias the explicit sub-path to the package + // root so Vite resolves it through the exports field (ESM build). + 'graphql/language/printer.js': 'graphql', }, }, optimizeDeps: { From 26c119e5277f34093a11ab061d4c7d85d208bc75 Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 21 May 2026 20:53:41 +0200 Subject: [PATCH 28/48] fix(storybook): include rehackt in optimizeDeps to force pre-bundling rehackt is a CJS-only package used by @apollo/client/react. It's only reachable via storybook-framework-cedarjs which is excluded from dep scanning, so Vite's ?import interop fails to detect named exports. Forcing it into optimizeDeps.include bypasses the scan exclusion. --- packages/storybook/src/preset.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 7d5b4f57ab..ddddf8eded 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -99,6 +99,10 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], + // Force pre-bundling of CJS-only packages that are only reachable through + // storybook-framework-cedarjs (excluded above). Without this, Vite serves + // them via ?import interop, which can't detect named exports in CJS files. + include: ['rehackt'], }, }, ) From 699b20216d0997f45ad2ca8a059a4f8f093cf282 Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 21 May 2026 22:06:15 +0200 Subject: [PATCH 29/48] fix(storybook): also define ~__REDWOOD__ aliases for backward compat MockProviders.tsx and StorybookProvider.tsx still import via the ~__REDWOOD__ alias names. The preset was updated to ~__CEDAR__ but the source files weren't changed, causing mocks to not load and GraphQLHooksProvider not to be found in nested Cell stories. Define both until the source files are updated. --- packages/storybook/src/preset.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index ddddf8eded..4fdee77773 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -58,6 +58,8 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { resolve: { alias: { '~__CEDAR__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, + '~__REDWOOD__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, + '~__REDWOOD__USER_WEB_SRC': cedarProjectPaths.web.src, '~__CEDAR__USER_WEB_SRC': cedarProjectPaths.web.src, // @cedarjs/web imports @apollo/client via explicit .cjs sub-paths // (e.g. `@apollo/client/cache/cache.cjs`). In a Node.js / tsc build From bac6832135b545f30792ec92f014a5581e2a5d07 Mon Sep 17 00:00:00 2001 From: Lisa Date: Thu, 21 May 2026 22:14:20 +0200 Subject: [PATCH 30/48] fix(storybook): remove ~__REDWOOD__ backward compat aliases from preset Source files (MockProviders, StorybookProvider, ambient.d.ts) already use ~__CEDAR__ aliases. Remove the duplicate ~__REDWOOD__ entries from preset.ts. --- packages/storybook/src/preset.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 4fdee77773..ddddf8eded 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -58,8 +58,6 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { resolve: { alias: { '~__CEDAR__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, - '~__REDWOOD__USER_ROUTES_FOR_MOCK': cedarProjectPaths.web.routes, - '~__REDWOOD__USER_WEB_SRC': cedarProjectPaths.web.src, '~__CEDAR__USER_WEB_SRC': cedarProjectPaths.web.src, // @cedarjs/web imports @apollo/client via explicit .cjs sub-paths // (e.g. `@apollo/client/cache/cache.cjs`). In a Node.js / tsc build From a2039fb5c2c13296014e8da5b50f10fc0d949593 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 15:13:23 -0700 Subject: [PATCH 31/48] fix(storybook): exclude @cedarjs/web from Vite dep pre-bundling When @cedarjs/web is pre-bundled by Vite's dep optimizer it creates a separate module instance from the one wrapped by the StorybookProvider decorator (which sets up RedwoodApolloProvider / GraphQLHooksProvider). Cells that call useQuery then hit the unwrapped instance and throw: 'You must register a useQuery hook via the GraphQLHooksProvider' Excluding @cedarjs/web and @cedarjs/web/apollo from optimizeDeps ensures all code shares the same module instance served through Vite's normal transform pipeline. --- packages/storybook/src/preset.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index ddddf8eded..cc570efbaf 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -98,7 +98,12 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // during esbuild dep scan), causing pre-bundling to fail entirely. // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. - exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], + exclude: [ + '@storybook/addon-docs', + 'storybook-framework-cedarjs', + '@cedarjs/web', + '@cedarjs/web/apollo', + ], // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. From f9216436212df7ee98ae8d2b7ab8ce67af48fc5c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 15:35:06 -0700 Subject: [PATCH 32/48] Revert "fix(storybook): exclude @cedarjs/web from Vite dep pre-bundling" This reverts commit a2039fb5c2c13296014e8da5b50f10fc0d949593. --- packages/storybook/src/preset.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index cc570efbaf..ddddf8eded 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -98,12 +98,7 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // during esbuild dep scan), causing pre-bundling to fail entirely. // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. - exclude: [ - '@storybook/addon-docs', - 'storybook-framework-cedarjs', - '@cedarjs/web', - '@cedarjs/web/apollo', - ], + exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. From 02c30234a4b4fc1ac1d477f53e9cf526e480ad85 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 15:35:42 -0700 Subject: [PATCH 33/48] fix(storybook): pre-bundle react-hook-form and @cedarjs/forms When Storybook renders a story that uses forms, Vite discovers react-hook-form and @cedarjs/forms mid-run and triggers a dep re-optimisation + full page reload. That reload creates a new module instance split: the GraphQLHooksProvider context set up by StorybookProvider's decorator is torn down and not re-established for the reloaded page, so any Cell nested in that story calls useQuery without a provider and throws. Adding these packages to optimizeDeps.include ensures they are pre-bundled from the start, preventing the mid-run reload. --- packages/storybook/src/preset.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index ddddf8eded..55acfadce4 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -102,7 +102,11 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. - include: ['rehackt'], + // Also include packages that trigger a mid-run Vite dep re-optimisation + // (and page reload) if discovered late: that reload causes a module + // instance split where the GraphQLHooksProvider context set up by the + // StorybookProvider decorator is lost, so Cells can't find useQuery. + include: ['rehackt', 'react-hook-form', '@cedarjs/forms'], }, }, ) From b4ea2e176e5a5b658e8cd0d4f02d7f4ac58d184c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 17:10:54 -0700 Subject: [PATCH 34/48] fix(storybook): exclude @cedarjs/web from pre-bundling to fix context split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When @cedarjs/web and @cedarjs/web/apollo are pre-bundled as separate esbuild entries, esbuild inlines GraphQLHooksProvider.js into each chunk independently, producing two distinct GraphQLHooksContext instances. The StorybookProvider decorator (via storybook-framework- cedarjs) registers useQuery on one instance; Cells in nested stories call useQuery via the other, causing: 'You must register a useQuery hook via the GraphQLHooksProvider' Excluding both from pre-bundling means Vite serves them through its transform pipeline, where all imports of the same file path resolve to the same module instance. Also adds invariant (a CJS-only transitive dep of @cedarjs/web) to optimizeDeps.include so Vite converts it to ESM — without this it causes 'does not provide an export named default' errors at runtime. And adds several packages to optimizeDeps.include to prevent mid- session dep re-optimisation reloads that would also tear down the GraphQLHooksProvider context. --- packages/storybook/src/preset.ts | 43 +++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 55acfadce4..61fa18ceb5 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -98,15 +98,46 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // during esbuild dep scan), causing pre-bundling to fail entirely. // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. - exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], + exclude: [ + '@storybook/addon-docs', + 'storybook-framework-cedarjs', + // Exclude @cedarjs/web and its apollo sub-entry from pre-bundling so + // they are served through Vite's transform pipeline. Pre-bundling them + // as separate esbuild entries causes each chunk to inline its own copy + // of GraphQLHooksProvider.js, producing two distinct + // GraphQLHooksContext instances — the StorybookProvider decorator + // registers useQuery on one instance while Cells read from the other, + // causing "You must register a useQuery hook" errors for nested Cells. + // Serving both through the transform pipeline means all imports of + // GraphQLHooksProvider.js resolve to the same file-level module. + '@cedarjs/web', + '@cedarjs/web/apollo', + ], // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. - // Also include packages that trigger a mid-run Vite dep re-optimisation - // (and page reload) if discovered late: that reload causes a module - // instance split where the GraphQLHooksProvider context set up by the - // StorybookProvider decorator is lost, so Cells can't find useQuery. - include: ['rehackt', 'react-hook-form', '@cedarjs/forms'], + // + // The remaining entries prevent mid-session Vite dep re-optimisation + // reloads. When Vite discovers a new dep mid-session it triggers a full + // page reload, which tears down the StorybookProvider decorator's + // GraphQLHooksProvider context so subsequent nested-Cell stories throw. + include: [ + 'rehackt', + 'react-hook-form', + '@cedarjs/forms', + '@cedarjs/testing/web/MockRouter.js', + '@cedarjs/testing/auth', + '@cedarjs/auth-dbauth-web', + 'graphql-tag', + // invariant is a transitive dep of @cedarjs/web (which is excluded + // above). Since it's CJS-only, serving it raw without pre-bundling + // causes "does not provide an export named 'default'" errors. Force + // pre-bundling here so Vite converts it to ESM. + 'invariant', + 'vite-plugin-node-polyfills/shims/buffer', + 'vite-plugin-node-polyfills/shims/global', + 'vite-plugin-node-polyfills/shims/process', + ], }, }, ) From 7c61de95978061aa2ce3c6be68c43a9cb2801afa Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 17:34:28 -0700 Subject: [PATCH 35/48] Revert "fix(storybook): exclude @cedarjs/web from pre-bundling to fix context split" This reverts commit b4ea2e176e5a5b658e8cd0d4f02d7f4ac58d184c. --- packages/storybook/src/preset.ts | 43 +++++--------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 61fa18ceb5..55acfadce4 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -98,46 +98,15 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // during esbuild dep scan), causing pre-bundling to fail entirely. // When excluded, Vite serves the package directly through its normal // transform pipeline, which does run the Cell plugin correctly. - exclude: [ - '@storybook/addon-docs', - 'storybook-framework-cedarjs', - // Exclude @cedarjs/web and its apollo sub-entry from pre-bundling so - // they are served through Vite's transform pipeline. Pre-bundling them - // as separate esbuild entries causes each chunk to inline its own copy - // of GraphQLHooksProvider.js, producing two distinct - // GraphQLHooksContext instances — the StorybookProvider decorator - // registers useQuery on one instance while Cells read from the other, - // causing "You must register a useQuery hook" errors for nested Cells. - // Serving both through the transform pipeline means all imports of - // GraphQLHooksProvider.js resolve to the same file-level module. - '@cedarjs/web', - '@cedarjs/web/apollo', - ], + exclude: ['@storybook/addon-docs', 'storybook-framework-cedarjs'], // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. - // - // The remaining entries prevent mid-session Vite dep re-optimisation - // reloads. When Vite discovers a new dep mid-session it triggers a full - // page reload, which tears down the StorybookProvider decorator's - // GraphQLHooksProvider context so subsequent nested-Cell stories throw. - include: [ - 'rehackt', - 'react-hook-form', - '@cedarjs/forms', - '@cedarjs/testing/web/MockRouter.js', - '@cedarjs/testing/auth', - '@cedarjs/auth-dbauth-web', - 'graphql-tag', - // invariant is a transitive dep of @cedarjs/web (which is excluded - // above). Since it's CJS-only, serving it raw without pre-bundling - // causes "does not provide an export named 'default'" errors. Force - // pre-bundling here so Vite converts it to ESM. - 'invariant', - 'vite-plugin-node-polyfills/shims/buffer', - 'vite-plugin-node-polyfills/shims/global', - 'vite-plugin-node-polyfills/shims/process', - ], + // Also include packages that trigger a mid-run Vite dep re-optimisation + // (and page reload) if discovered late: that reload causes a module + // instance split where the GraphQLHooksProvider context set up by the + // StorybookProvider decorator is lost, so Cells can't find useQuery. + include: ['rehackt', 'react-hook-form', '@cedarjs/forms'], }, }, ) From 5f5f8d5f26bb7aea1c7078ac4ef99ae632d4f17a Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 17:35:43 -0700 Subject: [PATCH 36/48] fix(storybook): eagerly pre-bundle Apollo and Cedar deps to prevent mid-session reloads Vite's dep optimizer doesn't use resolve.alias, so the Apollo .cjs sub-path aliases only apply in the transform pipeline. The .cjs imports inside @cedarjs/web's dist are discovered on first page load, triggering a mid-session dep re-optimisation and full page reload. That reload tears down the StorybookProvider decorator's GraphQLHooksProvider context. Subsequent stories that contain nested Cells then call useQuery without a provider and throw. Fix: add all Apollo .cjs sub-paths (and other late-discovered deps) to optimizeDeps.include so they are pre-bundled from startup, preventing the disruptive mid-session reload entirely. --- packages/storybook/src/preset.ts | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 55acfadce4..6309177c30 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -106,7 +106,36 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // (and page reload) if discovered late: that reload causes a module // instance split where the GraphQLHooksProvider context set up by the // StorybookProvider decorator is lost, so Cells can't find useQuery. - include: ['rehackt', 'react-hook-form', '@cedarjs/forms'], + include: [ + 'rehackt', + 'react-hook-form', + '@cedarjs/forms', + 'humanize-string', + // @cedarjs/web imports Apollo via explicit .cjs sub-paths. The + // resolve.alias above redirects these in Vite's transform pipeline, + // but esbuild (dep scanner) doesn't use resolve.alias. So Vite + // discovers them on first load and triggers a mid-session reload, + // which tears down the StorybookProvider's GraphQLHooksProvider + // context. Pre-bundling them eagerly prevents that reload. + '@apollo/client/cache/cache.cjs', + '@apollo/client/core/core.cjs', + '@apollo/client/link/context/context.cjs', + '@apollo/client/link/core/core.cjs', + '@apollo/client/link/persisted-queries/persisted-queries.cjs', + '@apollo/client/react/hooks/hooks.cjs', + '@apollo/client/react/react.cjs', + '@apollo/client/utilities/utilities.cjs', + 'graphql/language/printer.js', + 'graphql-tag', + '@cedarjs/web', + '@cedarjs/web/apollo', + '@cedarjs/testing/web/MockRouter.js', + '@cedarjs/testing/auth', + '@cedarjs/auth-dbauth-web', + 'vite-plugin-node-polyfills/shims/buffer', + 'vite-plugin-node-polyfills/shims/global', + 'vite-plugin-node-polyfills/shims/process', + ], }, }, ) From 53d9aaa1ec1a39bb856d197f7b12b5fbda0c2ae7 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 18:29:20 -0700 Subject: [PATCH 37/48] Add esbuild dep scan plugin --- packages/storybook/src/preset.ts | 62 +++++++++++++++----------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 6309177c30..cfbc625fc8 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -2,6 +2,7 @@ import { createRequire } from 'node:module' import path from 'node:path' import type { PresetProperty } from '@storybook/types' +import type { PluginBuild } from 'esbuild' import { mergeConfig } from 'vite' import { nodePolyfills } from 'vite-plugin-node-polyfills' @@ -102,40 +103,33 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. - // Also include packages that trigger a mid-run Vite dep re-optimisation - // (and page reload) if discovered late: that reload causes a module - // instance split where the GraphQLHooksProvider context set up by the - // StorybookProvider decorator is lost, so Cells can't find useQuery. - include: [ - 'rehackt', - 'react-hook-form', - '@cedarjs/forms', - 'humanize-string', - // @cedarjs/web imports Apollo via explicit .cjs sub-paths. The - // resolve.alias above redirects these in Vite's transform pipeline, - // but esbuild (dep scanner) doesn't use resolve.alias. So Vite - // discovers them on first load and triggers a mid-session reload, - // which tears down the StorybookProvider's GraphQLHooksProvider - // context. Pre-bundling them eagerly prevents that reload. - '@apollo/client/cache/cache.cjs', - '@apollo/client/core/core.cjs', - '@apollo/client/link/context/context.cjs', - '@apollo/client/link/core/core.cjs', - '@apollo/client/link/persisted-queries/persisted-queries.cjs', - '@apollo/client/react/hooks/hooks.cjs', - '@apollo/client/react/react.cjs', - '@apollo/client/utilities/utilities.cjs', - 'graphql/language/printer.js', - 'graphql-tag', - '@cedarjs/web', - '@cedarjs/web/apollo', - '@cedarjs/testing/web/MockRouter.js', - '@cedarjs/testing/auth', - '@cedarjs/auth-dbauth-web', - 'vite-plugin-node-polyfills/shims/buffer', - 'vite-plugin-node-polyfills/shims/global', - 'vite-plugin-node-polyfills/shims/process', - ], + include: ['rehackt'], + esbuildOptions: { + plugins: [ + { + // Cedar's Cell transform (a Vite plugin) injects `export default + // createCell(...)` at transform time, which doesn't run during + // esbuild's dep scan. Without a default export, esbuild crashes + // the scan with "No matching export ... for import 'default'", + // causing Vite to skip pre-bundling entirely. That in turn means + // all deps are discovered on first page load, triggering + // mid-session reloads that tear down Storybook's + // GraphQLHooksProvider context and break nested-Cell stories. + // + // This plugin intercepts Cell files during the dep scan and + // returns a stub that satisfies the default import, allowing the + // scan to complete. The real transform still runs later via + // Vite's normal pipeline when the browser requests the file. + name: 'cedar-cell-stub', + setup(build: PluginBuild) { + build.onLoad({ filter: /Cell\.[jt]sx?$/ }, () => ({ + contents: 'export default {}', + loader: 'js', + })) + }, + }, + ], + }, }, }, ) From 34067e084c1d324070c42914599fdde82f9fee86 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 18:57:13 -0700 Subject: [PATCH 38/48] chore(ci): Fix flaky cca tests --- packages/create-cedar-app/tests/e2e_prompts.sh | 7 +++++-- packages/create-cedar-app/tests/e2e_prompts_git.sh | 7 +++++-- packages/create-cedar-app/tests/e2e_prompts_m.sh | 7 +++++-- .../create-cedar-app/tests/e2e_prompts_node_greater.sh | 7 +++++-- packages/create-cedar-app/tests/e2e_prompts_overwrite.sh | 7 +++++-- packages/create-cedar-app/tests/e2e_prompts_ts.sh | 7 +++++-- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/create-cedar-app/tests/e2e_prompts.sh b/packages/create-cedar-app/tests/e2e_prompts.sh index 85a0be4e3b..493ebcaa59 100755 --- a/packages/create-cedar-app/tests/e2e_prompts.sh +++ b/packages/create-cedar-app/tests/e2e_prompts.sh @@ -38,12 +38,15 @@ expect eof catch wait result set exitStatus [lindex $result 3] +# Git can still be finishing work in .git/objects right after the generator exits, +# so wait briefly and treat cleanup as best-effort to avoid flaky local and CI runs. +after 500 +catch {exec rm -rf $projectDirectory} + if {$exitStatus == 0} { puts "Success" - exec rm -rf $projectDirectory exit 0 } else { puts "Error: The process failed with exit status $exitStatus" - exec rm -rf $projectDirectory exit 1 } diff --git a/packages/create-cedar-app/tests/e2e_prompts_git.sh b/packages/create-cedar-app/tests/e2e_prompts_git.sh index 6594343fff..02a5456b0e 100755 --- a/packages/create-cedar-app/tests/e2e_prompts_git.sh +++ b/packages/create-cedar-app/tests/e2e_prompts_git.sh @@ -32,12 +32,15 @@ expect eof catch wait result set exitStatus [lindex $result 3] +# Git can still be finishing work in .git/objects right after the generator exits, +# so wait briefly and treat cleanup as best-effort to avoid flaky local and CI runs. +after 500 +catch {exec rm -rf $projectDirectory} + if {$exitStatus == 0} { puts "Success" - exec rm -rf $projectDirectory exit 0 } else { puts "Error: The process failed with exit status $exitStatus" - exec rm -rf $projectDirectory exit 1 } diff --git a/packages/create-cedar-app/tests/e2e_prompts_m.sh b/packages/create-cedar-app/tests/e2e_prompts_m.sh index f45d40f81d..f182e48ce7 100755 --- a/packages/create-cedar-app/tests/e2e_prompts_m.sh +++ b/packages/create-cedar-app/tests/e2e_prompts_m.sh @@ -33,12 +33,15 @@ expect eof catch wait result set exitStatus [lindex $result 3] +# Git can still be finishing work in .git/objects right after the generator exits, +# so wait briefly and treat cleanup as best-effort to avoid flaky local and CI runs. +after 500 +catch {exec rm -rf $projectDirectory} + if {$exitStatus == 0} { puts "Success" - exec rm -rf $projectDirectory exit 0 } else { puts "Error: The process failed with exit status $exitStatus" - exec rm -rf $projectDirectory exit 1 } diff --git a/packages/create-cedar-app/tests/e2e_prompts_node_greater.sh b/packages/create-cedar-app/tests/e2e_prompts_node_greater.sh index 0d2ef1c4d6..4e95dea356 100755 --- a/packages/create-cedar-app/tests/e2e_prompts_node_greater.sh +++ b/packages/create-cedar-app/tests/e2e_prompts_node_greater.sh @@ -42,12 +42,15 @@ expect eof catch wait result set exitStatus [lindex $result 3] +# Git can still be finishing work in .git/objects right after the generator exits, +# so wait briefly and treat cleanup as best-effort to avoid flaky local and CI runs. +after 500 +catch {exec rm -rf $projectDirectory} + if {$exitStatus == 0} { puts "Success" - exec rm -rf $projectDirectory exit 0 } else { puts "Error: The process failed with exit status $exitStatus" - exec rm -rf $projectDirectory exit 1 } diff --git a/packages/create-cedar-app/tests/e2e_prompts_overwrite.sh b/packages/create-cedar-app/tests/e2e_prompts_overwrite.sh index 484d1bfa0e..423b487205 100755 --- a/packages/create-cedar-app/tests/e2e_prompts_overwrite.sh +++ b/packages/create-cedar-app/tests/e2e_prompts_overwrite.sh @@ -43,12 +43,15 @@ expect eof catch wait result set exitStatus [lindex $result 3] +# Git can still be finishing work in .git/objects right after the generator exits, +# so wait briefly and treat cleanup as best-effort to avoid flaky local and CI runs. +after 500 +catch {exec rm -rf $projectDirectory} + if {$exitStatus == 1} { puts "Success" - exec rm -rf $projectDirectory exit 0 } else { puts "Error: The process failed with exit status $exitStatus" - exec rm -rf $projectDirectory exit 1 } diff --git a/packages/create-cedar-app/tests/e2e_prompts_ts.sh b/packages/create-cedar-app/tests/e2e_prompts_ts.sh index 703d29d882..c99bed4b46 100755 --- a/packages/create-cedar-app/tests/e2e_prompts_ts.sh +++ b/packages/create-cedar-app/tests/e2e_prompts_ts.sh @@ -33,12 +33,15 @@ expect eof catch wait result set exitStatus [lindex $result 3] +# Git can still be finishing work in .git/objects right after the generator exits, +# so wait briefly and treat cleanup as best-effort to avoid flaky local and CI runs. +after 500 +catch {exec rm -rf $projectDirectory} + if {$exitStatus == 0} { puts "Success" - exec rm -rf $projectDirectory exit 0 } else { puts "Error: The process failed with exit status $exitStatus" - exec rm -rf $projectDirectory exit 1 } From b481ffc4db0431bb51fcf9304e3418c659ba9c10 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 19:31:47 -0700 Subject: [PATCH 39/48] Update esbuild plugin --- packages/storybook/src/preset.ts | 45 ++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index cfbc625fc8..a692d68334 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -116,16 +116,45 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // mid-session reloads that tear down Storybook's // GraphQLHooksProvider context and break nested-Cell stories. // - // This plugin intercepts Cell files during the dep scan and - // returns a stub that satisfies the default import, allowing the - // scan to complete. The real transform still runs later via - // Vite's normal pipeline when the browser requests the file. + // This plugin intercepts Cell files during the dep scan, reads + // the real file from disk, and appends a default export stub to + // it. This satisfies the default import requirement while + // keeping the real code available so esbuild can discover the + // file's actual imports and re-exports. The real Cell transform + // still runs later via Vite's normal pipeline when the browser + // requests the file. name: 'cedar-cell-stub', setup(build: PluginBuild) { - build.onLoad({ filter: /Cell\.[jt]sx?$/ }, () => ({ - contents: 'export default {}', - loader: 'js', - })) + const resolveDirByPath = new Map() + + build.onResolve({ filter: /Cell\.[jt]sx?$/ }, (args) => { + resolveDirByPath.set(args.path, args.resolveDir) + return { + path: args.path, + namespace: 'cedar-cell-stub', + } + }) + + build.onLoad( + { filter: /.*/, namespace: 'cedar-cell-stub' }, + async (args) => { + const fs = await import('node:fs') + + const resolveDir = resolveDirByPath.get(args.path) + const result = await build.resolve(args.path, { + resolveDir, + }) + const contents = await fs.promises.readFile( + result.path, + 'utf-8', + ) + + return { + contents: `${contents}\nexport default {}`, + loader: 'js', + } + }, + ) }, }, ], From 93ffb949d5ed186a9b4bd514181ba57b18ae275b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 19:40:26 -0700 Subject: [PATCH 40/48] more cca flakyness fixes --- packages/create-cedar-app/tests/e2e.test.ts | 34 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/create-cedar-app/tests/e2e.test.ts b/packages/create-cedar-app/tests/e2e.test.ts index 479c0eb12b..c28ee8ac48 100644 --- a/packages/create-cedar-app/tests/e2e.test.ts +++ b/packages/create-cedar-app/tests/e2e.test.ts @@ -28,8 +28,32 @@ for (const dir of ['esm-js', 'esm-ts', 'js', 'ts']) { cd(projectPath) +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +async function removeGeneratedApp() { + // Git can still be finishing work in .git/objects right after the generator + // exits, so retry cleanup a few times to avoid flaky local and CI runs. + for (let attempt = 1; attempt <= 3; attempt++) { + try { + await fs.rm('./cedar-app', { recursive: true, force: true }) + return + } catch (error) { + if (attempt === 3) { + console.warn( + 'Warning: failed to remove ./cedar-app during test cleanup', + ) + console.warn(error) + + return + } + + await sleep(500) + } + } +} + afterEach(async () => { - await fs.rm('./cedar-app', { recursive: true, force: true }) + await removeGeneratedApp() }) describe('create-cedar-app', () => { @@ -104,8 +128,12 @@ describe('create-cedar-app', () => { await $`yarn create-cedar-app --unknown-options`.timeout(2500) // Fail the test if the function didn't throw. expect(true).toEqual(false) - } catch (p) { - expect(p.exitCode).toEqual(1) + } catch (error) { + if (typeof error === 'object' && error !== null && 'exitCode' in error) { + expect(error.exitCode).toEqual(1) + } else { + throw error + } } }) }) From 8067e7a68f7bca1fd6f9e3c8f52555475b4eb1e2 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 20:02:15 -0700 Subject: [PATCH 41/48] update plugin --- packages/storybook/src/preset.ts | 43 +++++++++++++------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index a692d68334..7bd85d7e51 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' @@ -125,36 +126,26 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // requests the file. name: 'cedar-cell-stub', setup(build: PluginBuild) { - const resolveDirByPath = new Map() - - build.onResolve({ filter: /Cell\.[jt]sx?$/ }, (args) => { - resolveDirByPath.set(args.path, args.resolveDir) - return { - path: args.path, - namespace: 'cedar-cell-stub', + build.onLoad({ filter: /Cell\.[jt]sx?$/ }, async (args) => { + if (args.path.includes('node_modules')) { + return } - }) - build.onLoad( - { filter: /.*/, namespace: 'cedar-cell-stub' }, - async (args) => { - const fs = await import('node:fs') + const contents = await fs.promises.readFile( + args.path, + 'utf-8', + ) - const resolveDir = resolveDirByPath.get(args.path) - const result = await build.resolve(args.path, { - resolveDir, - }) - const contents = await fs.promises.readFile( - result.path, - 'utf-8', - ) + // Skip stubbing if the file already exports a default value + if (contents.includes('export default')) { + return + } - return { - contents: `${contents}\nexport default {}`, - loader: 'js', - } - }, - ) + return { + contents: `${contents}\nexport default {}`, + loader: 'js', + } + }) }, }, ], From b732b70b108841fc3cb5319ca5cdb591ddf03dfb Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 20:20:45 -0700 Subject: [PATCH 42/48] use correct loader in plugin --- packages/storybook/src/preset.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 7bd85d7e51..15710e805e 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -141,9 +141,16 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { return } + // The `as` cast is safe because of the filter regexp above + const ext = path.extname(args.path).slice(1) as + | 'js' + | 'jsx' + | 'ts' + | 'tsx' + return { contents: `${contents}\nexport default {}`, - loader: 'js', + loader: ext, } }) }, From 97ef70bf2c1444e7443c38917d32fe07e049d11b Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 20:50:24 -0700 Subject: [PATCH 43/48] fix(storybook): pre-bundle Apollo .cjs sub-paths to prevent mid-session reload The cedar-cell-stub esbuild plugin fixes the dep scan crash, but Vite still triggers a mid-session reload when it first serves a Storybook page and discovers @cedarjs/web's Apollo .cjs imports. resolve.alias redirects those imports in the transform pipeline but not in the dep optimizer (which uses esbuild directly). So they get discovered on first page load and cause a reload that tears down the StorybookProvider's GraphQLHooksProvider context, breaking nested-Cell stories. These are structural imports of @cedarjs/web itself, so the list is valid for all Cedar apps, not just the CI test project. --- packages/storybook/src/preset.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 15710e805e..c678a22b22 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -104,7 +104,27 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // Force pre-bundling of CJS-only packages that are only reachable through // storybook-framework-cedarjs (excluded above). Without this, Vite serves // them via ?import interop, which can't detect named exports in CJS files. - include: ['rehackt'], + // + // Also pre-bundle the Apollo .cjs sub-paths that @cedarjs/web imports. + // resolve.alias redirects them in the transform pipeline, but esbuild + // (dep optimizer) ignores resolve.alias. Without this, Vite discovers + // them on the first page load and triggers a mid-session reload, which + // tears down the StorybookProvider's GraphQLHooksProvider context and + // breaks nested-Cell stories. These are structural deps of @cedarjs/web + // itself, so this list is valid for all Cedar apps, not just the test + // project. + include: [ + 'rehackt', + '@apollo/client/cache/cache.cjs', + '@apollo/client/core/core.cjs', + '@apollo/client/link/context/context.cjs', + '@apollo/client/link/core/core.cjs', + '@apollo/client/link/persisted-queries/persisted-queries.cjs', + '@apollo/client/react/hooks/hooks.cjs', + '@apollo/client/react/react.cjs', + '@apollo/client/utilities/utilities.cjs', + 'graphql/language/printer.js', + ], esbuildOptions: { plugins: [ { From 429acee62be80f87af41f2fae55af0b4855bed20 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 21:18:44 -0700 Subject: [PATCH 44/48] fix(storybook): pre-bundle polyfill shims and testing deps to prevent mid-session reloads --- packages/storybook/src/preset.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index c678a22b22..408f60346e 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -124,6 +124,21 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { '@apollo/client/react/react.cjs', '@apollo/client/utilities/utilities.cjs', 'graphql/language/printer.js', + // Pre-bundle node-polyfill shims so Vite doesn't discover them + // mid-session when storybook-framework-cedarjs (which uses + // nodePolyfills()) is first rendered. Without this, each shim + // triggers a separate optimized-deps reload that tears down + // GraphQLHooksProvider and breaks nested-Cell stories. + 'vite-plugin-node-polyfills/shims/buffer', + 'vite-plugin-node-polyfills/shims/global', + 'vite-plugin-node-polyfills/shims/process', + // Pre-bundle @cedarjs/testing and graphql-tag so they don't cause + // mid-session reloads when StorybookProvider loads mock files. + '@cedarjs/testing/web', + // Sub-path explicitly imported by @cedarjs/router mock infrastructure + '@cedarjs/testing/web/MockRouter.js', + '@cedarjs/testing/auth', + 'graphql-tag', ], esbuildOptions: { plugins: [ From dc4d18bae6f5726caab20c4f5331c28cf198f510 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 21:55:50 -0700 Subject: [PATCH 45/48] fix(storybook): pre-bundle @cedarjs/web to prevent duplicate GraphQLHooksProvider instances --- packages/storybook/src/preset.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 408f60346e..bbfeee52b9 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -139,6 +139,15 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { '@cedarjs/testing/web/MockRouter.js', '@cedarjs/testing/auth', 'graphql-tag', + // Pre-bundle @cedarjs/web so there is only one instance of + // GraphQLHooksProvider. Without this, the pre-bundled dep graph and + // the Vite transform pipeline can each produce their own copy of + // the module, so RedwoodApolloProvider sets context in one copy + // while NamedCell reads from the other, causing the + // "You must register a useQuery hook via the GraphQLHooksProvider" + // error when a Cell is nested inside another story. + '@cedarjs/web', + '@cedarjs/web/apollo', ], esbuildOptions: { plugins: [ From cb77eeaf5fea627db407bd11c3df6c789bfc4494 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 22:01:18 -0700 Subject: [PATCH 46/48] fix(storybook): stub Cell files completely in dep scan to prevent GraphQLHooksContext split The previous cedar-cell-stub plugin read the real Cell file and appended 'export default {}'. This caused esbuild to follow the Cell's imports (e.g. createCell from @cedarjs/web), pulling GraphQLHooksProvider into a separate pre-bundled chunk from the one created for @cedarjs/web itself. Two distinct GraphQLHooksContext instances resulted, so the context set by RedwoodApolloProvider was invisible to the Cell's useQuery hook. Return only 'export default {}' without reading the file contents. esbuild treats Cell files as leaf nodes and stops following their imports, keeping @cedarjs/web's module graph in a single pre-bundled chunk. --- packages/storybook/src/preset.ts | 53 ++++++++++++-------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index bbfeee52b9..565bbde34a 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' @@ -156,45 +155,33 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // createCell(...)` at transform time, which doesn't run during // esbuild's dep scan. Without a default export, esbuild crashes // the scan with "No matching export ... for import 'default'", - // causing Vite to skip pre-bundling entirely. That in turn means - // all deps are discovered on first page load, triggering - // mid-session reloads that tear down Storybook's - // GraphQLHooksProvider context and break nested-Cell stories. + // causing Vite to skip pre-bundling entirely. // - // This plugin intercepts Cell files during the dep scan, reads - // the real file from disk, and appends a default export stub to - // it. This satisfies the default import requirement while - // keeping the real code available so esbuild can discover the - // file's actual imports and re-exports. The real Cell transform - // still runs later via Vite's normal pipeline when the browser - // requests the file. + // Additionally, if we include the real Cell file contents in the + // stub, esbuild follows the Cell's imports (e.g. createCell from + // @cedarjs/web) and pulls GraphQLHooksProvider into its own + // pre-bundled chunk separate from the @cedarjs/web chunk. That + // produces two distinct GraphQLHooksContext instances, so the + // context set by RedwoodApolloProvider is invisible to the Cell's + // useQuery, causing the "You must register a useQuery hook" + // error. + // + // By returning ONLY `export default {}` (ignoring the real file + // contents), esbuild treats the Cell as a leaf node and does not + // follow its imports. This keeps @cedarjs/web's module graph in a + // single pre-bundled chunk. The real Cell transform runs later via + // Vite's normal transform pipeline when the browser requests the + // file. name: 'cedar-cell-stub', setup(build: PluginBuild) { - build.onLoad({ filter: /Cell\.[jt]sx?$/ }, async (args) => { + build.onLoad({ filter: /Cell\.[jt]sx?$/ }, (args) => { if (args.path.includes('node_modules')) { - return + return undefined } - const contents = await fs.promises.readFile( - args.path, - 'utf-8', - ) - - // Skip stubbing if the file already exports a default value - if (contents.includes('export default')) { - return - } - - // The `as` cast is safe because of the filter regexp above - const ext = path.extname(args.path).slice(1) as - | 'js' - | 'jsx' - | 'ts' - | 'tsx' - return { - contents: `${contents}\nexport default {}`, - loader: ext, + contents: 'export default {}', + loader: 'js', } }) }, From 89f6ce0e8a2b74586084ae3c03b9ebb1ca416382 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 22:06:02 -0700 Subject: [PATCH 47/48] fix(storybook): synthesize named-export stubs in cell-stub dep scan plugin The plain 'export default {}' stub broke named imports like 'import { Loading, Success } from './BlogPostCell''. The plugin now reads the source, extracts all exported names via regex, and synthesizes 'export const Foo = undefined' stubs for each one plus the required default export. esbuild still sees only the synthesized module (no real imports to follow), so the GraphQLHooksContext stays in a single chunk. --- packages/storybook/src/preset.ts | 59 +++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/storybook/src/preset.ts b/packages/storybook/src/preset.ts index 565bbde34a..93ec58cf53 100644 --- a/packages/storybook/src/preset.ts +++ b/packages/storybook/src/preset.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' @@ -157,30 +158,56 @@ export const viteFinal: StorybookConfig['viteFinal'] = async (config) => { // the scan with "No matching export ... for import 'default'", // causing Vite to skip pre-bundling entirely. // - // Additionally, if we include the real Cell file contents in the - // stub, esbuild follows the Cell's imports (e.g. createCell from - // @cedarjs/web) and pulls GraphQLHooksProvider into its own - // pre-bundled chunk separate from the @cedarjs/web chunk. That - // produces two distinct GraphQLHooksContext instances, so the - // context set by RedwoodApolloProvider is invisible to the Cell's - // useQuery, causing the "You must register a useQuery hook" - // error. + // We must NOT return the real Cell file contents here. If esbuild + // follows the Cell's imports (e.g. createCell from @cedarjs/web), + // it pulls GraphQLHooksProvider into its own pre-bundled chunk + // separately from the @cedarjs/web chunk. That produces two + // distinct GraphQLHooksContext instances, so the context set by + // RedwoodApolloProvider is invisible to the Cell's useQuery, + // causing "You must register a useQuery hook via the + // GraphQLHooksProvider". // - // By returning ONLY `export default {}` (ignoring the real file - // contents), esbuild treats the Cell as a leaf node and does not - // follow its imports. This keeps @cedarjs/web's module graph in a - // single pre-bundled chunk. The real Cell transform runs later via - // Vite's normal transform pipeline when the browser requests the - // file. + // Instead we synthesize a stub module: we scan the source for + // exported names and re-export them as empty stubs, plus add a + // default export. esbuild treats the Cell as a leaf (no real + // imports to follow), keeping @cedarjs/web in a single chunk. + // The real Cell transform runs later via Vite's normal pipeline. name: 'cedar-cell-stub', setup(build: PluginBuild) { - build.onLoad({ filter: /Cell\.[jt]sx?$/ }, (args) => { + build.onLoad({ filter: /Cell\.[jt]sx?$/ }, async (args) => { if (args.path.includes('node_modules')) { return undefined } + const src = await fs.promises.readFile(args.path, 'utf-8') + + // Extract every explicitly exported name so that + // `import { Loading, Success } from './MyCell'` resolves + // to a stub value rather than undefined. + const exportedNames = new Set() + // Covers: export const/let/var/function/class Foo + for (const m of src.matchAll( + /^export\s+(?:const|let|var|function|class)\s+(\w+)/gm, + )) { + exportedNames.add(m[1]) + } + // Covers: export { Foo, Bar } and export { Foo as Bar } + for (const m of src.matchAll(/^export\s*\{([^}]+)\}/gm)) { + for (const part of m[1].split(',')) { + const name = (part.split(/\bas\b/).pop() ?? '').trim() + if (name) { + exportedNames.add(name) + } + } + } + + const namedStubs = [...exportedNames] + .filter((n) => n !== 'default') + .map((n) => `export const ${n} = undefined`) + .join('\n') + return { - contents: 'export default {}', + contents: `${namedStubs}\nexport default {}`, loader: 'js', } }) From e492f1f38b3d3c9ebe754e4d956365c985223a1e Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 21 May 2026 22:41:06 -0700 Subject: [PATCH 48/48] Explain +4 offset --- packages/prerender/src/runPrerender.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/prerender/src/runPrerender.tsx b/packages/prerender/src/runPrerender.tsx index 8f09d9e186..c90327fb8e 100644 --- a/packages/prerender/src/runPrerender.tsx +++ b/packages/prerender/src/runPrerender.tsx @@ -197,6 +197,9 @@ function insertChunkLoadingScript( const chunkPaths: string[] = [] if (route?.filePath) { + // + 4 skips the 'web/' prefix (4 chars). The resulting pagePath + // includes 'src/pages/...' which matches the build manifest keys which are + // relative to Vite's root, which is web/ const pagesIndex = route.filePath.indexOf(path.join('web', 'src', 'pages')) + 4 const pagePath = ensurePosixPath(route.filePath.slice(pagesIndex))