456a91d289
- F1 [HIGH]: added a root ChunkLoadErrorBoundary (react-error-boundary) wrapping
the routed app in main.tsx, ABOVE all the route-level/Aside/AiChatWindow
Suspense boundaries. A stale-deploy chunk 404 (React.lazy reject) is caught and
auto-reloads once (sessionStorage-guarded against a reload loop), else shows a
manual "new version available" reload UI — instead of unmounting the whole tree
to a white screen. Existing per-feature ErrorBoundaries untouched.
- F2 [MED-HIGH]: posthog no longer blocks/blanks the cloud first paint. main.tsx
now renders <App/> immediately for everyone, then `void initAnalytics()` — which
keeps the exact cloud gate, dynamically imports posthog, and RE-RENDERS the same
React root wrapped in PostHogProvider (React reconciles onto the painted DOM, so
cloud ends up wrapped exactly as before). The import+init is try/catch'd: a
failed analytics chunk (network / stale-404 / ad-blocker on a "posthog" chunk)
degrades to no-analytics instead of a permanently blank page.
- F3: sanitize-url.test.ts mirroring editor-ext's security contract (javascript:/
data:/vbscript:/obfuscated → ""; https/relative/mailto preserved).
- F4: the idle-warm `void import(...)` prefetch in layout.tsx gets `.catch(()=>{})`
so a failed best-effort prefetch can't surface as an unhandledrejection.
No new deps (lockfile unchanged). Gate: client tsc 0, sanitize test 3/3, client
build succeeds (entry chunk still 556K, posthog in separate dynamic chunks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
import { Suspense, useEffect } from "react";
|
|
import { UserProvider } from "@/features/user/user-provider.tsx";
|
|
import { Outlet, useParams } from "react-router-dom";
|
|
import { Center, Loader } from "@mantine/core";
|
|
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
|
|
import { SearchSpotlight } from "@/features/search/components/search-spotlight.tsx";
|
|
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
|
|
|
export default function Layout() {
|
|
const { spaceSlug } = useParams();
|
|
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
|
|
|
// Warm the (now route-split) editor chunk during idle time on authenticated
|
|
// routes, so the first navigation to a page renders from cache instead of a
|
|
// cold chunk fetch. Best-effort: gated on requestIdleCallback and never blocks
|
|
// startup — the dynamic import mirrors the App.tsx route lazy loader so both
|
|
// resolve to the same chunk.
|
|
useEffect(() => {
|
|
const ric =
|
|
typeof window !== "undefined" && (window as any).requestIdleCallback;
|
|
const warm = () => {
|
|
// Best-effort prefetch: a failed warm-up (offline, stale 404) is harmless
|
|
// and must not surface as an unhandledrejection.
|
|
void import("@/pages/page/page").catch(() => {});
|
|
};
|
|
if (ric) {
|
|
const id = ric(warm);
|
|
return () => (window as any).cancelIdleCallback?.(id);
|
|
}
|
|
const timer = setTimeout(warm, 2000);
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
return (
|
|
<UserProvider>
|
|
<GlobalAppShell>
|
|
<Suspense
|
|
fallback={
|
|
<Center h="60vh">
|
|
<Loader size="sm" />
|
|
</Center>
|
|
}
|
|
>
|
|
<Outlet />
|
|
</Suspense>
|
|
</GlobalAppShell>
|
|
<SearchSpotlight spaceId={space?.id} />
|
|
</UserProvider>
|
|
);
|
|
}
|