Files
gitmost/apps/client/vite.config.ts
claude_code 32058ff272 feat(offline): PWA shell, Yjs-backed titles, and offline read cache (M0–M2)
Implements docs/offline-sync-plan.md milestones M0–M2.

M0 (PWA shell):
- Add vite-plugin-pwa (generateSW, registerType: 'prompt', manifest:false);
  NetworkOnly for /api,/collab,/socket.io, NetworkFirst for GET /api,
  navigateFallback to index.html.
- Register SW via useRegisterSW with a Mantine update prompt; skip
  registration inside Capacitor native WebView (is-capacitor guard).

M1 (harden CRDT body + title into Yjs):
- Lift the per-page Y.Doc/Hocuspocus providers into a shared hook+context so
  body and title editors share one doc.
- Move the page title into a dedicated 'title' Yjs fragment (CRDT, offline-
  tolerant); drop the REST title save. Server persists the title fragment to
  page.title and seeds it for legacy pages (empty-fragment guard); a collab
  rename emits a treeUpdate so other users' tree/breadcrumbs refresh.
- Persist the rebuilt ydoc on the content->ydoc path to neutralize the Yjs
  duplication trap. Add a 3-state sync indicator.

M2 (offline read/navigation):
- Persist React Query to IndexedDB (idb-keyval persister, version buster,
  selected roots only).
- "Make available offline" action warms page, space, tree (root+ancestors+
  children) and comments under exact hook keys, plus the page ydoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 05:30:55 +03:00

131 lines
3.7 KiB
TypeScript

import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
import * as path from "path";
import { execSync } from "node:child_process";
const envPath = path.resolve(process.cwd(), "..", "..");
// Resolve the version string shown in the UI.
// Priority: explicit APP_VERSION env (injected by Docker/CI, where .git is absent),
// then `git describe` for local builds, then the package.json version as a fallback.
function resolveAppVersion(cwd: string): string {
const fromEnv = process.env.APP_VERSION?.trim();
if (fromEnv) return fromEnv;
try {
return execSync("git describe --tags --always", {
cwd,
stdio: ["ignore", "pipe", "ignore"],
})
.toString()
.trim();
} catch {
return `v${process.env.npm_package_version ?? "0.0.0"}`;
}
}
export default defineConfig(({ mode }) => {
const {
APP_URL,
FILE_UPLOAD_SIZE_LIMIT,
FILE_IMPORT_SIZE_LIMIT,
DRAWIO_URL,
CLOUD,
SUBDOMAIN_HOST,
COLLAB_URL,
BILLING_TRIAL_DAYS,
POSTHOG_HOST,
POSTHOG_KEY,
} = loadEnv(mode, envPath, "");
return {
define: {
"process.env": {
APP_URL,
FILE_UPLOAD_SIZE_LIMIT,
FILE_IMPORT_SIZE_LIMIT,
DRAWIO_URL,
CLOUD,
SUBDOMAIN_HOST,
COLLAB_URL,
BILLING_TRIAL_DAYS,
POSTHOG_HOST,
POSTHOG_KEY,
},
APP_VERSION: JSON.stringify(resolveAppVersion(envPath)),
},
plugins: [
react(),
VitePWA({
registerType: "prompt",
injectRegister: null,
strategies: "generateSW",
manifest: false,
workbox: {
globPatterns: ["**/*.{js,css,html,svg,png,ico,woff2,json}"],
navigateFallback: "index.html",
navigateFallbackDenylist: [/^\/api\//, /^\/collab\//, /^\/socket\.io\//],
cleanupOutdatedCaches: true,
clientsClaim: true,
runtimeCaching: [
{ urlPattern: ({ url }) => url.pathname.startsWith("/collab"), handler: "NetworkOnly" },
{ urlPattern: ({ url }) => url.pathname.startsWith("/socket.io"), handler: "NetworkOnly" },
// M2 read-path: GET navigation API responses fall back to cache when offline.
// Only GET is cached; mutations always hit the network (Workbox caching handlers
// only match GET by default, but scope explicitly for clarity/safety).
{
urlPattern: ({ url, request }) => url.pathname.startsWith("/api") && request.method === "GET",
handler: "NetworkFirst",
options: {
cacheName: "api-get-cache",
networkTimeoutSeconds: 5,
expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 },
},
},
// Any non-GET /api stays network-only (never served stale).
{ urlPattern: ({ url }) => url.pathname.startsWith("/api"), handler: "NetworkOnly" },
],
},
devOptions: { enabled: false },
}),
],
build: {
rolldownOptions: {
output: {
advancedChunks: {
groups: [
{
name: "vendor-mantine",
test: /[\\/]node_modules[\\/]@mantine[\\/]/,
},
],
},
},
},
},
resolve: {
alias: {
"@": "/src",
},
},
server: {
proxy: {
"/api": {
target: APP_URL,
changeOrigin: false,
},
"/socket.io": {
target: APP_URL,
ws: true,
rewriteWsOrigin: true,
},
"/collab": {
target: APP_URL,
ws: true,
rewriteWsOrigin: true,
},
},
},
};
});