Files
gitmost/apps/client/vite.config.ts
claude_code 2786ee4699 fix(offline,server,docs): apply PR #116 review findings to offline-sync
Carries the still-applicable findings from the PR #116 review into PR #120,
since #120 includes the mobile-bootstrap commit. CORS hardening (removing the
unconditional localhost/capacitor origins) is intentionally left out of scope.

Service worker routing (latent bug fix + testability):
- vite.config.ts: anchor Workbox path matching to a segment boundary
  (^/<seg>(/|$)) instead of startsWith, so siblings like /apidocs,
  /collaborators, /socket.iox are no longer mis-routed as API/realtime and
  forced NetworkOnly; align navigateFallbackDenylist with the same anchors.
- new apps/client/src/pwa/sw-strategy.ts holds the canonical predicates
  (isApiPath, isCollabOrSocketPath) + unit tests; the vite.config regexes
  mirror it inline (Workbox generateSW serializes urlPattern fns standalone,
  so they cannot import the module).

Server CORS (R1 extraction + coverage):
- extract buildCorsAllowlist / isOriginAllowed into cors.util.ts with unit
  tests (evil-origin rejected, WebView/no-Origin allowed); main.ts rewired to
  use them with byte-for-byte identical behavior.

Privacy — clear offline cache on logout:
- new clear-offline-cache.ts purges the persisted query cache
  (idb-keyval gitmost-rq-cache), the Yjs page.* IndexedDB databases, and the
  service-worker api-get-cache; wired into handleLogout (best-effort, before
  the redirect) so a previous user's private data does not linger locally.

Conventions & docs:
- prettier fixes on main.ts and login.dto.ts.
- CHANGELOG: document offline reading, returnToken opt-in, optional Swagger,
  new env vars, logout cache-clear, and the CORS open->allowlist breaking
  change.
- docs/mobile-app-plan.md: correct the now-false §2.4 claims and update the
  §12 checklist (native cap add ios left unchecked — generated locally,
  gitignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:48:35 +03:00

138 lines
4.3 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",
// Segment-anchored (`^/<seg>(/|$)`) so navigation requests to these
// segments are consistently excluded from the SPA fallback, mirroring
// the runtimeCaching urlPattern regexes below.
navigateFallbackDenylist: [/^\/api(\/|$)/, /^\/collab(\/|$)/, /^\/socket\.io(\/|$)/],
cleanupOutdatedCaches: true,
clientsClaim: true,
// The urlPattern regexes below mirror apps/client/src/pwa/sw-strategy.ts
// and MUST be kept in sync with it. Workbox `generateSW` serializes these
// functions standalone into the generated service worker, so they cannot
// import the module — the matching logic is intentionally duplicated as
// self-contained inline regex literals anchored to a path segment boundary.
runtimeCaching: [
{ urlPattern: ({ url }) => /^\/(collab|socket\.io)(\/|$)/.test(url.pathname), 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 }) => /^\/api(\/|$)/.test(url.pathname) && 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 }) => /^\/api(\/|$)/.test(url.pathname), 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,
},
},
},
};
});