feat(vite)!: move index.html from web/src to web#1799
Conversation
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 <noreply@anthropic.com>
✅ Deploy Preview for cedarjs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Greptile SummaryThis PR moves
Confidence Score: 5/5Safe to merge — changes are mechanical path adjustments backed by thorough updates across all fixtures, templates, and consumers. Every consumer of the old web/src root has been updated consistently. RSC/SSR fixtures that never had an index.html are correctly left untouched. The overview doc contains no references to the old web/src/index.html path and remains factually accurate. No files require special attention. Important Files Changed
Reviews (41): Last reviewed commit: "Merge branch 'main' into lisa/move-index..." | Re-trigger Greptile |
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx run-many -t build:pack --exclude create-ceda... |
✅ Succeeded | 1s | View ↗ |
nx run-many -t build |
✅ Succeeded | 3m 13s | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-05-22 06:54:36 UTC
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.
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).
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.
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 7e6eb85.
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.
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.
…nfig 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 <noreply@anthropic.com>
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.
…ders 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 <noreply@anthropic.com>
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 <tobbe@tlundberg.com>
…ling 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.
…eFinal 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.
…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.
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.
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.
Source files (MockProviders, StorybookProvider, ambient.d.ts) already use ~__CEDAR__ aliases. Remove the duplicate ~__REDWOOD__ entries from preset.ts.
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.
This reverts commit a2039fb.
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.
… split 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.
… context split" This reverts commit b4ea2e1.
…id-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.
…on 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.
… mid-session reloads
…ooksProvider instances
…phQLHooksContext 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.
…lugin
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.

Summary
Standard Vite convention is
index.htmlat the project root (web/), not inside the source directory (web/src/). Cedar inheritedweb/src/index.htmlfrom Redwood, which forcedgetMergedConfigto setroot: web/srcexplicitly so Vite could find it.Moving
index.htmltoweb/lets Vite find it by default —configFilelives inweb/, sorootdefaults toweb/. This removes the need for the explicitrootoverride entirely. This also lets us use default vite configs in other places, like Storybook, prerender, etc. This simplifies the entire Vite config story.Changes:
index.htmlfromweb/src/→web/in all fixtures, templates, and test fixturespackages/project-config/src/paths.ts:web.htmlnow points toweb/index.htmlpackages/vite/src/lib/getMergedConfig.ts: removeroot: web/srcoverride — Vite defaults are now correctpackages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts: compute entry script path relative toweb/instead ofweb/src/(injected src changes from/entry.client.tsx→/src/entry.client.tsx)entry.clientfiles and docs referencing the old pathNote for users with customizations in
web/src/index.html: Moving the file toweb/index.htmlis a straightforward rename — no content changes needed. Any relative paths inside the file (e.g. favicon, fonts) that referenced files relative toweb/src/may need updating to be relative toweb/instead, but the default Cedarindex.htmlhas no such relative paths.Test plan
packages/project-config,packages/create-cedar-app)web/index.html)web/index.html)index.htmlas entry)🤖 Generated with Claude Code