1import type { Plugin, ViteDevServer } from "vite-plus";
2
3// What the first page load pulls in: the app, the managed-auth shell, the chat
4// layout and its landing routes, and the sidebar's project icons.
5const STARTUP_LAZY_ENTRIES = [
6 "src/main.tsx",
7 "src/components/clerk/BrowserManagedAuthShell.tsx",
8 "src/routes/_chat.tsx",
9 "src/routes/_chat.index.tsx",
10 "src/routes/_chat.$environmentId.$threadId.tsx",
11 "src/lib/lucideIconByName.ts",
12];
13
14const WARMUP_CLIENT_ID = "t3code-startup-warmup";
15
16// Bundled dev keeps these private and untyped. A Vite upgrade that moves them
17// skips the warmup or logs a warning, never breaks dev; bundledDev.test.ts
18// fails so the upgrade cannot drop the warmup silently.
19interface BundledDevInternals {
20 waitForInitialBuildFinish(): Promise<void>;
21 triggerLazyBundling(
22 moduleId: string,
23 clientId: string,
24 ): Promise<{ readonly filename: string } | undefined>;
25 markPayloadDelivered(filename: string): void;
26 readonly devEngine: {
27 readonly moduleGraph: {
28 getModuleIds(): readonly string[];
29 getModuleInfo(id: string): { readonly dynamicallyImportedIds: readonly string[] } | null;
30 };
31 removeClient(clientId: string): Promise<void>;
32 };
33}
34
35function hasBundledDevInternals(value: unknown): value is BundledDevInternals {
36 return (
37 typeof value === "object" &&
38 value !== null &&
39 "waitForInitialBuildFinish" in value &&
40 typeof value.waitForInitialBuildFinish === "function" &&
41 "triggerLazyBundling" in value &&
42 typeof value.triggerLazyBundling === "function" &&
43 "markPayloadDelivered" in value &&
44 typeof value.markPayloadDelivered === "function"
45 );
46}
47
48/**
49 * Compiles `entries` as lazy chunks once, for a throwaway client, and returns
50 * the lazy module ids it compiled. The browser's own requests then reuse
51 * Rolldown's transform cache. Entries are paths relative to the Vite root.
52 */
53async function warmLazyEntries(
54 server: ViteDevServer,
55 entries: readonly string[],
56): Promise<string[]> {
57 // Widened so the guard narrows to the internals, not to Vite's public class.
58 const bundledDev: unknown = server.environments.client.bundledDev;
59 if (!hasBundledDevInternals(bundledDev)) return [];
60 await bundledDev.waitForInitialBuildFinish();
61
62 const { moduleGraph } = bundledDev.devEngine;
63 // Vite's root and Rolldown's module ids are both absolute, slash-separated paths.
64 const wanted = new Set(entries.map((entry) => `${server.config.root}/${entry}`));
65 const warmed = new Set<string>();
66 // A lazy entry is only in the graph after its parent compiled, so walk in
67 // passes. Each pass compiles at least one new wanted id, so it ends.
68 const nextLazyIds = () =>
69 new Set(
70 moduleGraph
71 .getModuleIds()
72 .flatMap((id) => moduleGraph.getModuleInfo(id)?.dynamicallyImportedIds ?? [])
73 .filter(
74 (id) =>
75 id.includes("rolldown-lazy") && !warmed.has(id) && wanted.has(id.split("?")[0] ?? id),
76 ),
77 );
78
79 try {
80 for (let pending = nextLazyIds(); pending.size > 0; pending = nextLazyIds()) {
81 for (const id of pending) {
82 warmed.add(id);
83 const result = await bundledDev.triggerLazyBundling(id, WARMUP_CLIENT_ID);
84 // Acked so the engine drops the pending payload instead of holding the chunk.
85 if (result) bundledDev.markPayloadDelivered(result.filename);
86 }
87 }
88 } finally {
89 await bundledDev.devEngine.removeClient(WARMUP_CLIENT_ID);
90 }
91 return [...warmed];
92}
93
94// Bundled dev compiles lazy chunks on first request and ignores `server.warmup`.
95// Warm them on listening to avoid serial compilation before first paint.
96export function warmBundledDevPlugin(): Plugin {
97 return {
98 name: "t3code:warm-bundled-dev",
99 apply: "serve",
100 configureServer(server) {
101 if (!server.config.experimental.bundledDev) return;
102 server.httpServer?.once("listening", () => {
103 const startedAt = performance.now();
104 warmLazyEntries(server, STARTUP_LAZY_ENTRIES).then(
105 (warmed) => {
106 if (warmed.length === 0) return;
107 const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);
108 server.config.logger.info(
109 `[t3code] pre-bundled ${String(warmed.length)} startup chunks in ${seconds}s`,
110 { timestamp: true },
111 );
112 },
113 (error: unknown) => {
114 server.config.logger.warn(`[t3code] startup chunk warmup failed: ${String(error)}`, {
115 timestamp: true,
116 });
117 },
118 );
119 });
120 },
121 };
122}