/* eslint-disable react-refresh/only-export-components */ import { createRoot } from 'react-dom/client'; import { createBrowserRouter, RouterProvider, useParams, useSearchParams } from 'react-router-dom'; import type { RouteObject } from 'react-router-dom'; import { Toaster } from 'sonner'; import 'src/index.css'; import { AvatarProvider, TranslationProvider, registerPageMiddleware, registerPageRoute, validatePagePath, } from '@luckystack/core/client'; import type { PageMiddleware } from '@luckystack/core/client'; import { LocationProvider } from '@luckystack/presence/client'; import { sessionBasedToken } from 'config'; import ErrorPage from 'src/_components/ErrorPage'; import { MenuHandlerProvider } from 'src/_components/MenuHandler'; import TemplateProvider from 'src/_components/templates/TemplateProvider'; import { SessionProvider } from 'src/_providers/SessionProvider'; import { SocketStatusProvider } from 'src/_providers/socketStatusProvider'; //? OAuth handoff for sessionStorage-token mode. The backend's /auth/callback //? cannot Set-Cookie a sessionStorage token, so it 302s to `...#token=` //? (a fragment, never the query string, so the token is not sent to servers //? or logged). OAuth is a full-page navigation (no fetch to read an //? X-Session-Token header), so we capture the token from the URL HERE — //? synchronously, before React mounts (SessionProvider's socket connect + first //? `system/session` request both fire on mount and read the token from //? sessionStorage). Then strip it from the URL via replaceState so the //? long-lived token never lingers in the address bar or browser history. //? No-op in cookie mode (no #token= is emitted). if (sessionBasedToken) { const url = new URL(globalThis.location.href); const fragment = new URLSearchParams(url.hash.slice(1)); const handoffToken = fragment.get('token'); if (handoffToken) { sessionStorage.setItem('token', handoffToken); fragment.delete('token'); url.hash = fragment.toString(); globalThis.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`); } } //? Side-effect import — registers the locale map + language source with //? @luckystack/core. Imported once at the entry. The i18n-backed notifier //? auto-registers via the @luckystack/core/client barrel above. import 'luckystack/i18n/locales'; //? Per-page route guards live on each `page.tsx` via `export const middleware` //? and are auto-registered below in `getRoutes()`. The framework allows-by- //? default for routes without a guard. If you need a cross-cutting global //? hook, import `registerMiddlewareHandler` from '@luckystack/core/client' //? and call it here with your own function. import type { Template } from 'src/_components/templates/TemplateProvider'; interface PageProps { params: Record; searchParams: Record; } interface PageModule { default: React.ComponentType; template?: Template; middleware?: PageMiddleware; } //? Wrapper to inject Next.js-style params and searchParams as props. const PageWrapper = ({ Page }: { Page: React.ComponentType }) => { const params = useParams(); const [searchParams] = useSearchParams(); const searchParamsObj = Object.fromEntries(searchParams); return ; }; type PageLoader = () => Promise; const getRoutes = (loaders: Record): RouteObject[] => { const routes: RouteObject[] = []; //? Collision detection — two page.tsx files in different folder trees //? can compute the SAME route after invisible-parent stripping. First //? wins; second is skipped with a loud console.error. const seenRoutes = new Map(); for (const path in loaders) { const loader = loaders[path]; //? Apply the framework's invisible-parent rule: `_` segments //? are stripped from the URL; pages directly inside an `_` are //? invalid; pages inside reserved framework folders (_api, _sync, ...) //? are invalid. The validator returns the computed route or a reason. const result = validatePagePath(path); if (!result.valid || !result.route) { if (import.meta.env.DEV && result.reason && !result.reason.startsWith('not a page file')) { console.warn(`[luckystack] page route skipped for ${path}: ${result.reason}`); } continue; } // eslint-disable-next-line unicorn/prefer-string-replace-all const finalPath = result.route.toLowerCase().replace(/\[([^\]]+)\]/g, ':$1'); const previousFile = seenRoutes.get(finalPath); if (previousFile !== undefined) { console.error( `[luckystack] duplicate page route "${finalPath}": ${previousFile} AND ${path} both resolve here. ` + `Keeping the first; the second will not render. ` + `Remember that "_" segments are stripped from the URL (invisible-parent rule).`, ); continue; } seenRoutes.set(finalPath, path); //? Known up front, guarded or not, so a page without a guard is never //? resolved to a less specific page's guard (guards register lazily). registerPageRoute(finalPath); //? Lazy: the page component + its import tree load only when the route is //? visited, instead of every page eager-loading on first paint (which made //? even `/login` pull in every page + its deps). The page's `template` and //? per-page `middleware` live on the same module, so they resolve inside the //? loader. routes.push({ path: finalPath, lazy: async () => { const module = await loader(); //? Auto-register the page's `export const middleware` (if any) against //? its route. Framework's + useRouter prefer this over the //? global handler. if (module.middleware) { registerPageMiddleware(finalPath, module.middleware); } const template = module.template ?? 'plain'; return { element: ( ), }; }, }); } return routes; }; //? File-based routing: any `page.tsx` under `src/` becomes a route. Files in //? folders prefixed with `_` (e.g. `_api/`, `_sync/`, `_components/`) are //? private and never become routes. const prodPages = import.meta.glob([ './**/page.tsx', '!./**/_api/**', '!./**/_sync/**', '!./**/_server/**', '!./**/*_server.tsx', //? The @luckystack/docs-ui API explorer (`src/docs/page.tsx`, present only when //? you opted into docs-ui) is dev-only: it imports the dev-generated //? `apiDocs.generated.json` and exposes every endpoint, so it must NOT ship in //? a production bundle. Excluded here; in dev it loads via `devPages` below. '!./**/docs/**', ]); const devPages = import.meta.glob([ './**/page.tsx', ]); const loaders = (import.meta.env.PROD ? prodPages : devPages) as Record; const routes = getRoutes(loaders); //? Catch-all — any URL that doesn't match a page renders ErrorPage inside the //? plain template wrapper, whose lets the framework intercept //? auth / bootstrap state before painting. routes.push({ path: '*', element: ( ), }); const router = createBrowserRouter([{ path: '/', element: , errorElement: , //? Lazy routes make this a data router that hydrates async; React Router 7 //? warns without a HydrateFallback for the initial chunk load. `() => null` //? renders nothing during that brief window (no flash) and silences the warn. HydrateFallback: () => null, children: routes, }]); const root = document.querySelector('#root'); if (root) { createRoot(root).render(
); }