-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvite.config.ts
More file actions
183 lines (175 loc) · 6.59 KB
/
Copy pathvite.config.ts
File metadata and controls
183 lines (175 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import { readdirSync } from "node:fs";
import { join } from "node:path";
import type { Plugin } from "vite";
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { nitro } from "nitro/vite";
// @ts-expect-error JS plugin alongside the TS vite config
import { grokPwaPlugin } from "./scripts/grok-pwa-plugin.mjs";
// @ts-expect-error JS plugin alongside the TS vite config
import { appEnvPlugin } from "./scripts/app-env-plugin.mjs";
import { isMigrationFile } from "./scripts/migration-plan.mjs";
/** The files `src/lib/db.ts` globs — same directory, same non-recursive scope. */
function hasGlobbedMigrations(root: string): boolean {
try {
return readdirSync(join(root, "migrations")).some(isMigrationFile);
} catch {
return false;
}
}
/**
* Finish PGLite bootstrap during dev-server setup (before traffic). Vite awaits
* async `configureServer` hooks. Production: `src/lib/db` kicks `ensureDbReady`
* on import.
*
* Vite awaiting the hook puts this on time-to-first-render, so an app with no
* migrations — no schema to apply — skips it entirely rather than paying for a
* PGLite instance it never queries.
*/
function pgliteBootstrapPlugin(): Plugin {
return {
name: "app-builder:pglite-bootstrap",
apply: "serve",
async configureServer(server) {
if (!hasGlobbedMigrations(server.config.root)) return;
try {
const mod = (await server.ssrLoadModule("/src/lib/db.ts")) as {
ensureDbReady?: () => Promise<void>;
};
if (typeof mod.ensureDbReady === "function") {
await mod.ensureDbReady();
}
} catch (err) {
console.error("[app-builder] DB bootstrap failed:", err);
throw err;
}
},
};
}
/**
* Live-preview OAuth popup — handled HERE so the agent never has to create a
* `/auth/popup` route (and cannot break it by scaffolding a React page that
* paints the full app shell in the popup).
*
* `signIn` (client.ts) opens `/auth/popup?providerId=…` in a top-level window.
* This middleware runs before TanStack Start, calls `handleAuthPopupRequest`,
* and returns the 302 / completion HTML. Deployed apps do not use the popup
* (full-page OAuth redirect), so `apply: "serve"` is enough.
*/
function authPopupPlugin(): Plugin {
return {
name: "app-builder:auth-popup",
apply: "serve",
configureServer(server) {
// Register immediately (not in a returned post-hook) so we run BEFORE
// TanStack Start / the SPA HTML fallback. A model-authored
// `src/routes/auth/popup.tsx` React page must never win this path.
server.middlewares.use(async (req, res, next) => {
try {
const rawUrl = req.url ?? "";
const pathOnly = rawUrl.split("?", 1)[0] ?? "";
if (pathOnly !== "/auth/popup") {
next();
return;
}
if ((req.method ?? "GET").toUpperCase() !== "GET") {
res.statusCode = 405;
res.setHeader("content-type", "text/plain; charset=utf-8");
res.end("Method Not Allowed");
return;
}
const host = String(
req.headers["x-forwarded-host"] ?? req.headers.host ?? "localhost:8080",
);
const proto = String(
req.headers["x-forwarded-proto"] ??
((req.socket as { encrypted?: boolean } | undefined)?.encrypted ? "https" : "http"),
);
const requestHeaders = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value === undefined) continue;
if (Array.isArray(value)) {
for (const v of value) requestHeaders.append(key, v);
} else {
requestHeaders.set(key, value);
}
}
// Ensure Host is the public preview host so Better Auth's dynamic
// baseURL / redirect_uri match the popup origin.
if (!requestHeaders.has("host")) requestHeaders.set("host", host);
const request = new Request(`${proto}://${host}${rawUrl}`, {
method: "GET",
headers: requestHeaders,
});
const mod = (await server.ssrLoadModule("/src/lib/auth/popup.server.ts")) as {
handleAuthPopupRequest: (req: Request) => Promise<Response>;
};
const response = await mod.handleAuthPopupRequest(request);
res.statusCode = response.status;
// Preserve multiple Set-Cookie headers (OAuth state + session).
const setCookies =
typeof response.headers.getSetCookie === "function"
? response.headers.getSetCookie()
: [];
response.headers.forEach((value, key) => {
if (key.toLowerCase() === "set-cookie") return;
res.setHeader(key, value);
});
for (const cookie of setCookies) {
res.appendHeader("set-cookie", cookie);
}
const body = Buffer.from(await response.arrayBuffer());
res.end(body);
} catch (err) {
console.error("[app-builder] /auth/popup handler failed:", err);
if (!res.headersSent) {
res.statusCode = 500;
res.setHeader("content-type", "text/plain; charset=utf-8");
res.end("auth popup failed");
}
}
});
},
};
}
// `0.0.0.0:8080` is the live-preview contract — don't change host/port.
// The dev server starts once `src/router.tsx` and `src/routes/` exist — see
// AGENTS.md § "First scaffold".
export default defineConfig(({ command, isPreview }) => ({
server: {
host: "0.0.0.0",
port: 8080,
strictPort: true,
},
preview: {
host: "127.0.0.1",
port: 8081,
strictPort: true,
},
resolve: { tsconfigPaths: true },
plugins: [
pgliteBootstrapPlugin(),
// Before tanstackStart so /auth/popup never falls through to the SPA.
authPopupPlugin(),
// Dev-only /__app-env, read by scripts/check-auth-invariant.mjs.
appEnvPlugin(),
// PWA head + ?install=1 tutorial page; runs before Start/Nitro.
grokPwaPlugin(),
tailwindcss(),
tanstackStart(),
...(command === "build" || isPreview
? [
nitro({
preset: "vercel",
// Auto-registers server/middleware/* (the PWA install page +
// manifest + head-tag middleware). Nitro v3 defaults serverDir to
// false, so removing this silently unwires /?install=1 on deploys.
serverDir: "./server",
}),
]
: []),
viteReact(),
],
}));