Skip to content
File

Blob: vite.config.ts

typescript173 lines
1import { defineConfig, type Plugin } from "vite";
2import react, { reactCompilerPreset } from "@vitejs/plugin-react";
3import babel from "@rolldown/plugin-babel";
4import tailwindcss from "@tailwindcss/vite";
5import { cloudflare } from "@cloudflare/vite-plugin";
6import { serwist } from "@serwist/vite";
7import path from "path";
8import { cpSync, existsSync, readFileSync } from "node:fs";
9import { parseEnv } from "node:util";
10 
11// Captured once per build. Used as the revision for the precached Worker
12// rendered shell so every deploy refreshes the offline fallback copy.
13const SHELL_PRECACHE_REVISION = Date.now().toString(36);
14 
15// Non-user-facing URL for the precached shell. Using a URL that no visitor
16// ever navigates to keeps Serwist's precache route (registered before any
17// runtime route) from intercepting real `/` navigations; that would serve
18// an install-time cached shell to online users, reintroducing the stale
19// deploy hazard. The Worker treats any non-asset, non-api GET path as the
20// SPA shell (see `src/worker/lib/http-entry.ts`), so this path resolves to
21// `renderSpaShell` at SW install time.
22const SHELL_PRECACHE_URL = "/__pwa-shell";
23 
24const persistStatePath = process.env.BLAND_PERSIST_STATE_PATH;
25const ignoredWatchPaths = ["docs/**", "scripts/**", "playwright-report/**", "test-results/**", "tests/**"];
26const fontAssetPattern = /\.(?:woff2?|ttf|otf|eot)$/i;
27 
28function readDevVarsAiMode(): string {
29 const cloudflareEnv = process.env.CLOUDFLARE_ENV;
30 const devVarsPaths = [
31 ...(cloudflareEnv ? [path.resolve(import.meta.dirname, `.dev.vars.${cloudflareEnv}`)] : []),
32 path.resolve(import.meta.dirname, ".dev.vars"),
33 ];
34 
35 for (const devVarsPath of devVarsPaths) {
36 if (!existsSync(devVarsPath)) continue;
37 
38 return parseEnv(readFileSync(devVarsPath, "utf8")).BLAND_AI_MODE || "workers-ai";
39 }
40 
41 return "workers-ai";
42}
43 
44const useRemoteBindings = readDevVarsAiMode() === "workers-ai";
45 
46// Excalidraw defaults to fetching its font .woff2 files from esm.sh. Self-host
47// by copying `dist/prod/fonts/*` from node_modules into `public/fonts/`; at
48// mount time we set `window.EXCALIDRAW_ASSET_PATH = "/"` so Excalidraw's
49// internal `@font-face` URLs resolve against same-origin. Runs during
50// `configureServer` (dev) and `buildStart` (build) so both `npm run dev` and
51// `npm run build` pick up the assets. The esm.sh origin stays in CSP
52// `font-src` as a fallback; see `src/worker/lib/security-headers.ts`.
53function excalidrawFontsPlugin(): Plugin {
54 const src = path.resolve(import.meta.dirname, "node_modules/@excalidraw/excalidraw/dist/prod/fonts");
55 const dest = path.resolve(import.meta.dirname, "public/fonts");
56 const sentinel = path.join(dest, "Virgil", "Virgil-Regular.woff2");
57 const copy = () => {
58 if (!existsSync(src) || existsSync(sentinel)) return;
59 cpSync(src, dest, { recursive: true, force: false });
60 };
61 return {
62 name: "excalidraw-fonts-copy",
63 configureServer: copy,
64 buildStart: copy,
65 };
66}
67 
68export default defineConfig({
69 environments: {
70 bland: {
71 build: {
72 sourcemap: true,
73 },
74 },
75 },
76 plugins: [
77 react(),
78 babel({
79 presets: [reactCompilerPreset()],
80 }),
81 tailwindcss(),
82 excalidrawFontsPlugin(),
83 cloudflare({
84 ...(persistStatePath ? { persistState: { path: persistStatePath } } : {}),
85 remoteBindings: useRemoteBindings,
86 }),
87 serwist({
88 swSrc: "src/client/service-worker.ts",
89 // Serwist runs in Vite's client build, whose outDir is `dist/client`.
90 swDest: "sw.js",
91 swUrl: "/sw.js",
92 scope: "/",
93 type: "classic",
94 rollupFormat: "iife",
95 injectionPoint: "self.__SW_MANIFEST",
96 globDirectory: "dist/client",
97 // Precache a fetched copy of the Worker-rendered shell at SW install so
98 // an installed app launched offline still boots. The revision changes per
99 // build so each deploy refreshes the entry. See `SHELL_PRECACHE_URL` for
100 // why the URL is non-user-facing.
101 additionalPrecacheEntries: [{ url: SHELL_PRECACHE_URL, revision: SHELL_PRECACHE_REVISION }],
102 // The Excalidraw vendor chunk is ~4.75 MB; raising the default 2 MB cap
103 // so the canvas still works offline (goal: cache all assets).
104 maximumFileSizeToCacheInBytes: 8 * 1024 * 1024,
105 globPatterns: ["**/*.{js,css,woff,woff2,ttf,png,svg,ico,webmanifest,json}"],
106 globIgnores: ["**/index.html", "**/sw.js", "**/workbox-*.js"],
107 }),
108 ],
109 server: {
110 watch: {
111 ignored: ignoredWatchPaths,
112 },
113 },
114 resolve: {
115 alias: {
116 "@": path.resolve(import.meta.dirname, "src"),
117 },
118 },
119 build: {
120 assetsDir: "app-assets",
121 assetsInlineLimit: (filePath) => (fontAssetPattern.test(filePath) ? false : undefined),
122 chunkSizeWarningLimit: 1000,
123 manifest: "manifest.json",
124 rollupOptions: {
125 input: {
126 index: path.resolve(import.meta.dirname, "index.html"),
127 },
128 output: {
129 codeSplitting: {
130 groups: [
131 {
132 name: "vendor-react",
133 test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-sync-external-store)[\\/]/,
134 priority: 100,
135 entriesAware: true,
136 },
137 {
138 name: "vendor-prosemirror",
139 test: /[\\/]prosemirror-|[\\/]@tiptap[\\/]pm[\\/]/,
140 priority: 40,
141 entriesAware: true,
142 },
143 {
144 name: "vendor-tiptap",
145 test: /[\\/]@tiptap[\\/]/,
146 priority: 30,
147 entriesAware: true,
148 },
149 {
150 name: "vendor-yjs",
151 test: /[\\/](yjs|y-protocols|y-indexeddb|y-partyserver|partysocket|lib0)[\\/]/,
152 priority: 30,
153 entriesAware: true,
154 },
155 {
156 name: "vendor-sentry",
157 test: /[\\/]@sentry[\\/]/,
158 priority: 20,
159 entriesAware: true,
160 },
161 {
162 name: "vendor-excalidraw",
163 test: /[\\/]@excalidraw[\\/]/,
164 priority: 20,
165 entriesAware: true,
166 },
167 ],
168 },
169 },
170 },
171 },
172});