515c08afed
Everything sat in the eager startup graph (App.tsx statically imported all 28
routes; the editor pulled TipTap + KaTeX + ~45 lowlight grammars + drawio;
posthog + AI SDK loaded for everyone) — a /login visitor downloaded+compiled the
whole editor. Client-only; functionality 1:1, only WHEN code loads changed.
Result (prod build): eager JS 3.5MB -> ~1.12MB, entry 1920KB -> 552KB; KaTeX
(250KB) and the TipTap engine (~586KB) are now lazy chunks, off the startup path.
- App.tsx: route-level React.lazy + Suspense (editor Page, all settings/*, share,
space/home routes). Auth/redirect/cold-start routes stay eager. Suspense lives
inside Layout/ShareLayout around the Outlet so the shell stays mounted.
- Lazy KaTeX node views (math-inline-lazy/math-block-lazy) + lazy drawio
(drawio-view-lazy/drawio-menu-lazy), mirroring mermaid/excalidraw, each with a
node-sized Suspense placeholder so a slow chunk can't crash the editor.
- posthog-js is now a conditional dynamic import under the unchanged
isCloud() && isPostHogEnabled gate — self-hosted never downloads it.
- AiChatWindow is React.lazy, mounted on first open and kept mounted (a live AI
stream isn't torn down); renders null while closed (identical behavior).
- Cut eager TipTap pulls from always-loaded shell modules: editor-atoms /
global-bridge Editor -> import type; Aside lazily loaded (page routes only);
config.ts sanitizeUrl and use-clipboard execCommandCopy moved to client-local
src/lib/{sanitize-url,copy-to-clipboard}.ts (byte-identical to the editor-ext
originals, dropping the barrel's top-level @tiptap import); WebSocketStatus
import replaced with the "connected" literal the status atom already stores.
- vite.config.ts: a vendor-katex chunk group (TipTap/PM/Yjs intentionally NOT
grouped — grouping dragged the engine eager; documented in the config).
- lowlight grammar registration is left inside the (now-lazy) editor chunk:
listLanguages()/highlighting are synchronous, so deferring registration would
change behavior for marginal in-chunk gain — the route split already removes it
from startup, which was the complaint.
Gate: client build succeeds, tsc --noEmit clean, frozen install EXIT 0 (added
@braintree/sanitize-url as a direct client dep + regenerated the lock).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
3.2 KiB
TypeScript
120 lines
3.2 KiB
TypeScript
import bytes from "bytes";
|
|
import { castToBoolean } from "@/lib/utils.tsx";
|
|
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
|
import { sanitizeUrl } from "@/lib/sanitize-url.ts";
|
|
|
|
declare global {
|
|
interface Window {
|
|
CONFIG?: Record<string, string>;
|
|
}
|
|
}
|
|
|
|
export function getAppName(): string {
|
|
return "Gitmost";
|
|
}
|
|
|
|
export function getAppUrl(): string {
|
|
return `${window.location.protocol}//${window.location.host}`;
|
|
}
|
|
|
|
export function getServerAppUrl(): string {
|
|
return getConfigValue("APP_URL");
|
|
}
|
|
|
|
export function getBackendUrl(): string {
|
|
return getAppUrl() + "/api";
|
|
}
|
|
|
|
export function getCollaborationUrl(): string {
|
|
const baseUrl =
|
|
getConfigValue("COLLAB_URL") ||
|
|
(import.meta.env.DEV ? process.env.APP_URL : getAppUrl());
|
|
|
|
const collabUrl = new URL("/collab", baseUrl);
|
|
collabUrl.protocol = collabUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
return collabUrl.toString();
|
|
}
|
|
|
|
export function getSubdomainHost(): string {
|
|
return getConfigValue("SUBDOMAIN_HOST");
|
|
}
|
|
|
|
export function isCloud(): boolean {
|
|
return castToBoolean(getConfigValue("CLOUD"));
|
|
}
|
|
|
|
export function isCompactPageTreeEnabled(): boolean {
|
|
return castToBoolean(getConfigValue("COMPACT_PAGE_TREE", "true"));
|
|
}
|
|
|
|
// #355 — operator toggle for client perf-telemetry. DEFAULT OFF: the server
|
|
// mirrors CLIENT_TELEMETRY_ENABLED into window.CONFIG; when off the client
|
|
// installs no observers and sends nothing (the sink endpoint doesn't exist).
|
|
export function isClientTelemetryEnabled(): boolean {
|
|
return castToBoolean(getConfigValue("CLIENT_TELEMETRY_ENABLED", "false"));
|
|
}
|
|
|
|
export function getAvatarUrl(
|
|
avatarUrl: string,
|
|
type: AvatarIconType = AvatarIconType.AVATAR,
|
|
) {
|
|
if (!avatarUrl) return null;
|
|
if (avatarUrl?.startsWith("http")) return avatarUrl;
|
|
|
|
return getBackendUrl() + `/attachments/img/${type}/` + encodeURI(avatarUrl);
|
|
}
|
|
|
|
export function getSpaceUrl(spaceSlug: string) {
|
|
return "/s/" + spaceSlug;
|
|
}
|
|
|
|
export function getFileUrl(src: string) {
|
|
if (!src) return src;
|
|
if (src.startsWith("http")) return src;
|
|
if (src.startsWith("/api/")) {
|
|
// Remove the '/api' prefix
|
|
return getBackendUrl() + src.substring(4);
|
|
}
|
|
if (src.startsWith("/files/")) {
|
|
return getBackendUrl() + src;
|
|
}
|
|
return sanitizeUrl(src);
|
|
}
|
|
|
|
export function getFileUploadSizeLimit() {
|
|
const limit = getConfigValue("FILE_UPLOAD_SIZE_LIMIT", "50mb");
|
|
return bytes(limit);
|
|
}
|
|
|
|
export function getFileImportSizeLimit() {
|
|
const limit = getConfigValue("FILE_IMPORT_SIZE_LIMIT", "200mb");
|
|
return bytes(limit);
|
|
}
|
|
|
|
export function getDrawioUrl() {
|
|
return getConfigValue("DRAWIO_URL", "https://embed.diagrams.net");
|
|
}
|
|
|
|
export function getBillingTrialDays() {
|
|
return getConfigValue("BILLING_TRIAL_DAYS");
|
|
}
|
|
|
|
export function getPostHogHost() {
|
|
return getConfigValue("POSTHOG_HOST");
|
|
}
|
|
|
|
export function isPostHogEnabled(): boolean {
|
|
return Boolean(getPostHogHost() && getPostHogKey());
|
|
}
|
|
|
|
export function getPostHogKey() {
|
|
return getConfigValue("POSTHOG_KEY");
|
|
}
|
|
|
|
function getConfigValue(key: string, defaultValue: string = undefined): string {
|
|
const rawValue = import.meta.env.DEV
|
|
? process?.env?.[key]
|
|
: window?.CONFIG?.[key];
|
|
return rawValue ?? defaultValue;
|
|
}
|