alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,654 lines • 97.4 kB
JavaScript
import { $atom, $env, $hook, $inject, $module, $state, Alepha, AlephaError, KIND, OPTIONS, PipelineHandler, Primitive, SchemaValidator, coerceObject, createPrimitive, z } from "alepha";
import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
import { AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, useAlepha, useEvents, useInject, useStore } from "alepha/react";
import { AlephaReactHead, BrowserHeadProvider, ServerHeadProvider } from "alepha/react/head";
import { AlephaServer, ServerProvider, ServerRouterProvider } from "alepha/server";
import { AlephaServerEtag } from "alepha/server/etag";
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "alepha/server/links";
import { $cache } from "alepha/cache";
import { $logger } from "alepha/logger";
import { currentUserAtom } from "alepha/security";
import { StrictMode, createContext, createElement, memo, use, useEffect, useRef, useState } from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { join } from "node:path";
import { ServerStaticProvider } from "alepha/server/static";
import { FileSystemProvider } from "alepha/system";
import { renderToReadableStream, renderToString } from "react-dom/server";
import { RouterProvider } from "alepha/router";
//#region ../../src/react/router/constants/PAGE_PRELOAD_KEY.ts
/**
* Symbol key for SSR module preloading path.
* Using Symbol.for() allows the Vite plugin to inject this at build time.
* @internal
*/
const PAGE_PRELOAD_KEY = Symbol.for("alepha.page.preload");
//#endregion
//#region ../../src/react/router/services/ReactPageService.ts
/**
* $page methods interface.
*/
var ReactPageService = class {
fetch(pathname, options = {}) {
throw new AlephaError("Fetch is not available for this environment.");
}
render(name, options = {}) {
throw new AlephaError("Render is not available for this environment.");
}
};
//#endregion
//#region ../../src/react/router/primitives/$page.ts
/**
* Main primitive for defining a React route in the application.
*
* The $page primitive is the core building block for creating type-safe, SSR-enabled React routes.
* It provides a declarative way to define pages with powerful features:
*
* **Routing & Navigation**
* - URL pattern matching with parameters (e.g., `/users/:id`)
* - Nested routing with parent-child relationships
* - Type-safe URL parameter and query string validation
*
* **Data Loading**
* - Server-side data fetching with the `loader` function
* - Automatic serialization and hydration for SSR
* - Access to request context, URL params, and parent data
*
* **Component Loading**
* - Direct component rendering or lazy loading for code splitting
* - Client-only rendering when browser APIs are needed
* - Automatic fallback handling during hydration
*
* **Performance Optimization**
* - Static generation for pre-rendered pages at build time
* - Server-side caching with configurable TTL and providers
* - Code splitting through lazy component loading
*
* **Error Handling**
* - Custom error handlers with support for redirects
* - Hierarchical error handling (child → parent)
* - HTTP status code handling (404, 401, etc.)
*
* @example Simple page with data fetching
* ```typescript
* const userProfile = $page({
* path: "/users/:id",
* schema: {
* params: z.object({ id: z.integer() }),
* query: z.object({ tab: z.text().optional() })
* },
* loader: async ({ params }) => {
* const user = await userApi.getUser(params.id);
* return { user };
* },
* lazy: () => import("./UserProfile.tsx")
* });
* ```
*
* @example Nested routing with error handling
* ```typescript
* const projectSection = $page({
* path: "/projects/:id",
* children: () => [projectBoard, projectSettings],
* loader: async ({ params }) => {
* const project = await projectApi.get(params.id);
* return { project };
* },
* errorHandler: (error) => {
* if (HttpError.is(error, 404)) {
* return <ProjectNotFound />;
* }
* }
* });
* ```
*
* @example Static generation with caching
* ```typescript
* const blogPost = $page({
* path: "/blog/:slug",
* static: {
* entries: posts.map(p => ({ params: { slug: p.slug } }))
* },
* loader: async ({ params }) => {
* const post = await loadPost(params.slug);
* return { post };
* }
* });
* ```
*/
const $page = (options) => {
return createPrimitive(PagePrimitive, options);
};
var PagePrimitive = class extends Primitive {
reactPageService = $inject(ReactPageService);
onInit() {
if (this.options.static) {
this.options.use ??= [];
if (!this.options.use.some((m) => m[OPTIONS]?.name === "$cache")) this.options.use.push($cache({
name: `page:${this.name}`,
provider: "memory",
ttl: [1, "week"]
}));
}
}
get name() {
return this.options.name ?? this.config.propertyKey;
}
/**
* For testing or build purposes.
*
* This will render the page (HTML layout included or not) and return the HTML + context.
* Only valid for server-side rendering, it will throw an error if called on the client-side.
*/
async render(options) {
return this.reactPageService.render(this.name, options);
}
async fetch(options) {
return this.reactPageService.fetch(this.options.path || "", options);
}
};
$page[KIND] = PagePrimitive;
//#endregion
//#region ../../src/react/router/hooks/useRouterState.ts
const useRouterState = () => {
const [state] = useStore("alepha.react.router.state");
if (!state) throw new AlephaError("Missing react router state");
return state;
};
//#endregion
//#region ../../src/react/router/components/ErrorViewer.tsx
const mono = "ui-monospace, \"JetBrains Mono\", SFMono-Regular, Menlo, Monaco, Consolas, monospace";
const ErrorViewer = (props) => {
if (props.alepha.isProduction()) return /* @__PURE__ */ jsx(ProdErrorPage, { error: props.error });
return /* @__PURE__ */ jsx(DevErrorPage, { ...props });
};
const DevErrorPage = (props) => {
const [copied, setCopied] = useState(false);
const pathname = useRouterState().url.pathname;
const handleCopy = () => {
const text = buildErrorText(props.error);
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2e3);
}
};
const status = getHttpStatus(props.error);
return /* @__PURE__ */ jsx("div", {
style: dev.overlay,
children: /* @__PURE__ */ jsxs("div", {
style: dev.card,
children: [
/* @__PURE__ */ jsxs("div", {
style: dev.header,
children: [/* @__PURE__ */ jsxs("div", {
style: dev.headerLeft,
children: [/* @__PURE__ */ jsx("div", {
style: dev.icon,
children: "!"
}), /* @__PURE__ */ jsx("div", {
style: dev.title,
children: props.error.name || "Error"
})]
}), /* @__PURE__ */ jsxs("div", {
style: dev.headerRight,
children: [props.onRetry && /* @__PURE__ */ jsx("button", {
type: "button",
style: dev.retryBtn,
onClick: props.onRetry,
children: "Retry"
}), /* @__PURE__ */ jsx("button", {
type: "button",
style: dev.copyBtn,
onClick: handleCopy,
children: copied ? "Copied" : "Copy"
})]
})]
}),
/* @__PURE__ */ jsx(ErrorBlock, { error: props.error }),
/* @__PURE__ */ jsxs("div", {
style: dev.meta,
children: [
pathname && /* @__PURE__ */ jsx("span", {
style: dev.metaItem,
children: pathname
}),
/* @__PURE__ */ jsx("span", {
style: dev.metaItem,
children: (/* @__PURE__ */ new Date()).toLocaleTimeString()
}),
status && /* @__PURE__ */ jsx("span", {
style: dev.statusBadge,
children: status
})
]
})
]
})
});
};
const ErrorBlock = (props) => {
const { error, depth = 0 } = props;
return /* @__PURE__ */ jsxs(Fragment, { children: [
depth > 0 && /* @__PURE__ */ jsx("div", {
style: dev.causedBy,
children: "Caused by:"
}),
/* @__PURE__ */ jsxs("pre", {
style: dev.messageBlock,
children: [
error.name,
": ",
error.message
]
}),
error.stack && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
style: dev.stackLabel,
children: "STACK TRACE"
}), /* @__PURE__ */ jsx("pre", {
style: dev.stackBlock,
children: cleanStack(error.stack)
})] }),
error.cause instanceof Error && /* @__PURE__ */ jsx(ErrorBlock, {
error: error.cause,
depth: depth + 1
})
] });
};
const ProdErrorPage = (props) => {
const requestId = props.error.requestId;
const status = getHttpStatus(props.error);
const handleReload = () => {
if (typeof window !== "undefined") window.location.reload();
};
const { heading, subtitle } = status === 401 ? {
heading: "Sign in required",
subtitle: "Please sign in to view this page."
} : status === 403 ? {
heading: "Access denied",
subtitle: "You do not have permission to view this page."
} : {
heading: "Something went wrong",
subtitle: "We're having trouble processing your request."
};
const canReload = status !== 401 && status !== 403;
return /* @__PURE__ */ jsx("div", {
style: prod.page,
children: /* @__PURE__ */ jsxs("div", {
style: prod.card,
children: [
/* @__PURE__ */ jsx("div", {
style: prod.heading,
children: heading
}),
/* @__PURE__ */ jsx("div", {
style: prod.subtitle,
children: subtitle
}),
requestId && /* @__PURE__ */ jsxs("div", {
style: prod.refText,
children: ["Reference: ", requestId]
}),
/* @__PURE__ */ jsxs("div", {
style: prod.actions,
children: [canReload && /* @__PURE__ */ jsx("button", {
type: "button",
style: prod.reloadBtn,
onClick: handleReload,
children: "Reload page"
}), /* @__PURE__ */ jsx("a", {
href: "/",
style: prod.homeLink,
children: "Go home"
})]
})
]
})
});
};
function cleanStack(stack) {
return stack.split("\n").slice(1).map((l) => l.trim()).join("\n");
}
function buildErrorText(error) {
const parts = [];
const append = (err, depth) => {
if (depth > 0) parts.push(`\nCaused by:`);
parts.push(`${err.name}: ${err.message}`);
if (err.stack) parts.push(cleanStack(err.stack));
if (err.cause instanceof Error) append(err.cause, depth + 1);
};
append(error, 0);
return parts.join("\n");
}
function getHttpStatus(error) {
if ("status" in error && typeof error.status === "number") return error.status;
}
const dev = {
overlay: {
position: "fixed",
inset: 0,
zIndex: 2147483647,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "24px",
backgroundColor: "rgba(0, 0, 0, 0.75)",
backdropFilter: "blur(4px)",
fontFamily: mono,
fontSize: "13px",
color: "#e5e7eb",
boxSizing: "border-box",
overflow: "auto"
},
card: {
width: "100%",
maxWidth: "900px",
maxHeight: "90vh",
overflow: "auto",
padding: "24px",
backgroundColor: "#111",
borderLeft: "4px solid #ef4444",
borderRadius: "8px",
boxShadow: "0 25px 50px rgba(0, 0, 0, 0.5)"
},
header: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "20px",
flexWrap: "wrap",
gap: "12px"
},
headerLeft: {
display: "flex",
alignItems: "center",
gap: "12px",
minWidth: 0
},
headerRight: {
display: "flex",
gap: "8px",
flexShrink: 0
},
icon: {
width: "28px",
height: "28px",
borderRadius: "50%",
backgroundColor: "#ef4444",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
fontSize: "16px",
flexShrink: 0
},
title: {
color: "#f87171",
fontSize: "18px",
fontWeight: 700
},
retryBtn: {
padding: "6px 16px",
backgroundColor: "#2563eb",
color: "#fff",
border: "none",
borderRadius: "6px",
cursor: "pointer",
fontFamily: mono,
fontSize: "12px",
fontWeight: 600
},
copyBtn: {
padding: "6px 16px",
backgroundColor: "transparent",
color: "#9ca3af",
border: "1px solid #374151",
borderRadius: "6px",
cursor: "pointer",
fontFamily: mono,
fontSize: "12px",
fontWeight: 600
},
messageBlock: {
margin: "0 0 12px",
padding: "12px",
backgroundColor: "#1f2937",
borderRadius: "6px",
color: "#f9fafb",
fontSize: "13px",
lineHeight: 1.6,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflow: "auto"
},
stackLabel: {
fontSize: "10px",
color: "#6b7280",
textTransform: "uppercase",
letterSpacing: "1.5px",
marginBottom: "6px",
fontWeight: 600
},
stackBlock: {
margin: "0 0 20px",
padding: "12px",
backgroundColor: "#0f172a",
borderRadius: "6px",
color: "#94a3b8",
fontSize: "12px",
lineHeight: 1.6,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
overflow: "auto"
},
causedBy: {
fontSize: "10px",
color: "#6b7280",
textTransform: "uppercase",
letterSpacing: "1.5px",
marginBottom: "8px",
paddingTop: "12px",
borderTop: "1px solid #374151",
fontWeight: 600
},
meta: {
display: "flex",
gap: "16px",
alignItems: "center",
paddingTop: "12px",
borderTop: "1px solid #1f2937",
flexWrap: "wrap"
},
metaItem: {
fontSize: "11px",
color: "#6b7280"
},
statusBadge: {
fontSize: "11px",
color: "#fbbf24",
backgroundColor: "#422006",
padding: "2px 8px",
borderRadius: "4px",
fontWeight: 600
}
};
const prod = {
page: {
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0f172a",
fontFamily: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
padding: "24px"
},
card: {
textAlign: "center",
maxWidth: "440px"
},
heading: {
fontSize: "28px",
fontWeight: 700,
color: "#f1f5f9",
marginBottom: "12px"
},
subtitle: {
fontSize: "16px",
color: "#94a3b8",
marginBottom: "24px",
lineHeight: 1.5
},
refText: {
fontSize: "13px",
color: "#64748b",
marginBottom: "24px",
fontFamily: mono
},
actions: {
display: "flex",
gap: "12px",
justifyContent: "center",
alignItems: "center"
},
reloadBtn: {
padding: "10px 24px",
backgroundColor: "#2563eb",
color: "#fff",
border: "none",
borderRadius: "8px",
cursor: "pointer",
fontSize: "14px",
fontWeight: 600
},
homeLink: {
padding: "10px 24px",
color: "#94a3b8",
textDecoration: "none",
fontSize: "14px"
}
};
//#endregion
//#region ../../src/react/router/contexts/RouterLayerContext.ts
const RouterLayerContext = createContext(void 0);
//#endregion
//#region ../../src/react/router/errors/Redirection.ts
/**
* Used for Redirection during the page loading.
*
* Depends on the context, it can be thrown or just returned.
*
* @example
* ```ts
* import { Redirection } from "alepha/react";
*
* const MyPage = $page({
* loader: async () => {
* if (needRedirect) {
* throw new Redirection("/new-path");
* }
* },
* });
* ```
*/
var Redirection = class extends AlephaError {
redirect;
constructor(redirect) {
super("Redirection");
this.redirect = redirect;
}
};
//#endregion
//#region ../../src/react/router/components/NestedView.tsx
/**
* A component that renders the current view of the nested router layer.
*
* To be simple, it renders the `element` of the current child page of a parent page.
*
* @example
* ```tsx
* import { NestedView } from "alepha/react";
*
* class App {
* parent = $page({
* component: () => <NestedView />,
* });
*
* child = $page({
* parent: this.root,
* component: () => <div>Child Page</div>,
* });
* }
* ```
*/
const NestedView = (props) => {
const routerLayer = use(RouterLayerContext);
const index = routerLayer?.index ?? 0;
const onError = routerLayer?.onError;
const state = useRouterState();
const alepha = useAlepha();
const boundaryResetKeys = [state.url.pathname];
const [view, setView] = useState(state.layers[index]?.element);
const [animation, setAnimation] = useState("");
const animationExitDuration = useRef(0);
const animationExitNow = useRef(0);
useEvents({
"react:transition:begin": async ({ previous, state }) => {
const layer = previous.layers[index];
if (!layer) return;
if (`${state.url.pathname}/`.startsWith(`${layer.path}/`)) return;
const animationExit = parseAnimation(layer.route?.animation, state, "exit");
if (animationExit) {
const duration = animationExit.duration || 200;
animationExitNow.current = Date.now();
animationExitDuration.current = duration;
setAnimation(animationExit.animation);
} else {
animationExitNow.current = 0;
animationExitDuration.current = 0;
setAnimation("");
}
},
"react:transition:end": async ({ state }) => {
const layer = state.layers[index];
if (animationExitNow.current) {
const duration = animationExitDuration.current;
const diff = Date.now() - animationExitNow.current;
if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
}
if (!layer?.cache) {
setView(layer?.element);
const animationEnter = parseAnimation(layer?.route?.animation, state, "enter");
if (animationEnter) setAnimation(animationEnter.animation);
else setAnimation("");
}
}
}, []);
let element = view ?? props.children ?? null;
if (animation) element = /* @__PURE__ */ jsx("div", {
style: {
display: "flex",
flex: 1,
height: "100%",
width: "100%",
position: "relative",
overflow: "hidden"
},
children: /* @__PURE__ */ jsx("div", {
style: {
height: "100%",
width: "100%",
display: "flex",
animation
},
children: element
})
});
if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
fallback: props.errorBoundary,
resetKeys: boundaryResetKeys,
children: element
});
const fallback = (error) => {
const result = onError?.(error, state) ?? /* @__PURE__ */ jsx(ErrorViewer, {
error,
alepha
});
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
return result;
};
return /* @__PURE__ */ jsx(ErrorBoundary, {
fallback,
resetKeys: boundaryResetKeys,
children: element
});
};
var NestedView_default = memo(NestedView);
function parseAnimation(animationLike, state, type = "enter") {
if (!animationLike) return;
const DEFAULT_DURATION = 300;
const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
if (typeof animation === "string") {
if (type === "exit") return;
return {
duration: DEFAULT_DURATION,
animation: `${DEFAULT_DURATION}ms ${animation}`
};
}
if (typeof animation === "object") {
const anim = animation[type];
const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
const name = typeof anim === "object" ? anim.name : anim;
if (type === "exit") return {
duration,
animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
};
return {
duration,
animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
};
}
}
//#endregion
//#region ../../src/react/router/components/NotFound.tsx
/**
* Default 404 Not Found page component.
*/
const NotFound = (props) => /* @__PURE__ */ jsxs("div", {
style: {
width: "100%",
minHeight: "90vh",
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
padding: "2rem",
...props.style
},
children: [/* @__PURE__ */ jsx("div", {
style: {
fontSize: "6rem",
fontWeight: 200,
lineHeight: 1
},
children: "404"
}), /* @__PURE__ */ jsx("div", {
style: {
fontSize: "0.875rem",
marginTop: "1rem",
opacity: .6
},
children: "Page not found"
})]
});
//#endregion
//#region ../../src/react/router/providers/RootComponentsProvider.ts
/**
* Extension point letting any module contribute root-level React nodes that
* render on every page (siblings of the page view, inside AlephaContext).
*
* A module pushes into `rootComponents` from its `register` hook; the array
* is rendered by `ReactPageProvider.root()`. SSR-safe (same element feeds
* server render + client hydrate).
*/
var RootComponentsProvider = class {
rootComponents = [];
};
//#endregion
//#region ../../src/react/router/providers/RouterLocaleProvider.ts
/**
* Generic locale path-prefix mechanism for the router.
*
* This provider knows nothing about i18n — it only deals with URL path
* segments. It is configured by the i18n module (`I18nProvider`) when
* `routing: "prefix"` is enabled, which keeps the dependency one-directional
* (`i18n → router`) and avoids a module cycle.
*
* The default locale is served WITHOUT a prefix (`/about` = default,
* `/fr/about` = French). The active locale is derived from the current
* request/navigation and stored under `alepha.react.router.locale`, so every
* URL the router builds (`pathname()`) automatically carries the right prefix.
*/
var RouterLocaleProvider = class {
alepha = $inject(Alepha);
/**
* Whether locale path-prefixing is active. Off by default — opt-in via the
* i18n module.
*/
enabled = false;
/**
* The default locale, served without a path prefix (e.g. `"en"` → `/about`).
*/
defaultLocale = "";
/**
* All known locales, including the default one.
*/
locales = [];
/**
* Configure the provider. Called by the i18n module before the SSR routes
* are registered.
*/
configure(options) {
if (options.enabled !== void 0) this.enabled = options.enabled;
if (options.defaultLocale !== void 0) this.defaultLocale = options.defaultLocale;
if (options.locales !== void 0) this.locales = options.locales;
}
/**
* Locales that carry a URL prefix — every known locale except the default.
*/
get prefixedLocales() {
return this.locales.filter((locale) => locale !== this.defaultLocale);
}
/**
* Splits a leading locale segment off a pathname.
*
* - `/fr/about` → `{ locale: "fr", pathname: "/about" }` when `fr` is a
* prefixed locale.
* - `/about` → `{ locale: defaultLocale, pathname: "/about" }`.
*
* When prefixing is disabled the pathname is returned untouched.
*/
detect(pathname) {
if (this.enabled) {
const first = pathname.split("/")[1];
if (first && this.prefixedLocales.includes(first)) {
const rest = pathname.slice(first.length + 1);
return {
locale: first,
pathname: this.normalize(rest)
};
}
}
return {
locale: this.defaultLocale,
pathname
};
}
/**
* Prepends the locale prefix to a pathname when needed. The default locale
* (and any unknown/disabled case) returns the pathname unchanged.
*/
withPrefix(pathname, locale = this.current) {
if (!this.enabled || !locale || locale === this.defaultLocale || !this.prefixedLocales.includes(locale)) return pathname;
return `/${locale}${pathname === "/" ? "" : pathname}`;
}
/**
* The active locale, derived from the current request/navigation. Falls back
* to the default locale when nothing has been detected.
*/
get current() {
return this.alepha.store.get("alepha.react.router.locale") || this.defaultLocale;
}
set current(locale) {
this.alepha.store.set("alepha.react.router.locale", locale);
}
/**
* Normalizes a stripped pathname so it always starts with a single slash and
* carries no trailing slash (except the root `/`).
*/
normalize(pathname) {
if (!pathname || pathname === "/") return "/";
const withLeading = pathname.startsWith("/") ? pathname : `/${pathname}`;
return withLeading.length > 1 && withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
}
};
//#endregion
//#region ../../src/react/router/providers/ReactPageProvider.ts
const reactPageOptions = $atom({
name: "alepha.react.page.options",
description: "Configuration options for the React page provider.",
schema: z.object({
/**
* Enable React StrictMode wrapper.
*/
strictMode: z.boolean().default(true),
/**
* RegExp pattern (as string) to detect file-like URLs (e.g. /hello.txt, /wp-login.php).
* When a request hits the catch-all wildcard route and matches this pattern,
* SSR is skipped and a plain 404 response is returned instead.
*
* Set to empty string to disable this behavior.
*
* @default "\\.[a-zA-Z0-9]{1,10}$"
*/
staticFilePattern: z.string()
}),
default: {
strictMode: true,
staticFilePattern: "\\.[a-zA-Z0-9]{1,10}$"
}
});
/**
* Handle page routes for React applications. (Browser and Server)
*/
var ReactPageProvider = class {
dateTimeProvider = $inject(DateTimeProvider);
log = $logger();
options = $state(reactPageOptions);
alepha = $inject(Alepha);
rootComponentsProvider = $inject(RootComponentsProvider);
localeProvider = $inject(RouterLocaleProvider);
pages = [];
nextIdCursor = 0;
configure = $hook({
on: "configure",
handler: () => {
let hasNotFoundHandler = false;
const pages = this.alepha.primitives($page);
const hasParent = (it) => {
if (it.options.parent) return true;
for (const page of pages) if ((page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : []).includes(it)) return true;
};
for (const page of pages) {
if (page.options.path === "/*") hasNotFoundHandler = true;
if (hasParent(page)) continue;
this.add(this.map(pages, page));
}
if (!hasNotFoundHandler && pages.length > 0) this.add({
path: "/*",
name: "notFound",
component: NotFound,
onServerResponse: ({ reply }) => {
reply.status = 404;
}
});
}
});
getPages() {
return this.pages;
}
getConcretePages() {
const pages = [];
for (const page of this.pages) {
if (page.children && page.children.length > 0) continue;
const fullPath = this.pathname(page.name);
if (fullPath.includes(":") || fullPath.includes("*")) {
if (typeof page.static === "object") {
const entries = page.static.entries;
if (entries && entries.length > 0) for (const entry of entries) {
const params = entry.params;
const path = this.compile(page.path ?? "", params);
if (!path.includes(":") && !path.includes("*")) pages.push({
...page,
name: params[Object.keys(params)[0]],
staticName: page.name,
path,
...entry
});
}
}
continue;
}
pages.push(page);
}
return pages;
}
page(name) {
for (const page of this.pages) if (page.name === name) return page;
throw new AlephaError(`Page '${name}' not found`);
}
/**
* Find a route by name anywhere in the tree (including nested children).
* Returns undefined if no page with that name exists.
*/
findRoute(name, routes = this.pages) {
for (const route of routes) {
if (route.name === name) return route;
if (route.children?.length) {
const found = this.findRoute(name, route.children);
if (found) return found;
}
}
}
pathname(name, options = {}) {
const page = this.page(name);
if (!page) throw new AlephaError(`Page ${name} not found`);
let url = page.path ?? "";
let parent = page.parent;
while (parent) {
url = `${parent.path ?? ""}/${url}`;
parent = parent.parent;
}
url = this.compile(url, options.params ?? {});
url = url.replace(/\/\/+/g, "/") || "/";
url = this.localeProvider.withPrefix(url);
if (options.query) {
const query = new URLSearchParams(options.query);
if (query.toString()) url += `?${query.toString()}`;
}
return url;
}
url(name, options = {}) {
return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
}
root(state) {
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element), ...this.rootComponentsProvider.rootComponents);
if (this.options.strictMode) return createElement(StrictMode, {}, root);
return root;
}
convertStringObjectToObject = (schema, value) => {
if (z.schema.isObject(schema) && typeof value === "object") for (const key in schema.properties) {
const propSchema = z.schema.unwrap(schema.properties[key]);
if (z.schema.isObject(propSchema) && typeof value[key] === "string") try {
value[key] = this.alepha.codec.decode(propSchema, decodeURIComponent(value[key]));
} catch (e) {}
}
return value;
};
/**
* Create a new RouterState based on a given route and request.
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
* It also handles errors and redirects.
*/
async createLayers(route, state, previous = []) {
let context = {};
const stack = [{ route }];
let parent = route.parent;
while (parent) {
stack.unshift({ route: parent });
parent = parent.parent;
}
let forceRefresh = false;
for (let i = 0; i < stack.length; i++) {
const it = stack[i];
const route = it.route;
const config = {};
try {
this.convertStringObjectToObject(route.schema?.query, state.query);
config.query = route.schema?.query ? this.alepha.codec.decode(route.schema.query, coerceObject(route.schema.query, state.query)) : {};
} catch (e) {
it.error = e instanceof Error ? e : new Error(String(e));
break;
}
try {
config.params = route.schema?.params ? this.alepha.codec.decode(route.schema.params, coerceObject(route.schema.params, state.params)) : {};
} catch (e) {
it.error = e instanceof Error ? e : new Error(String(e));
break;
}
it.config = { ...config };
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
if (JSON.stringify({
part: url(previous[i].part),
params: previous[i].config?.params ?? {}
}) === JSON.stringify({
part: url(route.path),
params: config.params ?? {}
})) {
it.props = previous[i].props;
it.error = previous[i].error;
it.cache = true;
context = {
...context,
...it.props
};
continue;
}
forceRefresh = true;
}
if (route.redirect) return { redirect: route.redirect };
const middleware = this.alepha.isBrowser() ? (route.use ?? []).filter((m) => m[OPTIONS]?.name !== "$cache") : [];
if (!route.loader && middleware.length === 0) continue;
try {
const args = Object.create(state);
Object.assign(args, config, context);
let reached = false;
const terminal = async (a) => {
reached = true;
return await route.loader?.(a) ?? {};
};
const props = middleware.length ? await new PipelineHandler(terminal, middleware).run(args) ?? {} : await terminal(args);
if (!reached) {
const user = this.alepha.store.get(currentUserAtom);
if (!user) {
const login = this.findRoute("login");
if (login?.match && !/[:*]/.test(login.match)) {
const back = encodeURIComponent(state.url.pathname + state.url.search);
return { redirect: `${login.match}?redirect=${back}` };
}
}
const denied = new AlephaError(user ? "You do not have permission to access this page." : "Authentication required.");
denied.status = user ? 403 : 401;
throw denied;
}
it.props = { ...props };
context = {
...context,
...props
};
} catch (e) {
if (e instanceof Redirection) return { redirect: e.redirect };
this.log.error("Page loader has failed", e);
it.error = e instanceof Error ? e : new Error(String(e));
break;
}
}
let acc = "";
for (let i = 0; i < stack.length; i++) {
const it = stack[i];
const props = it.props ?? {};
const params = { ...it.config?.params };
for (const key of Object.keys(params)) params[key] = String(params[key]);
acc += "/";
acc += it.route.path ? this.compile(it.route.path, params) : "";
const path = acc.replace(/\/+/, "/");
const localErrorHandler = this.getErrorHandler(it.route);
if (localErrorHandler) {
const onErrorParent = state.onError;
state.onError = (error, context) => {
const result = localErrorHandler(error, context);
if (result === void 0) return onErrorParent(error, context);
return result;
};
}
if (!it.error) try {
const element = await this.createElement(it.route, {
...it.route.props ? it.route.props() : {},
...props,
...context
}, state.url);
state.layers.push({
name: it.route.name,
props,
part: it.route.path,
config: it.config,
element: this.renderView(i + 1, path, element, it.route),
index: i + 1,
path,
route: it.route,
cache: it.cache
});
} catch (e) {
it.error = e instanceof Error ? e : new Error(String(e));
}
if (it.error) try {
let element = await state.onError(it.error, state);
if (element === void 0) throw it.error;
if (element instanceof Redirection) return { redirect: element.redirect };
if (element === null) element = this.renderError(it.error);
state.layers.push({
props,
error: it.error,
name: it.route.name,
part: it.route.path,
config: it.config,
element: this.renderView(i + 1, path, element, it.route),
index: i + 1,
path,
route: it.route,
cache: it.cache
});
break;
} catch (e) {
if (e instanceof Redirection) return { redirect: e.redirect };
throw e;
}
}
if (state.layers.length > 0 && !this.isSSR(route)) {
const rootLayer = state.layers[0];
rootLayer.element = createElement(ClientOnly, {}, rootLayer.element);
}
return { state };
}
getErrorHandler(route) {
if (route.errorHandler) return route.errorHandler;
let parent = route.parent;
while (parent) {
if (parent.errorHandler) return parent.errorHandler;
parent = parent.parent;
}
}
async createElement(page, props, targetUrl) {
if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
if (page.lazy) try {
return createElement((await page.lazy()).default, props);
} catch (error) {
if (this.alepha.isBrowser() && this.isChunkLoadError(error)) {
if (this.reloadAfterChunkError(targetUrl)) return;
}
throw error;
}
if (page.component) return createElement(page.component, props);
}
/**
* Detect chunk load errors caused by stale dynamic imports after a deployment.
* When new assets are deployed with different hashes, old chunk URLs return 404.
*/
isChunkLoadError(error) {
if (!(error instanceof Error)) return false;
const msg = error.message;
return /Failed to fetch dynamically imported module/.test(msg) || /error loading dynamically imported module/i.test(msg) || /Unable to preload CSS/.test(msg) || /Importing a module script failed/.test(msg);
}
/**
* Navigate to the target URL to fetch updated assets after a chunk load failure.
* Uses sessionStorage to prevent infinite reload loops.
* Returns true if navigation was initiated.
*/
reloadAfterChunkError(url) {
const key = "alepha:chunk-reload";
const lastReload = sessionStorage.getItem(key);
const now = this.dateTimeProvider.nowMillis();
if (lastReload && now - Number(lastReload) < 1e4) {
this.log.error("Chunk load failed after recent reload, not retrying to avoid loop");
return false;
}
this.log.warn("Chunk load failed after deployment, reloading page");
sessionStorage.setItem(key, String(now));
window.location.assign(url ? url.pathname + url.search : window.location.href);
return true;
}
renderError(error) {
return createElement(ErrorViewer, {
error,
alepha: this.alepha
});
}
renderEmptyView() {
return createElement(NestedView_default, {});
}
href(page, params = {}) {
const found = this.pages.find((it) => it.name === page.options.name);
if (!found) throw new AlephaError(`Page ${page.options.name} not found`);
let url = found.path ?? "";
let parent = found.parent;
while (parent) {
url = `${parent.path ?? ""}/${url}`;
parent = parent.parent;
}
url = this.compile(url, params);
return url.replace(/\/\/+/g, "/") || "/";
}
compile(path, params = {}) {
for (const [key, value] of Object.entries(params)) path = path.replace(`:${key}`, value);
return path;
}
renderView(index, path, view, page) {
view ??= this.renderEmptyView();
return createElement(RouterLayerContext.Provider, { value: {
index,
path,
onError: this.getErrorHandler(page) ?? ((error) => this.renderError(error))
} }, view);
}
/**
* Resolve the effective `ssr` value for a route by walking up the parent
* chain. Returns the nearest explicit `ssr` value, defaulting to `true`.
*
* The decision is made at the leaf: a parent's `ssr` only acts as a default
* for descendants that did not set their own value.
*/
isSSR(route) {
let current = route;
while (current) {
if (typeof current.ssr === "boolean") return current.ssr;
current = current.parent;
}
return true;
}
map(pages, target) {
const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
const getChildrenFromParent = (it) => {
const children = [];
for (const page of pages) if (page.options.parent === it) children.push(page);
return children;
};
children.push(...getChildrenFromParent(target));
return {
...target.options,
name: target.name,
parent: void 0,
children: children.map((it) => this.map(pages, it))
};
}
add(entry) {
if (this.alepha.isReady()) throw new AlephaError("Router is already initialized");
entry.name ??= this.nextId();
const page = entry;
page.match = this.createMatch(page);
this.pages.push(page);
if (page.children) for (const child of page.children) {
child.parent = page;
this.add(child);
}
}
createMatch(page) {
let url = page.path ?? "/";
let target = page.parent;
while (target) {
url = `${target.path ?? ""}/${url}`;
target = target.parent;
}
let path = url.replace(/\/\/+/g, "/");
if (path.endsWith("/") && path !== "/") path = path.slice(0, -1);
return path;
}
nextId() {
this.nextIdCursor += 1;
return `P${this.nextIdCursor}`;
}
};
const isPageRoute = (it) => {
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
};
/**
* SSR Manifest atom containing all manifest data for SSR module preloading.
*
* This atom is populated at build time by embedding manifest data into the
* generated index.js. This approach is optimal for serverless deployments
* as it eliminates filesystem reads at runtime.
*
* The manifest includes:
* - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
* - client: Maps source files to their output info (file, imports, css)
*/
const ssrManifestAtom = $atom({
name: "alepha.react.ssr.manifest",
description: "SSR manifest for module preloading",
schema: z.object({
/**
* Base path for assets (from Vite's base config).
* Used to prefix asset URLs when serving from a subpath.
* @example "/devtools" or "/"
*/
base: z.string().optional(),
/**
* Preload manifest mapping short keys to source paths.
* Generated by viteAlephaSsrPreload plugin at build time.
*/
preload: z.record(z.string(), z.string()).optional(),
/**
* Client manifest mapping source files to their output information.
* Only includes fields actually used for preloading.
*/
client: z.record(z.string(), z.object({
file: z.string(),
isEntry: z.boolean().optional(),
imports: z.array(z.string()).optional(),
css: z.array(z.string()).optional()
})).optional(),
/**
* Dev mode head content.
* Contains pre-transformed scripts injected by Vite and plugins (React, etc.).
* Only set in dev mode via ViteDevServerProvider.
*/
devHead: z.string().optional(),
/**
* Auto-detected favicon path and MIME type.
* Format: "type:path" (e.g., "image/svg+xml:/favicon.svg").
* Set at build/dev time by scanning the public directory.
*/
favicon: z.string().optional()
}),
default: {}
});
//#endregion
//#region ../../src/react/router/providers/SSRManifestProvider.ts
/**
* Provider for SSR manifest data used for module preloading.
*
* The manifest is populated at build time by embedding data into the
* generated index.js via the ssrManifestAtom. This eliminates filesystem
* reads at runtime, making it optimal for serverless deployments.
*
* Manifest files are generated during `vite build`:
* - manifest.json (client manifest)
* - preload-manifest.json (from viteAlephaSsrPreload plugin)
*/
var SSRManifestProvider = class {
alepha = $inject(Alepha);
/**
* Get the manifest from the store at runtime.
* This ensures the manifest is available even when set after module load.
*/
get manifest() {
return this.alepha.store.get(ssrManifestAtom) ?? {};
}
/**
* Get the full manifest object.
*/
getManifest() {
return this.manifest;
}
/**
* Get the base path for assets (from Vite's base config).
* Returns empty string if base is "/" (default), otherwise returns the base path.
*/
get base() {
return this.manifest.base ?? "";
}
/**
* Get the preload manifest.
*/
get preloadManifest() {
return this.manifest.preload;
}
/**
* Get the client manifest.
*/
get clientManifest() {
return this.manifest.client;
}
/**
* Resolve a preload key to its source path.
*
* The key is a short hash injected by viteAlephaSsrPreload plugin,
* which maps to the full source path in the preload manifest.
*
* @param key - Short hash key (e.g., "a1b2c3d4")
* @returns Source path (e.g., "src/pages/UserDetail.tsx") or undefined
*/
resolvePreloadKey(key) {
return this.preloadManifest?.[key];
}
/**
* Get all chunks required for a source file, including transitive dependencies.
*
* Uses the client manifest to recursively resolve all imported chunks.
*
* @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
* @returns Array of chunk URLs to preload, or empty array if not found
*/
getChunks(sourcePath) {
if (!this.clientManifest) return [];
if (!this.findManifestEntry(sourcePath)) return [];
const chunks = /* @__PURE__ */ new Set();
const visited = /* @__PURE__ */ new Set();
this.collectChunksRecursive(sourcePath, chunks, visited);
return Array.from(chunks);
}
/**
* Find manifest entry for a source path, trying different extensions.
*/
findManifestEntry(sourcePath) {
if (!this.clientManifest) return void 0;
if (this.clientManifest[sourcePath]) return this.clientManifest[sourcePath];
const basePath = sourcePath.replace(/\.[^.]+$/, "");
for (const ext of [
".tsx",
".ts",
".jsx",
".js"
]) {
const pathWithExt = basePath + ext;
if (this.clientManifest[pathWithExt]) return this.clientManifest[pathWithExt];
}
}
/**
* Recursively collect all chunk URLs for a manifest entry.
*/
collectChunksRecursive(key, chunks, visited) {
if (visited.has(key)) return;
visited.add(key);
if (!this.clientManifest) return;
const entry = this.clientManifest[key];
if (!entry) return;
const base = this.base;
if (entry.file) chunks.add(`${base}/${entry.file}`);
if (entry.css) for (const css of entry.css) chunks.add(`${base}/${css}`);
if (entry.imports) for (const imp of entry.imports) {
if (imp === "index.html" || imp.endsWith(".html")) continue;
this.collectChunksRecursive(imp, chunks, visited);
}
}
/**
* Collect modulepreload links for a route and its parent chain.
*/
collectPreloadLinks(route) {
if (!this.isAvailable()) return [];
const preloadPaths = [];
let current = route;
while (current) {
const preloadKey = current[PAGE_PRELOAD_KEY];
if (preloadKey) {
const sourcePath = this.resolvePreloadKey(preloadKey);
if (sourcePath) preloadPaths.push(sourcePath);
}
current = current.parent;
}
if (preloadPaths.length === 0) return [];
return this.getChunksForMultiple(preloadPaths).map((href) => {
if (href.endsWith(".css")) return {
rel: "preload",
href,
as: "style",
crossorigin: ""
};
return {
rel: "modulepreload",
href
};
});
}
/**
* Get all chunks for multiple source files.
*
* @param sourcePaths - Array of source file paths
* @returns Deduplicated array of chunk URLs
*/
getChunksForMultiple(sourcePaths) {
const allChunks = /* @__PURE__ */ new Set();
for (const path of sourcePaths) {
const chunks = this.getChunks(path);
for (const chunk of chunks) allChunks.add(chunk);
}
return Array.from(allChunks);
}
/**
* Check if manifest is loaded and available.
*/
isAvailable() {
return this.clientManifest !== void 0;
}
/**
* Cached entry assets - computed once at first access.
*/
cachedEntryAssets = null;
/**
* Get the entry point assets (main entry.js and associated CSS files).
*
* These assets are always required for all pages and can be preloaded
* before page-specific loaders run.
*
* @returns Entry assets with js and css paths, or null if manifest unavailable
*/
getEntryAssets() {
if (this.cachedEntryAssets) return this.cachedEntryAssets;
if (!this.clientManifest) return null;
const base = this.base;
for (const [key, entry] of Object.entries(this.clientManifest)) if (entry.isEntry) {
this.cachedEntryAssets = {
js: `${base}/${entry.file}`,
css: entry.css?.map((css) => `${base}/${css}`) ?? []
};
return this.cachedEntryAssets;
}
return null;
}
/**
* Build preload link tags for entry assets.
*
* @returns Array of link objects ready to be rendered
*/
getEntryPreloadLinks() {
const assets = this.getEntryAssets();
if (!assets) return [];
const links = [];
for (const css of assets.css) links.push({
rel: "stylesheet",
href: css,
crossorigin: ""
});
if (assets.js) links.push({
rel: "modulepreload",
href: assets.js
});
return links;
}
};
//#endregion
//#region ../../src/react/router/providers/ReactPreloadProvider.ts
/**
* Adds HTTP Link headers for preloading entry assets.
*
* Benefits:
* - Early Hints (103): Servers can send preload hints before the full response
* - CDN optimization: Many CDNs use Link headers to optimize asset delivery
* - Browser prefetching: Browsers can start fetching resources earlier
*
* The Link header is computed once at first request and cached for reuse.
*/
var ReactPreloadProvider = class {
alepha = $inject(Alepha);
ssrManifest = $inject(SSRManifestProvider);
/**
* Cached Link header value - computed once, reused for all requests.
*/
cachedLinkHeader;
/**
* Build the Link header string from entry assets.
*
* Format: <url>; rel=preload; as=type, <url>; rel=modulepreload
*
* @returns Link header string or null if no assets
*/
buildLinkHeader() {
const assets = this.ssrManifest.getEntryAssets();
if (!assets) return null;
const links = [];
for (const css of assets.css) links.push(`<${css}>; rel=preload; as=style`);
if (assets.js) links.push(`<${assets.js}>; rel=modulepreload`);
return links.length > 0 ? links.join(", ") : null;
}
/**
* Get the cached Link header, computing it on first access.
*/
getLinkHeader() {
if (this.cachedLinkHeader === void 0) this.cachedLinkHeader = this.buildLinkHeader();
return this.cachedLinkHeader;
}
/**
* Add Link header to HTML responses for asset preloading.
*/
onResponse = $hook({
on: "server:onResponse",
priority: "first",
handler: ({ response }) => {
if (!response.headers["content-type"]?.includes("text/html")) return;
const linkHeader = this.getLinkHeader();
if (!linkHeader) return;
if (response.headers.link) response.headers.link = `${response.headers.link}, ${linkHeader}`;
else response.headers.link = linkHeader;
}
});
};
//#endregion
//#region ../../src/react/router/providers/ReactServerTemplateProvider.ts
/**
* Handles HTML streaming for SSR.
*
* Uses hardcoded HTML structure - all customization via $head primitive.
* Pre-encodes static parts as Uint8Array for zero-copy streaming.
*/
var ReactServerTemplateProvider = class {
log = $logger();
alepha = $inject(Alepha);
/**
* Shared TextEncoder - reused across all requests.
*/
encoder = new TextEncoder();
/**
* Pre-encoded static HTML parts for zero-copy streaming.
*/
SLOTS = {
DOCTYPE: this.encoder.encode("<!DOCTYPE html>\n"),
HTML_OPEN: this.encoder.encode("<html"),
HTML_CLOSE: this.encoder.encode(">\n"),
HEAD_OPEN: this.encoder.encode("<head>"),
HEAD_CLOSE: this.encoder.encode("</head>\n"),
BODY_OPEN: this.encoder.encode("<body"),
BODY_CLOSE: this.encoder.encode(">\n"),
ROOT_OPEN: this.encoder.encode("<div id=\"root\">"),
ROOT_CLOSE: this.encoder.encode("</div>\n"),
BODY_HTML_CLOSE: this.encoder.encode("</body>\n</html>"),
HYDRATION_PREFIX: this.encoder.encode("<script id=\"__ssr\" type=\"application/json\">"),
HYDRATION_SUFFIX: this.encoder.encode("<\/script>")
};
/**
* Early head content (charset, viewport, entry assets).
* Set once during configuration, reused for all requests.
*/
earlyHeadContent = "";
/**
* Root element ID for React mounting.
*/
rootId = "root";
/**
* Regex for extracting root div content from HTML.
*/
rootDivRegex = new RegExp(`<div[^>]*\\s+id=["']${this.rootId}["'][^>]*>([\\s\\S]*?)<\\/div>`, "i");
/**
* Extract content inside the root div from HTML.
*/
extractRootContent(html) {
return html.match(this.rootDivRegex)?.[1];
}
/**
* Set early head content (charset, viewport, entry assets).
* Called once during server configuration.
*/
setEarlyHeadContent(entryAssets, globalHead) {
const charset = globalHead?.charset ?? "UTF-8";
const viewport = globalHead?.viewport ?? "width=device-width, initial-scale=1";
this.earlyHeadContent = `<meta charset="${this.escapeHtml(charset)}">\n<meta name="viewport" content="${this.escapeHtml(viewport)}">\n` + entryAssets;
}
/**
* Render attributes record to HTML string.
*/
renderAttributes(attrs) {
if (!attrs) return "";
const entries = Object.entries(attrs);
if (entries.lengt