diff --git a/vite.config.ts b/vite.config.ts index 7119ab4..97d5608 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -77,6 +77,7 @@ export default defineConfig({ rollupOptions: { output: { entryFileNames: "shell.js", + chunkFileNames: "assets/[name]-[hash].js", }, }, }, @@ -1416,7 +1417,8 @@ export default defineConfig({ // Compute 8-char SHA-256 content hashes for module + shell assets const hashableFiles = allFiles.filter((f) => (f.startsWith("/modules/") && (f.endsWith(".js") || f.endsWith(".css")) && !f.includes("-demo.js")) || - f === "/shell.js" || f === "/shell.css" || f === "/theme.css" + f === "/shell.js" || f === "/shell.css" || f === "/theme.css" || + (f.startsWith("/assets/shell-") && f.endsWith(".js")) ); const hashes: Record = {}; for (const file of hashableFiles) { @@ -1441,7 +1443,8 @@ export default defineConfig({ f === "/shell.css" || f === "/theme.css" || f === "/favicon.png" || - f === "/manifest.json" + f === "/manifest.json" || + (f.startsWith("/assets/shell-") && f.endsWith(".js")) ).map((f) => hashes[f] ? `${f}?v=${hashes[f]}` : f); // Ensure root URL is present if (!core.some((f) => f === "/" || f.startsWith("/?v="))) core.unshift("/"); diff --git a/website/shell-offline.ts b/website/shell-offline.ts new file mode 100644 index 0000000..e05ff0c --- /dev/null +++ b/website/shell-offline.ts @@ -0,0 +1,66 @@ +/** + * Lazy-loaded offline runtime chunk. + * + * Contains Automerge + WASM (~2.5MB) — split from shell.ts so the main + * shell bundle stays small and doesn't block first paint. + */ + +import { RStackHistoryPanel } from "../shared/components/rstack-history-panel"; +import { RSpaceOfflineRuntime } from "../shared/local-first/runtime"; +import { CommunitySync } from "../lib/community-sync"; +import { OfflineStore } from "../lib/offline-store"; + +// Define the history panel component (depends on Automerge) +RStackHistoryPanel.define(); + +export function initOffline(spaceSlug: string) { + const runtime = new RSpaceOfflineRuntime(spaceSlug); + (window as any).__rspaceOfflineRuntime = runtime; + + // Configure module scope resolution from server-rendered data + try { + const scopeJson = document.body?.getAttribute("data-scope-overrides"); + const overrides: Record = scopeJson ? JSON.parse(scopeJson) : {}; + const moduleList: Array<{ id: string; scoping?: { defaultScope: string } }> = + (window as any).__rspaceModuleList || []; + const scopes: Array<{ id: string; scope: 'global' | 'space' }> = moduleList.map(m => ({ + id: m.id, + scope: (overrides[m.id] || m.scoping?.defaultScope || 'space') as 'global' | 'space', + })); + runtime.setModuleScopes(scopes); + } catch { /* scope config unavailable — defaults to space-scoped */ } + + runtime.init().catch((e: unknown) => { + console.warn("[shell] Offline runtime init failed — REST fallback only:", e); + }); + + // Flush pending writes before the page unloads + window.addEventListener("beforeunload", () => { + runtime.flush(); + }); + + // ── CommunitySync (tab list Automerge sync) ── + const offlineStore = new OfflineStore(); + const communitySync = new CommunitySync(spaceSlug, offlineStore); + (window as any).__communitySync = communitySync; + + (async () => { + try { + await offlineStore.open(); + await communitySync.initFromCache(); + } catch (e) { + console.warn("[shell] CommunitySync cache init failed:", e); + } + document.dispatchEvent(new CustomEvent("community-sync-ready", { + detail: { sync: communitySync, communitySlug: spaceSlug }, + })); + + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${proto}//${location.host}/ws/${spaceSlug}`; + communitySync.connect(wsUrl); + })(); + + window.addEventListener("beforeunload", () => { + communitySync.saveBeforeUnload(); + }); +} diff --git a/website/shell.ts b/website/shell.ts index 4dbb2be..2787561 100644 --- a/website/shell.ts +++ b/website/shell.ts @@ -15,7 +15,6 @@ import { RStackTabBar } from "../shared/components/rstack-tab-bar"; import { RStackMi } from "../shared/components/rstack-mi"; import { RStackModuleSetup } from "../shared/components/rstack-module-setup"; -import { RStackHistoryPanel } from "../shared/components/rstack-history-panel"; import { RStackOfflineIndicator } from "../shared/components/rstack-offline-indicator"; import { RStackSharePanel } from "../shared/components/rstack-share-panel"; import { RStackCommentBell } from "../shared/components/rstack-comment-bell"; @@ -23,9 +22,6 @@ import { RStackCollabOverlay } from "../shared/components/rstack-collab-overlay" import { RStackUserDashboard } from "../shared/components/rstack-user-dashboard"; import { rspaceNavUrl } from "../shared/url-helpers"; import { TabCache } from "../shared/tab-cache"; -import { RSpaceOfflineRuntime } from "../shared/local-first/runtime"; -import { CommunitySync } from "../lib/community-sync"; -import { OfflineStore } from "../lib/offline-store"; // Expose URL helper globally (used by shell inline scripts + components) (window as any).__rspaceNavUrl = rspaceNavUrl; @@ -42,67 +38,19 @@ RStackTabBar.define(); RStackMi.define(); RStackModuleSetup.define(); -RStackHistoryPanel.define(); RStackOfflineIndicator.define(); RStackSharePanel.define(); RStackCommentBell.define(); RStackCollabOverlay.define(); RStackUserDashboard.define(); -// ── Offline Runtime ── -// Instantiate the shared runtime from the space slug on the tag. -// Components access it via window.__rspaceOfflineRuntime. +// ── Offline Runtime (lazy-loaded) ── +// Automerge + WASM (~2.5MB) loaded in a separate chunk to avoid blocking first paint. +// Components that depend on window.__rspaceOfflineRuntime already handle late init. const spaceSlug = document.body?.getAttribute("data-space-slug"); if (spaceSlug) { - const runtime = new RSpaceOfflineRuntime(spaceSlug); - (window as any).__rspaceOfflineRuntime = runtime; - - // Configure module scope resolution from server-rendered data - try { - const scopeJson = document.body?.getAttribute("data-scope-overrides"); - const overrides: Record = scopeJson ? JSON.parse(scopeJson) : {}; - // Build scope config: merge module defaults with space overrides - const moduleList: Array<{ id: string; scoping?: { defaultScope: string } }> = - (window as any).__rspaceModuleList || []; - const scopes: Array<{ id: string; scope: 'global' | 'space' }> = moduleList.map(m => ({ - id: m.id, - scope: (overrides[m.id] || m.scoping?.defaultScope || 'space') as 'global' | 'space', - })); - runtime.setModuleScopes(scopes); - } catch { /* scope config unavailable — defaults to space-scoped */ } - - runtime.init().catch((e: unknown) => { - console.warn("[shell] Offline runtime init failed — REST fallback only:", e); - }); - - // Flush pending writes before the page unloads - window.addEventListener("beforeunload", () => { - runtime.flush(); - }); - - // ── CommunitySync (tab list Automerge sync) ── - const offlineStore = new OfflineStore(); - const communitySync = new CommunitySync(spaceSlug, offlineStore); - (window as any).__communitySync = communitySync; - - (async () => { - try { - await offlineStore.open(); - await communitySync.initFromCache(); - } catch (e) { - console.warn("[shell] CommunitySync cache init failed:", e); - } - document.dispatchEvent(new CustomEvent("community-sync-ready", { - detail: { sync: communitySync, communitySlug: spaceSlug }, - })); - - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${proto}//${location.host}/ws/${spaceSlug}`; - communitySync.connect(wsUrl); - })(); - - window.addEventListener("beforeunload", () => { - communitySync.saveBeforeUnload(); + import('./shell-offline').then(m => m.initOffline(spaceSlug)).catch(e => { + console.warn("[shell] Failed to load offline chunk:", e); }); }