Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

96 linesCodeBlameRaw
1import { reactRouter } from "@react-router/dev/vite";
2import { cloudflare } from "@cloudflare/vite-plugin";
3import tailwindcss from "@tailwindcss/vite";
4import { defineConfig, type Plugin } from "vite";
5import { randomUUID } from "node:crypto";
6
7/**
8 * The doc editor's chunk and everything it imports, so a doc's page can
9 * ask the browser to fetch them with the page (`<link rel="modulepreload">`,
10 * routes/workspace/folios/folio.tsx `links`) rather than after the page's
11 * own code has run and asked for them. The editor is only ever imported
12 * lazily (it runs in the browser alone), so no route lists it among its
13 * modules; this notes its files as the browser build writes them, for the
14 * server build that follows in the same run. In development there are no
15 * such files, and the page adds no links.
16 */
17function editorChunk(): Plugin {
18 const id = "virtual:g1t-editor-chunk";
19 let files: string[] = [];
20 return {
21 name: "g1t-editor-chunk",
22 resolveId(source) {
23 return source === id ? `\0${id}` : null;
24 },
25 load(source) {
26 return source === `\0${id}` ? `export default ${JSON.stringify(files)};` : null;
27 },
28 generateBundle(_options, bundle) {
29 if (this.environment.name !== "client") return;
30 const chunks = Object.values(bundle).filter((output) => output.type === "chunk");
31 const editor = chunks.find((chunk) => chunk.moduleIds.some((moduleId) => moduleId.replace(/\\/g, "/").endsWith("/app/components/folios/doc/editor.tsx")));
32 if (!editor) return;
33 // The chunk first, then what it statically imports, each once.
34 const seen = new Set<string>();
35 const walk = (fileName: string) => {
36 if (seen.has(fileName)) return;
37 seen.add(fileName);
38 const chunk = chunks.find((c) => c.fileName === fileName);
39 for (const imported of chunk?.imports ?? []) walk(imported);
40 };
41 walk(editor.fileName);
42 files = [...seen].map((fileName) => `/${fileName}`);
43 },
44 };
45}
46
47export default defineConfig({
48 define: { "import.meta.env.G1T_BUILD_ID": JSON.stringify(randomUUID()) },
49 plugins: [
50 cloudflare({ viteEnvironment: { name: "ssr" } }),
51 tailwindcss(),
52 reactRouter(),
53 editorChunk(),
54 ],
55 resolve: {
56 tsconfigPaths: true,
57 },
58 build: {
59 rolldownOptions: {
60 output: {
61 codeSplitting: {
62 groups: [
63 // React and react-dom are CommonJS, which the bundler wraps in
64 // initialisers that a chunk calls while it loads. Left to
65 // automatic splitting, react-dom's landed in an app chunk that
66 // imports the chunk calling it, so that chunk ran first and
67 // threw `s is not a function`: tooltips, cards, menus and the
68 // spend pill never came up. In a chunk of their own, which
69 // imports nothing of the app's, they are always ready first.
70 { name: "react", test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-sync-external-store|react-is)[\\/]/, priority: 3 },
71 // The UI primitives behind shadcn's components (Radix, and the
72 // floating-ui that positions tooltips, cards and menus) set
73 // themselves up while they load, calling each other. Inside the
74 // app's chunks they could end up in two that import each other,
75 // and whichever a page loaded first would call into the other
76 // before it was ready. Together, apart from the app, they can't.
77 { name: "ui", test: /[\\/]node_modules[\\/](radix-ui|@radix-ui[\\/][^\\/]+|@floating-ui[\\/][^\\/]+|aria-hidden|react-remove-scroll[^\\/]*|use-callback-ref|use-sidecar|react-style-singleton|get-nonce|tslib|detect-node-es)[\\/]/, priority: 3 },
78 // Icons are shared by nearly every page, a few hundred bytes each:
79 // one file for all of them, not a request per icon.
80 { name: "icons", test: /[\\/]node_modules[\\/]lucide-react[\\/]/, priority: 2 },
81 // The app's own components and helpers that several routes
82 // share: grouped by which routes use them, and small groups
83 // merged into their neighbours, so a page fetches a few files
84 // of them rather than one per module. A signed-in page fetched
85 // 124 files, most of them a kilobyte or two, each a request and
86 // a `modulepreload` to parse. Modules one route alone uses stay
87 // with that route. Packages are left to automatic splitting: a
88 // group over them merged what the editor needs with what the
89 // diagram renderers need, and every doc fetched both.
90 { name: "app", test: /[\\/]apps[\\/]web[\\/]app[\\/](components|lib)[\\/]/, minShareCount: 2, entriesAware: true, entriesAwareMergeThreshold: 24_000, priority: 1 },
91 ],
92 },
93 },
94 },
95 },
96});