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>
131 lines
3.7 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
});
|