Skip to content

feat(vite)!: move index.html from web/src to web#1799

Merged
Tobbe merged 52 commits into
cedarjs:mainfrom
lisa-assistant:lisa/move-index-html-to-web-root
May 22, 2026
Merged

feat(vite)!: move index.html from web/src to web#1799
Tobbe merged 52 commits into
cedarjs:mainfrom
lisa-assistant:lisa/move-index-html-to-web-root

Conversation

@lisa-assistant
Copy link
Copy Markdown
Contributor

@lisa-assistant lisa-assistant commented May 17, 2026

Summary

Standard Vite convention is index.html at the project root (web/), not inside the source directory (web/src/). Cedar inherited web/src/index.html from Redwood, which forced getMergedConfig to set root: web/src explicitly so Vite could find it.

Moving index.html to web/ lets Vite find it by default — configFile lives in web/, so root defaults to web/. This removes the need for the explicit root override entirely. This also lets us use default vite configs in other places, like Storybook, prerender, etc. This simplifies the entire Vite config story.

Changes:

  • Move index.html from web/src/web/ in all fixtures, templates, and test fixtures
  • 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 — Vite defaults are now correct
  • packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts: compute entry script path relative to web/ instead of web/src/ (injected src changes from /entry.client.tsx/src/entry.client.tsx)
  • Update error messages in entry.client files and docs referencing the old path

Note for users with customizations in web/src/index.html: Moving the file to web/index.html is a straightforward rename — no content changes needed. Any relative paths inside the file (e.g. favicon, fonts) that referenced files relative to web/src/ may need updating to be relative to web/ instead, but the default Cedar index.html has no such relative paths.

Test plan

  • Existing unit tests pass (packages/project-config, packages/create-cedar-app)
  • Dev server serves the app correctly (Vite picks up web/index.html)
  • Production build works (Rollup entry injection finds web/index.html)
  • SSR/streaming SSR builds unaffected (they don't use index.html as entry)

🤖 Generated with Claude Code

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>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 17, 2026

Deploy Preview for cedarjs ready!

Name Link
🔨 Latest commit 223e4b4
🔍 Latest deploy log https://app.netlify.com/projects/cedarjs/deploys/6a0ff699d7d2b20008281dff
😎 Deploy Preview https://deploy-preview-1799--cedarjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added this to the chore milestone May 17, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 17, 2026

Greptile Summary

This PR moves index.html from web/src/ to web/, aligning Cedar with standard Vite convention and eliminating the explicit root: web/src override throughout the codebase. All downstream consumers — prerender manifest lookups, the FeServer entry key, the entry-injection plugin, and Storybook — are updated to match the new root.

  • getMergedConfig: Vite root is now web/; getRollupInput short-circuits when the user has already supplied input, preventing Cedar from overriding Storybook's rollup config.
  • vite-plugin-cedar-entry-injection: Entry path is now relative to web/, a new resolveId hook maps the absolute-URL form /src/entry.client.tsx to the real file so Vite's dep optimizer can find it, and transformIndexHtml guards on ctx.filename to skip non-Cedar HTML files. normalizePath is correctly applied to fix Windows backslash separators.
  • Prerender / FeServer: Build-manifest keys updated to src/pages/... and src/entry.client.{tsx,jsx} to reflect paths relative to the new web/ root.

Confidence Score: 5/5

Safe 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

Filename Overview
packages/vite/src/lib/getMergedConfig.ts Vite root changed from web/src to web/; getRollupInput now respects user-set input to avoid overriding Storybook's config
packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts Entry path now relative to web/; adds resolveId hook for Vite dep optimizer; transformIndexHtml guards on ctx.filename to skip non-Cedar HTML files; normalizePath applied to fix Windows separators
packages/storybook/src/preset.ts Removes root: web/src override; filters out cedar-entry-injection plugin; adds cedar-cell-stub esbuild plugin for dep scanning; alias keys correctly use ~CEDAR names
packages/prerender/src/runPrerender.tsx Manifest key offset changed from +8 to +4 so page paths are relative to web/, matching the new build manifest key format
packages/prerender/src/runPrerenderEsm.tsx Same offset fix as runPrerender.tsx — +8 to +4 for page manifest key lookup
packages/vite/src/runFeServer.ts Client entry manifest key updated from entry.client.tsx to src/entry.client.tsx to match new Vite root
packages/project-config/src/paths.ts web.html path updated from web/src/index.html to web/index.html
packages/vite/src/buildUDApiServer.ts Removes explicit root override that was a workaround for the old web/src root
packages/create-cedar-app/tests/templates.test.ts Template snapshot updated: /web/index.html replaces /web/src/index.html in all four templates

Reviews (41): Last reviewed commit: "Merge branch 'main' into lisa/move-index..." | Re-trigger Greptile

Comment thread packages/vite/src/plugins/vite-plugin-cedar-entry-injection.ts Outdated
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 17, 2026

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 223e4b4

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

@Tobbe Tobbe changed the title chore(vite): move index.html from web/src to web feat(vite)!: move index.html from web/src to web May 17, 2026
lisa-assistant and others added 9 commits May 17, 2026 21:05
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.
@Tobbe Tobbe modified the milestones: chore, next-release-major May 18, 2026
Tobbe and others added 12 commits May 18, 2026 08:36
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.
Tobbe and others added 15 commits May 21, 2026 09:52
…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.
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.
…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.
Comment thread packages/storybook/src/preset.ts
Tobbe added 9 commits May 21, 2026 20:20
…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.
…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.
@Tobbe Tobbe merged commit c1db5e1 into cedarjs:main May 22, 2026
72 of 75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants