Skip to content

Self-host dev: serve the SPA shell for /login and /setup - #1232

Merged
RhysSullivan merged 1 commit into
mainfrom
selfhost-login-spa-shell
Jul 1, 2026
Merged

Self-host dev: serve the SPA shell for /login and /setup#1232
RhysSullivan merged 1 commit into
mainfrom
selfhost-login-spa-shell

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Problem

In the self-host Vite dev server, a browser navigating to /login (or /setup) gets the raw login.tsx module source as text/javascript, not the app. Vite treats the extensionless request as a JS request and its resolver maps /login to web/login.tsx, which sits in the Vite root and shadows the route.

It only bites the MCP OAuth flow, because that is the one path that does a real server-side document navigation to /login (Better Auth's oidcConfig.loginPage 302). Normal in-app sign-in renders the login page client-side and never GETs /login.

Fix

In the dev API middleware (which already runs before Vite's transform middleware), rewrite real page navigations to the index so Vite's html fallback serves index.html; the SPA then reads window.location and renders the right route. Gated on Sec-Fetch-Dest: document / Accept: text/html and no file extension, so modules, assets, and /@vite internals pass through untouched.

Dev-only: the production static server already serves index.html for unknown routes, with login/setup bundled into the SPA.

The Vite dev server resolves an extensionless request like /login to a
colliding web-root module (web/login.tsx shadows /login, web/setup.tsx
shadows /setup) and serves it as text/javascript, so a browser navigating
to those routes (e.g. Better Auth's MCP-OAuth loginPage 302) renders raw JS
instead of the app. In the dev API middleware (which runs before Vite's
transform middleware), rewrite genuine document navigations to the index so
the html fallback serves index.html and the SPA renders the route. Gated on
Sec-Fetch-Dest: document / Accept: text/html and no file extension, so
modules, assets, and /@Vite internals pass through untouched. Dev-only: the
production static server already serves index.html for unknown routes.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud bd5b8de Jun 30 2026, 11:44 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing bd5b8de Commit Preview URL

Branch Preview URL
Jun 30 2026, 11:43 PM

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a dev-only middleware guard in apps/host-selfhost/vite.config.ts to intercept real browser document navigations (e.g., Better Auth's loginPage 302 to /login) before Vite's transform middleware can serve the colliding web/login.tsx / web/setup.tsx modules as text/javascript. Matched requests are rewritten to req.url = "/" so Vite's HTML fallback serves index.html; the SPA then routes via window.location.

  • The detection is gated on Sec-Fetch-Dest: document (with an Accept: text/html fallback for contexts that don't send the header), an absence of a file extension on the pathname, and exclusion of /@-prefixed Vite internals, so module/asset/HMR requests are unaffected.
  • The change is entirely inside the apply: "serve" plugin, so it has no effect on production builds.

Confidence Score: 5/5

Safe to merge; the change is dev-server-only and has no effect on production builds or any existing request path other than extensionless document navigations that were already broken.

The fix is contained to a single apply: serve plugin, touches no production code path, and the guard conditions (method, Sec-Fetch-Dest / Accept, no file extension, not /@) are tight enough that module, asset, and HMR requests are unaffected. The rewrite of req.url to / is correct: Vite sees the index while the browser retains the original URL for the SPA router to consume via window.location.

No files require special attention; the single changed file is the Vite config for the self-host dev app.

Important Files Changed

Filename Overview
apps/host-selfhost/vite.config.ts Adds a 25-line document-navigation rewrite block inside the existing dev API middleware; logic and guards are well-scoped; one subtle edge case with query-string-bearing login redirects is a non-issue since the browser URL is unchanged.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Browser
    participant DevMiddleware as Dev API Middleware
    participant ViteTransform as Vite Transform Middleware
    participant ViteHTML as Vite HTML Fallback

    Note over Browser,ViteHTML: Before fix — MCP OAuth 302 to /login
    Browser->>DevMiddleware: GET /login (Sec-Fetch-Dest: document)
    DevMiddleware->>DevMiddleware: "handled = false (not /api, /mcp, etc.)"
    DevMiddleware->>ViteTransform: "next() with req.url = /login"
    ViteTransform->>Browser: 200 text/javascript (web/login.tsx source)

    Note over Browser,ViteHTML: After fix — MCP OAuth 302 to /login
    Browser->>DevMiddleware: GET /login (Sec-Fetch-Dest: document)
    DevMiddleware->>DevMiddleware: "handled = false"
    DevMiddleware->>DevMiddleware: "isGet=true, wantsDocument=true, hasExtension=false"
    DevMiddleware->>DevMiddleware: "req.url = / (rewrite)"
    DevMiddleware->>ViteTransform: "next() with req.url = /"
    ViteTransform->>ViteHTML: no match, fallback
    ViteHTML->>Browser: 200 text/html (index.html)
    Note over Browser: window.location = /login, SPA renders login route
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Browser
    participant DevMiddleware as Dev API Middleware
    participant ViteTransform as Vite Transform Middleware
    participant ViteHTML as Vite HTML Fallback

    Note over Browser,ViteHTML: Before fix — MCP OAuth 302 to /login
    Browser->>DevMiddleware: GET /login (Sec-Fetch-Dest: document)
    DevMiddleware->>DevMiddleware: "handled = false (not /api, /mcp, etc.)"
    DevMiddleware->>ViteTransform: "next() with req.url = /login"
    ViteTransform->>Browser: 200 text/javascript (web/login.tsx source)

    Note over Browser,ViteHTML: After fix — MCP OAuth 302 to /login
    Browser->>DevMiddleware: GET /login (Sec-Fetch-Dest: document)
    DevMiddleware->>DevMiddleware: "handled = false"
    DevMiddleware->>DevMiddleware: "isGet=true, wantsDocument=true, hasExtension=false"
    DevMiddleware->>DevMiddleware: "req.url = / (rewrite)"
    DevMiddleware->>ViteTransform: "next() with req.url = /"
    ViteTransform->>ViteHTML: no match, fallback
    ViteHTML->>Browser: 200 text/html (index.html)
    Note over Browser: window.location = /login, SPA renders login route
Loading

Reviews (1): Last reviewed commit: "Self-host dev: serve the SPA shell for /..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1232

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1232

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1232

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1232

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1232

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1232

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1232

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1232

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1232

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1232

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1232

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1232

executor

npm i https://pkg.pr.new/executor@1232

commit: bd5b8de

@RhysSullivan
RhysSullivan merged commit 0816628 into main Jul 1, 2026
17 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.

1 participant