@alepha/react 0.14.4 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +10 -0
  2. package/dist/auth/index.browser.js +603 -242
  3. package/dist/auth/index.browser.js.map +1 -1
  4. package/dist/auth/index.d.ts +2 -2
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/index.js +1317 -952
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/core/index.d.ts +17 -17
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +20 -20
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/form/index.d.ts +9 -10
  13. package/dist/form/index.d.ts.map +1 -1
  14. package/dist/form/index.js +15 -15
  15. package/dist/form/index.js.map +1 -1
  16. package/dist/head/index.browser.js +20 -0
  17. package/dist/head/index.browser.js.map +1 -1
  18. package/dist/head/index.d.ts +62 -64
  19. package/dist/head/index.d.ts.map +1 -1
  20. package/dist/head/index.js +20 -0
  21. package/dist/head/index.js.map +1 -1
  22. package/dist/i18n/index.d.ts +9 -9
  23. package/dist/i18n/index.d.ts.map +1 -1
  24. package/dist/i18n/index.js.map +1 -1
  25. package/dist/router/index.browser.js +605 -244
  26. package/dist/router/index.browser.js.map +1 -1
  27. package/dist/router/index.d.ts +100 -111
  28. package/dist/router/index.d.ts.map +1 -1
  29. package/dist/router/index.js +1317 -952
  30. package/dist/router/index.js.map +1 -1
  31. package/dist/websocket/index.d.ts +0 -1
  32. package/dist/websocket/index.d.ts.map +1 -1
  33. package/package.json +6 -6
  34. package/src/auth/__tests__/$auth.spec.ts +164 -150
  35. package/src/auth/index.ts +9 -3
  36. package/src/auth/services/ReactAuth.ts +15 -5
  37. package/src/core/hooks/useAction.ts +1 -2
  38. package/src/core/index.ts +4 -4
  39. package/src/form/errors/FormValidationError.ts +4 -6
  40. package/src/form/hooks/useFormState.ts +1 -1
  41. package/src/form/index.ts +1 -1
  42. package/src/form/services/FormModel.ts +31 -25
  43. package/src/head/helpers/SeoExpander.ts +2 -1
  44. package/src/head/hooks/useHead.spec.tsx +2 -2
  45. package/src/head/index.browser.ts +2 -2
  46. package/src/head/index.ts +4 -4
  47. package/src/head/interfaces/Head.ts +15 -3
  48. package/src/head/primitives/$head.ts +2 -5
  49. package/src/head/providers/BrowserHeadProvider.ts +55 -0
  50. package/src/head/providers/HeadProvider.ts +4 -1
  51. package/src/i18n/__tests__/integration.spec.tsx +1 -1
  52. package/src/i18n/components/Localize.spec.tsx +2 -2
  53. package/src/i18n/hooks/useI18n.browser.spec.tsx +2 -2
  54. package/src/i18n/index.ts +1 -1
  55. package/src/i18n/primitives/$dictionary.ts +1 -1
  56. package/src/i18n/providers/I18nProvider.spec.ts +1 -1
  57. package/src/i18n/providers/I18nProvider.ts +1 -1
  58. package/src/router/__tests__/page-head-browser.browser.spec.ts +5 -1
  59. package/src/router/__tests__/page-head.spec.ts +11 -7
  60. package/src/router/__tests__/seo-head.spec.ts +7 -3
  61. package/src/router/atoms/ssrManifestAtom.ts +2 -11
  62. package/src/router/components/ErrorViewer.tsx +626 -167
  63. package/src/router/components/Link.tsx +4 -2
  64. package/src/router/components/NestedView.tsx +7 -9
  65. package/src/router/components/NotFound.tsx +2 -2
  66. package/src/router/hooks/useQueryParams.ts +1 -1
  67. package/src/router/hooks/useRouter.ts +1 -1
  68. package/src/router/hooks/useRouterState.ts +1 -1
  69. package/src/router/index.browser.ts +10 -11
  70. package/src/router/index.shared.ts +7 -7
  71. package/src/router/index.ts +10 -7
  72. package/src/router/primitives/$page.browser.spec.tsx +6 -1
  73. package/src/router/primitives/$page.spec.tsx +7 -1
  74. package/src/router/primitives/$page.ts +5 -9
  75. package/src/router/providers/ReactBrowserProvider.ts +17 -6
  76. package/src/router/providers/ReactBrowserRouterProvider.ts +1 -1
  77. package/src/router/providers/ReactPageProvider.ts +4 -3
  78. package/src/router/providers/ReactServerProvider.ts +32 -50
  79. package/src/router/providers/ReactServerTemplateProvider.ts +336 -155
  80. package/src/router/providers/SSRManifestProvider.ts +17 -60
  81. package/src/router/services/ReactPageService.ts +4 -1
  82. package/src/router/services/ReactRouter.ts +6 -5
@@ -1,19 +1,19 @@
1
1
  import { AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, useAlepha, useEvents, useStore } from "@alepha/react";
2
2
  import { $atom, $env, $hook, $inject, $module, $use, Alepha, AlephaError, KIND, Primitive, createPrimitive, t } from "alepha";
3
+ import { $auth, AlephaServerAuth, alephaServerAuthRoutes, tokenResponseSchema, userinfoResponseSchema } from "alepha/server/auth";
4
+ import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "alepha/server/links";
3
5
  import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
6
+ import { AlephaServer, HttpClient, ServerProvider, ServerRouterProvider } from "alepha/server";
7
+ import { AlephaServerCache } from "alepha/server/cache";
4
8
  import { $logger } from "alepha/logger";
5
- import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "alepha/server/links";
6
- import { BrowserHeadProvider, ServerHeadProvider } from "@alepha/react/head";
7
- import { RouterProvider } from "alepha/router";
8
- import { StrictMode, createContext, createElement, memo, use, useRef, useState } from "react";
9
+ import { StrictMode, createContext, createElement, memo, use, useEffect, useRef, useState } from "react";
9
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
- import { AlephaServer, HttpClient, ServerProvider, ServerRouterProvider, ServerTimingProvider } from "alepha/server";
11
11
  import { join } from "node:path";
12
+ import { BrowserHeadProvider, ServerHeadProvider } from "@alepha/react/head";
12
13
  import { FileSystemProvider } from "alepha/file";
13
14
  import { ServerStaticProvider } from "alepha/server/static";
14
- import { renderToReadableStream } from "react-dom/server";
15
- import { AlephaServerCache } from "alepha/server/cache";
16
- import { $auth, AlephaServerAuth, alephaServerAuthRoutes, tokenResponseSchema, userinfoResponseSchema } from "alepha/server/auth";
15
+ import { renderToReadableStream, renderToString } from "react-dom/server";
16
+ import { RouterProvider } from "alepha/router";
17
17
 
18
18
  //#region ../../src/auth/providers/ReactAuthProvider.ts
19
19
  var ReactAuthProvider = class {
@@ -30,6 +30,15 @@ var ReactAuthProvider = class {
30
30
  });
31
31
  };
32
32
 
33
+ //#endregion
34
+ //#region ../../src/router/constants/PAGE_PRELOAD_KEY.ts
35
+ /**
36
+ * Symbol key for SSR module preloading path.
37
+ * Using Symbol.for() allows the Vite plugin to inject this at build time.
38
+ * @internal
39
+ */
40
+ const PAGE_PRELOAD_KEY = Symbol.for("alepha.page.preload");
41
+
33
42
  //#endregion
34
43
  //#region ../../src/router/services/ReactPageService.ts
35
44
  /**
@@ -44,15 +53,6 @@ var ReactPageService = class {
44
53
  }
45
54
  };
46
55
 
47
- //#endregion
48
- //#region ../../src/router/constants/PAGE_PRELOAD_KEY.ts
49
- /**
50
- * Symbol key for SSR module preloading path.
51
- * Using Symbol.for() allows the Vite plugin to inject this at build time.
52
- * @internal
53
- */
54
- const PAGE_PRELOAD_KEY = Symbol.for("alepha.page.preload");
55
-
56
56
  //#endregion
57
57
  //#region ../../src/router/primitives/$page.ts
58
58
  /**
@@ -178,72 +178,179 @@ var PagePrimitive = class extends Primitive {
178
178
  };
179
179
  $page[KIND] = PagePrimitive;
180
180
 
181
- //#endregion
182
- //#region ../../src/router/components/NotFound.tsx
183
- /**
184
- * Default 404 Not Found page component.
185
- */
186
- const NotFound = (props) => /* @__PURE__ */ jsxs("div", {
187
- style: {
188
- width: "100%",
189
- minHeight: "90vh",
190
- boxSizing: "border-box",
191
- display: "flex",
192
- flexDirection: "column",
193
- justifyContent: "center",
194
- alignItems: "center",
195
- textAlign: "center",
196
- fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
197
- padding: "2rem",
198
- ...props.style
199
- },
200
- children: [/* @__PURE__ */ jsx("div", {
201
- style: {
202
- fontSize: "6rem",
203
- fontWeight: 200,
204
- lineHeight: 1
205
- },
206
- children: "404"
207
- }), /* @__PURE__ */ jsx("div", {
208
- style: {
209
- fontSize: "0.875rem",
210
- marginTop: "1rem",
211
- opacity: .6
212
- },
213
- children: "Page not found"
214
- })]
215
- });
216
- var NotFound_default = NotFound;
217
-
218
181
  //#endregion
219
182
  //#region ../../src/router/components/ErrorViewer.tsx
183
+ const isBrowser = typeof window !== "undefined";
220
184
  /**
221
- * Error viewer component that displays error details in development mode
185
+ * Error viewer component - Terminal/brutalist aesthetic
222
186
  */
223
187
  const ErrorViewer = ({ error, alepha }) => {
224
188
  const [expanded, setExpanded] = useState(false);
225
- if (alepha.isProduction()) return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
189
+ const [showNodeModules, setShowNodeModules] = useState(false);
190
+ const [visible, setVisible] = useState(false);
191
+ const containerRef = useRef(null);
192
+ const isProduction = alepha.isProduction();
193
+ useEffect(() => {
194
+ const timer = setTimeout(() => setVisible(true), 10);
195
+ return () => clearTimeout(timer);
196
+ }, []);
197
+ useEffect(() => {
198
+ if (!isBrowser) return;
199
+ const handler = (e) => {
200
+ if (e.key === "c" && !e.metaKey && !e.ctrlKey) copyToClipboard(error.stack || error.message);
201
+ };
202
+ window.addEventListener("keydown", handler);
203
+ return () => window.removeEventListener("keydown", handler);
204
+ }, [error]);
205
+ if (isProduction) return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
226
206
  const frames = parseStackTrace(error.stack);
227
- const visibleFrames = expanded ? frames : frames.slice(0, 6);
228
- const hiddenCount = frames.length - 6;
229
- return /* @__PURE__ */ jsx("div", {
230
- style: styles.overlay,
231
- children: /* @__PURE__ */ jsxs("div", {
232
- style: styles.container,
233
- children: [/* @__PURE__ */ jsx(Header, { error }), /* @__PURE__ */ jsx(StackTraceSection, {
234
- frames,
235
- visibleFrames,
236
- expanded,
237
- hiddenCount,
238
- onToggle: () => setExpanded(!expanded)
239
- })]
240
- })
207
+ const appFrames = frames.filter((f) => !f.isNodeModules);
208
+ const nodeModulesFrames = frames.filter((f) => f.isNodeModules);
209
+ const visibleAppFrames = expanded ? appFrames : appFrames.slice(0, 5);
210
+ const hiddenAppCount = appFrames.length - 5;
211
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
212
+ hour12: false,
213
+ hour: "2-digit",
214
+ minute: "2-digit",
215
+ second: "2-digit"
216
+ });
217
+ return /* @__PURE__ */ jsxs("div", {
218
+ ref: containerRef,
219
+ style: {
220
+ ...styles.overlay,
221
+ opacity: visible ? 1 : 0
222
+ },
223
+ role: "alertdialog",
224
+ "aria-modal": "true",
225
+ "aria-labelledby": "error-viewer-title",
226
+ children: [/* @__PURE__ */ jsx("div", {
227
+ style: styles.scanlines,
228
+ "aria-hidden": "true"
229
+ }), /* @__PURE__ */ jsxs("div", {
230
+ style: {
231
+ ...styles.container,
232
+ transform: visible ? "translateY(0)" : "translateY(-20px)",
233
+ opacity: visible ? 1 : 0
234
+ },
235
+ children: [
236
+ /* @__PURE__ */ jsxs("div", {
237
+ style: styles.terminalBar,
238
+ children: [
239
+ /* @__PURE__ */ jsxs("div", {
240
+ style: styles.terminalDots,
241
+ children: [
242
+ /* @__PURE__ */ jsx("span", { style: {
243
+ ...styles.dot,
244
+ backgroundColor: "#ff5f57"
245
+ } }),
246
+ /* @__PURE__ */ jsx("span", { style: {
247
+ ...styles.dot,
248
+ backgroundColor: "#febc2e"
249
+ } }),
250
+ /* @__PURE__ */ jsx("span", { style: {
251
+ ...styles.dot,
252
+ backgroundColor: "#28c840"
253
+ } })
254
+ ]
255
+ }),
256
+ /* @__PURE__ */ jsx("div", {
257
+ style: styles.terminalTitle,
258
+ children: /* @__PURE__ */ jsxs("span", {
259
+ style: styles.terminalTitleText,
260
+ children: ["error — ", timestamp]
261
+ })
262
+ }),
263
+ /* @__PURE__ */ jsxs("div", {
264
+ style: styles.terminalActions,
265
+ children: [/* @__PURE__ */ jsx("kbd", {
266
+ style: styles.kbd,
267
+ children: "C"
268
+ }), /* @__PURE__ */ jsx("span", {
269
+ style: styles.kbdLabel,
270
+ children: "copy"
271
+ })]
272
+ })
273
+ ]
274
+ }),
275
+ /* @__PURE__ */ jsx(Header, { error }),
276
+ /* @__PURE__ */ jsxs("div", {
277
+ style: styles.stackSection,
278
+ children: [/* @__PURE__ */ jsxs("div", {
279
+ style: styles.stackHeader,
280
+ children: [/* @__PURE__ */ jsx("span", {
281
+ style: styles.stackHeaderText,
282
+ children: "STACK TRACE"
283
+ }), /* @__PURE__ */ jsxs("span", {
284
+ style: styles.stackCount,
285
+ children: [
286
+ appFrames.length,
287
+ " frames",
288
+ nodeModulesFrames.length > 0 && ` · ${nodeModulesFrames.length} in node_modules`
289
+ ]
290
+ })]
291
+ }), /* @__PURE__ */ jsxs("div", {
292
+ style: styles.frameList,
293
+ children: [
294
+ visibleAppFrames.map((frame, i) => /* @__PURE__ */ jsx(StackFrameRow, {
295
+ frame,
296
+ index: i
297
+ }, `${frame.raw}-${i}`)),
298
+ hiddenAppCount > 0 && !expanded && /* @__PURE__ */ jsx(ExpandButton, {
299
+ onClick: () => setExpanded(true),
300
+ label: `Show ${hiddenAppCount} more frames`
301
+ }),
302
+ expanded && hiddenAppCount > 0 && /* @__PURE__ */ jsx(ExpandButton, {
303
+ onClick: () => setExpanded(false),
304
+ label: "Collapse"
305
+ }),
306
+ nodeModulesFrames.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
307
+ type: "button",
308
+ onClick: () => setShowNodeModules(!showNodeModules),
309
+ style: styles.nodeModulesToggle,
310
+ children: [
311
+ /* @__PURE__ */ jsx("span", {
312
+ style: styles.nodeModulesIcon,
313
+ children: showNodeModules ? "▼" : "▶"
314
+ }),
315
+ /* @__PURE__ */ jsx("span", {
316
+ style: styles.nodeModulesLabel,
317
+ children: "node_modules"
318
+ }),
319
+ /* @__PURE__ */ jsx("span", {
320
+ style: styles.nodeModulesCount,
321
+ children: nodeModulesFrames.length
322
+ })
323
+ ]
324
+ }), showNodeModules && /* @__PURE__ */ jsx("div", {
325
+ style: styles.nodeModulesFrames,
326
+ children: nodeModulesFrames.map((frame, i) => /* @__PURE__ */ jsx(StackFrameRow, {
327
+ frame,
328
+ index: appFrames.length + i,
329
+ dimmed: true
330
+ }, `nm-${frame.raw}-${i}`))
331
+ })] })
332
+ ]
333
+ })]
334
+ }),
335
+ /* @__PURE__ */ jsx("div", {
336
+ style: styles.footer,
337
+ children: /* @__PURE__ */ jsxs("span", {
338
+ style: styles.footerText,
339
+ children: [
340
+ "Press ",
341
+ /* @__PURE__ */ jsx("kbd", {
342
+ style: styles.kbdInline,
343
+ children: "C"
344
+ }),
345
+ " to copy stack trace"
346
+ ]
347
+ })
348
+ })
349
+ ]
350
+ })]
241
351
  });
242
352
  };
243
353
  var ErrorViewer_default = ErrorViewer;
244
- /**
245
- * Parse stack trace string into structured frames
246
- */
247
354
  function parseStackTrace(stack) {
248
355
  if (!stack) return [];
249
356
  const lines = stack.split("\n").slice(1);
@@ -256,17 +363,16 @@ function parseStackTrace(stack) {
256
363
  }
257
364
  return frames;
258
365
  }
259
- /**
260
- * Parse a single stack trace line into a structured frame
261
- */
262
366
  function parseStackLine(line) {
367
+ const isNodeModules = line.includes("node_modules") || line.includes("node:");
263
368
  const withFn = line.match(/^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/);
264
369
  if (withFn) return {
265
370
  fn: withFn[1],
266
371
  file: withFn[2],
267
372
  line: withFn[3],
268
373
  col: withFn[4],
269
- raw: line
374
+ raw: line,
375
+ isNodeModules
270
376
  };
271
377
  const withoutFn = line.match(/^at\s+(.+):(\d+):(\d+)$/);
272
378
  if (withoutFn) return {
@@ -274,236 +380,412 @@ function parseStackLine(line) {
274
380
  file: withoutFn[1],
275
381
  line: withoutFn[2],
276
382
  col: withoutFn[3],
277
- raw: line
383
+ raw: line,
384
+ isNodeModules
278
385
  };
279
386
  return {
280
387
  fn: "",
281
388
  file: line.replace(/^at\s+/, ""),
282
389
  line: "",
283
390
  col: "",
284
- raw: line
391
+ raw: line,
392
+ isNodeModules
285
393
  };
286
394
  }
287
- /**
288
- * Copy text to clipboard
289
- */
290
395
  function copyToClipboard(text) {
291
- navigator.clipboard.writeText(text).catch((err) => {
292
- console.error("Clipboard error:", err);
293
- });
396
+ if (!isBrowser || !navigator.clipboard) return Promise.resolve(false);
397
+ return navigator.clipboard.writeText(text).then(() => true).catch(() => false);
294
398
  }
295
399
  /**
296
- * Header section with error type and message
400
+ * Header with error badge and message
297
401
  */
298
402
  function Header({ error }) {
299
403
  const [copied, setCopied] = useState(false);
300
- const handleCopy = () => {
301
- copyToClipboard(error.stack || error.message);
302
- setCopied(true);
303
- setTimeout(() => setCopied(false), 2e3);
404
+ const [hovered, setHovered] = useState(false);
405
+ useEffect(() => {
406
+ if (!copied) return;
407
+ const timer = setTimeout(() => setCopied(false), 2e3);
408
+ return () => clearTimeout(timer);
409
+ }, [copied]);
410
+ const handleCopy = async () => {
411
+ if (await copyToClipboard(error.stack || error.message)) setCopied(true);
304
412
  };
305
413
  return /* @__PURE__ */ jsxs("div", {
306
414
  style: styles.header,
307
415
  children: [/* @__PURE__ */ jsxs("div", {
308
- style: styles.headerTop,
309
- children: [/* @__PURE__ */ jsx("div", {
310
- style: styles.badge,
311
- children: error.name
416
+ style: styles.headerRow,
417
+ children: [/* @__PURE__ */ jsxs("div", {
418
+ style: styles.errorIndicator,
419
+ children: [/* @__PURE__ */ jsx("div", { style: styles.errorGlow }), /* @__PURE__ */ jsx("div", {
420
+ style: styles.errorBadge,
421
+ children: error.name
422
+ })]
312
423
  }), /* @__PURE__ */ jsx("button", {
313
424
  type: "button",
314
425
  onClick: handleCopy,
315
- style: styles.copyBtn,
316
- children: copied ? "Copied" : "Copy Stack"
426
+ onMouseEnter: () => setHovered(true),
427
+ onMouseLeave: () => setHovered(false),
428
+ style: {
429
+ ...styles.copyBtn,
430
+ ...hovered ? styles.copyBtnHover : {}
431
+ },
432
+ children: copied ? "✓ Copied" : "Copy"
317
433
  })]
318
434
  }), /* @__PURE__ */ jsx("h1", {
435
+ id: "error-viewer-title",
319
436
  style: styles.message,
320
437
  children: error.message
321
438
  })]
322
439
  });
323
440
  }
324
441
  /**
325
- * Stack trace section with expandable frames
442
+ * Single stack frame row
326
443
  */
327
- function StackTraceSection({ frames, visibleFrames, expanded, hiddenCount, onToggle }) {
328
- if (frames.length === 0) return null;
329
- return /* @__PURE__ */ jsxs("div", {
330
- style: styles.stackSection,
331
- children: [/* @__PURE__ */ jsx("div", {
332
- style: styles.stackHeader,
333
- children: "Call Stack"
444
+ function StackFrameRow({ frame, index, dimmed = false }) {
445
+ const [hovered, setHovered] = useState(false);
446
+ const isFirst = index === 0 && !dimmed;
447
+ const fileName = frame.file.split("/").pop() || frame.file;
448
+ const dirPath = frame.file.substring(0, frame.file.length - fileName.length);
449
+ const vsCodeLink = frame.file && frame.line ? `vscode://file${frame.file}:${frame.line}:${frame.col || 1}` : null;
450
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
451
+ style: {
452
+ ...styles.frameIndex,
453
+ color: isFirst ? "#ff6b6b" : dimmed ? "#555" : "#666"
454
+ },
455
+ children: String(index + 1).padStart(2, "0")
456
+ }), /* @__PURE__ */ jsxs("div", {
457
+ style: styles.frameContent,
458
+ children: [frame.fn && /* @__PURE__ */ jsx("div", {
459
+ style: {
460
+ ...styles.fnName,
461
+ color: dimmed ? "#888" : "#f0f0f0"
462
+ },
463
+ children: formatFunctionName(frame.fn)
334
464
  }), /* @__PURE__ */ jsxs("div", {
335
- style: styles.frameList,
465
+ style: styles.filePath,
336
466
  children: [
337
- visibleFrames.map((frame, i) => /* @__PURE__ */ jsx(StackFrameRow, {
338
- frame,
339
- index: i
340
- }, i)),
341
- !expanded && hiddenCount > 0 && /* @__PURE__ */ jsxs("button", {
342
- type: "button",
343
- onClick: onToggle,
344
- style: styles.expandBtn,
467
+ /* @__PURE__ */ jsx("span", {
468
+ style: {
469
+ ...styles.dirPath,
470
+ opacity: dimmed ? .6 : .8
471
+ },
472
+ children: dirPath
473
+ }),
474
+ /* @__PURE__ */ jsx("span", {
475
+ style: {
476
+ ...styles.fileName,
477
+ color: dimmed ? "#5a9aba" : "#7cc4eb"
478
+ },
479
+ children: fileName
480
+ }),
481
+ frame.line && /* @__PURE__ */ jsxs("span", {
482
+ style: {
483
+ ...styles.lineCol,
484
+ color: dimmed ? "#9a8a40" : "#e5b83a"
485
+ },
345
486
  children: [
346
- "Show ",
347
- hiddenCount,
348
- " more frames"
487
+ ":",
488
+ frame.line,
489
+ frame.col && `:${frame.col}`
349
490
  ]
350
- }),
351
- expanded && hiddenCount > 0 && /* @__PURE__ */ jsx("button", {
352
- type: "button",
353
- onClick: onToggle,
354
- style: styles.expandBtn,
355
- children: "Show less"
356
491
  })
357
492
  ]
358
493
  })]
494
+ })] });
495
+ const rowStyles = {
496
+ ...styles.frame,
497
+ ...isFirst ? styles.frameFirst : {},
498
+ backgroundColor: hovered ? "rgba(255,255,255,0.03)" : "transparent"
499
+ };
500
+ if (vsCodeLink && isBrowser) return /* @__PURE__ */ jsx("a", {
501
+ href: vsCodeLink,
502
+ style: {
503
+ ...rowStyles,
504
+ textDecoration: "none"
505
+ },
506
+ onMouseEnter: () => setHovered(true),
507
+ onMouseLeave: () => setHovered(false),
508
+ children: content
509
+ });
510
+ return /* @__PURE__ */ jsx("div", {
511
+ style: rowStyles,
512
+ children: content
359
513
  });
360
514
  }
361
515
  /**
362
- * Single stack frame row
516
+ * Format function name with syntax highlighting
363
517
  */
364
- function StackFrameRow({ frame, index }) {
365
- const isFirst = index === 0;
366
- const fileName = frame.file.split("/").pop() || frame.file;
367
- const dirPath = frame.file.substring(0, frame.file.length - fileName.length);
368
- return /* @__PURE__ */ jsxs("div", {
518
+ function formatFunctionName(fn) {
519
+ const asyncMatch = fn.match(/^(async\s+)?(.+)$/);
520
+ if (asyncMatch?.[1]) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
521
+ style: { color: "#c678dd" },
522
+ children: "async "
523
+ }), /* @__PURE__ */ jsx("span", { children: asyncMatch[2] })] });
524
+ const methodMatch = fn.match(/^(.+)\.([^.]+)$/);
525
+ if (methodMatch) return /* @__PURE__ */ jsxs(Fragment, { children: [
526
+ /* @__PURE__ */ jsx("span", {
527
+ style: { color: "#e5c07b" },
528
+ children: methodMatch[1]
529
+ }),
530
+ /* @__PURE__ */ jsx("span", {
531
+ style: { color: "#666" },
532
+ children: "."
533
+ }),
534
+ /* @__PURE__ */ jsx("span", { children: methodMatch[2] })
535
+ ] });
536
+ return fn;
537
+ }
538
+ /**
539
+ * Expand/collapse button
540
+ */
541
+ function ExpandButton({ onClick, label }) {
542
+ const [hovered, setHovered] = useState(false);
543
+ return /* @__PURE__ */ jsx("button", {
544
+ type: "button",
545
+ onClick,
546
+ onMouseEnter: () => setHovered(true),
547
+ onMouseLeave: () => setHovered(false),
369
548
  style: {
370
- ...styles.frame,
371
- ...isFirst ? styles.frameFirst : {}
549
+ ...styles.expandBtn,
550
+ backgroundColor: hovered ? "rgba(255,255,255,0.05)" : "transparent",
551
+ color: hovered ? "#aaa" : "#777"
372
552
  },
373
- children: [/* @__PURE__ */ jsx("div", {
374
- style: styles.frameIndex,
375
- children: index + 1
376
- }), /* @__PURE__ */ jsxs("div", {
377
- style: styles.frameContent,
378
- children: [frame.fn && /* @__PURE__ */ jsx("div", {
379
- style: styles.fnName,
380
- children: frame.fn
381
- }), /* @__PURE__ */ jsxs("div", {
382
- style: styles.filePath,
383
- children: [
384
- /* @__PURE__ */ jsx("span", {
385
- style: styles.dirPath,
386
- children: dirPath
387
- }),
388
- /* @__PURE__ */ jsx("span", {
389
- style: styles.fileName,
390
- children: fileName
391
- }),
392
- frame.line && /* @__PURE__ */ jsxs("span", {
393
- style: styles.lineCol,
394
- children: [
395
- ":",
396
- frame.line,
397
- ":",
398
- frame.col
399
- ]
400
- })
401
- ]
402
- })]
403
- })]
553
+ children: label
404
554
  });
405
555
  }
406
556
  /**
407
- * Production error view - minimal information
557
+ * Production error view - minimal, user-friendly
408
558
  */
409
559
  function ErrorViewerProduction() {
560
+ const [hovered, setHovered] = useState(false);
561
+ const handleReload = () => {
562
+ if (isBrowser) window.location.reload();
563
+ };
410
564
  return /* @__PURE__ */ jsx("div", {
411
565
  style: styles.overlay,
566
+ role: "alertdialog",
567
+ "aria-modal": "true",
412
568
  children: /* @__PURE__ */ jsxs("div", {
413
569
  style: styles.prodContainer,
414
570
  children: [
415
571
  /* @__PURE__ */ jsx("div", {
416
572
  style: styles.prodIcon,
417
- children: "!"
573
+ children: /* @__PURE__ */ jsxs("svg", {
574
+ width: "32",
575
+ height: "32",
576
+ viewBox: "0 0 24 24",
577
+ fill: "none",
578
+ stroke: "currentColor",
579
+ strokeWidth: "2",
580
+ children: [
581
+ /* @__PURE__ */ jsx("circle", {
582
+ cx: "12",
583
+ cy: "12",
584
+ r: "10"
585
+ }),
586
+ /* @__PURE__ */ jsx("line", {
587
+ x1: "12",
588
+ y1: "8",
589
+ x2: "12",
590
+ y2: "12"
591
+ }),
592
+ /* @__PURE__ */ jsx("line", {
593
+ x1: "12",
594
+ y1: "16",
595
+ x2: "12.01",
596
+ y2: "16"
597
+ })
598
+ ]
599
+ })
418
600
  }),
419
601
  /* @__PURE__ */ jsx("h1", {
420
602
  style: styles.prodTitle,
421
- children: "Application Error"
603
+ children: "Something went wrong"
422
604
  }),
423
605
  /* @__PURE__ */ jsx("p", {
424
606
  style: styles.prodMessage,
425
- children: "An unexpected error occurred. Please try again later."
607
+ children: "We encountered an unexpected error. Please try again."
426
608
  }),
427
609
  /* @__PURE__ */ jsx("button", {
428
610
  type: "button",
429
- onClick: () => window.location.reload(),
430
- style: styles.prodButton,
431
- children: "Reload Page"
611
+ onClick: handleReload,
612
+ onMouseEnter: () => setHovered(true),
613
+ onMouseLeave: () => setHovered(false),
614
+ style: {
615
+ ...styles.prodButton,
616
+ backgroundColor: hovered ? "#333" : "#222",
617
+ borderColor: hovered ? "#555" : "#444"
618
+ },
619
+ children: "Reload page"
432
620
  })
433
621
  ]
434
622
  })
435
623
  });
436
624
  }
625
+ const MONO_FONT = "ui-monospace, \"JetBrains Mono\", \"Fira Code\", SFMono-Regular, Menlo, Monaco, Consolas, monospace";
437
626
  const styles = {
438
627
  overlay: {
439
628
  position: "fixed",
440
629
  inset: 0,
441
- backgroundColor: "rgba(0, 0, 0, 0.8)",
630
+ backgroundColor: "rgba(0, 0, 0, 0.92)",
442
631
  display: "flex",
443
632
  alignItems: "flex-start",
444
633
  justifyContent: "center",
445
634
  padding: "40px 20px",
446
635
  overflow: "auto",
447
- fontFamily: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
448
- zIndex: 99999
636
+ fontFamily: MONO_FONT,
637
+ fontSize: "13px",
638
+ zIndex: 99999,
639
+ transition: "opacity 0.2s ease-out"
640
+ },
641
+ scanlines: {
642
+ position: "fixed",
643
+ inset: 0,
644
+ background: "repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px)",
645
+ pointerEvents: "none",
646
+ zIndex: 1e5
449
647
  },
450
648
  container: {
451
649
  width: "100%",
452
- maxWidth: "960px",
453
- backgroundColor: "#1a1a1a",
454
- borderRadius: "12px",
650
+ maxWidth: "900px",
651
+ backgroundColor: "#0d0d0d",
652
+ borderRadius: "8px",
455
653
  overflow: "hidden",
456
- boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)"
654
+ boxShadow: "0 0 0 1px #333, 0 25px 80px -12px rgba(0, 0, 0, 0.8)",
655
+ transition: "transform 0.3s ease-out, opacity 0.3s ease-out"
656
+ },
657
+ terminalBar: {
658
+ display: "flex",
659
+ alignItems: "center",
660
+ padding: "12px 16px",
661
+ backgroundColor: "#1a1a1a",
662
+ borderBottom: "1px solid #333"
663
+ },
664
+ terminalDots: {
665
+ display: "flex",
666
+ gap: "8px"
667
+ },
668
+ dot: {
669
+ width: "12px",
670
+ height: "12px",
671
+ borderRadius: "50%"
672
+ },
673
+ terminalTitle: {
674
+ flex: 1,
675
+ textAlign: "center"
676
+ },
677
+ terminalTitleText: {
678
+ color: "#777",
679
+ fontSize: "12px",
680
+ letterSpacing: "0.5px"
681
+ },
682
+ terminalActions: {
683
+ display: "flex",
684
+ alignItems: "center",
685
+ gap: "6px"
686
+ },
687
+ kbd: {
688
+ display: "inline-block",
689
+ padding: "2px 6px",
690
+ backgroundColor: "#2a2a2a",
691
+ borderRadius: "4px",
692
+ fontSize: "11px",
693
+ color: "#aaa",
694
+ border: "1px solid #444"
695
+ },
696
+ kbdInline: {
697
+ display: "inline-block",
698
+ padding: "1px 5px",
699
+ backgroundColor: "#222",
700
+ borderRadius: "3px",
701
+ fontSize: "11px",
702
+ color: "#888",
703
+ border: "1px solid #444",
704
+ marginLeft: "4px",
705
+ marginRight: "4px"
706
+ },
707
+ kbdLabel: {
708
+ color: "#777",
709
+ fontSize: "11px"
457
710
  },
458
711
  header: {
459
- padding: "24px 28px",
460
- borderBottom: "1px solid #333",
461
- background: "linear-gradient(to bottom, #1f1f1f, #1a1a1a)"
712
+ padding: "24px",
713
+ borderBottom: "1px solid #333"
462
714
  },
463
- headerTop: {
715
+ headerRow: {
464
716
  display: "flex",
465
717
  alignItems: "center",
466
718
  justifyContent: "space-between",
467
719
  marginBottom: "16px"
468
720
  },
469
- badge: {
721
+ errorIndicator: {
722
+ position: "relative",
723
+ display: "inline-flex"
724
+ },
725
+ errorGlow: {
726
+ position: "absolute",
727
+ inset: "-4px",
728
+ background: "radial-gradient(ellipse at center, rgba(255,80,80,0.3) 0%, transparent 70%)",
729
+ borderRadius: "12px",
730
+ filter: "blur(8px)"
731
+ },
732
+ errorBadge: {
733
+ position: "relative",
470
734
  display: "inline-block",
471
- padding: "6px 12px",
472
- backgroundColor: "#dc2626",
473
- color: "#fff",
735
+ padding: "6px 14px",
736
+ backgroundColor: "#3d1a1a",
737
+ color: "#ff7b7b",
474
738
  fontSize: "12px",
475
739
  fontWeight: 600,
476
740
  borderRadius: "6px",
477
- letterSpacing: "0.025em"
741
+ border: "1px solid #5a2828",
742
+ letterSpacing: "0.5px"
478
743
  },
479
744
  copyBtn: {
480
- padding: "8px 16px",
745
+ padding: "8px 14px",
481
746
  backgroundColor: "transparent",
482
747
  color: "#888",
483
- fontSize: "13px",
748
+ fontSize: "12px",
484
749
  fontWeight: 500,
485
- border: "1px solid #444",
750
+ borderWidth: "1px",
751
+ borderStyle: "solid",
752
+ borderColor: "#444",
486
753
  borderRadius: "6px",
487
754
  cursor: "pointer",
488
- transition: "all 0.15s"
755
+ transition: "all 0.15s",
756
+ fontFamily: MONO_FONT
757
+ },
758
+ copyBtnHover: {
759
+ backgroundColor: "#252525",
760
+ color: "#bbb",
761
+ borderColor: "#555"
489
762
  },
490
763
  message: {
491
764
  margin: 0,
492
- fontSize: "20px",
493
- fontWeight: 500,
494
- color: "#fff",
495
- lineHeight: 1.5,
496
- wordBreak: "break-word"
765
+ fontSize: "18px",
766
+ fontWeight: 400,
767
+ color: "#e8e8e8",
768
+ lineHeight: 1.6,
769
+ wordBreak: "break-word",
770
+ fontFamily: MONO_FONT
497
771
  },
498
- stackSection: { padding: "0" },
772
+ stackSection: { borderTop: "1px solid #2a2a2a" },
499
773
  stackHeader: {
500
- padding: "16px 28px",
501
- fontSize: "11px",
774
+ display: "flex",
775
+ alignItems: "center",
776
+ justifyContent: "space-between",
777
+ padding: "14px 24px",
778
+ borderBottom: "1px solid #2a2a2a"
779
+ },
780
+ stackHeaderText: {
781
+ fontSize: "10px",
502
782
  fontWeight: 600,
503
783
  color: "#666",
504
- textTransform: "uppercase",
505
- letterSpacing: "0.1em",
506
- borderBottom: "1px solid #2a2a2a"
784
+ letterSpacing: "1.5px"
785
+ },
786
+ stackCount: {
787
+ fontSize: "11px",
788
+ color: "#555"
507
789
  },
508
790
  frameList: {
509
791
  display: "flex",
@@ -512,92 +794,134 @@ const styles = {
512
794
  frame: {
513
795
  display: "flex",
514
796
  alignItems: "flex-start",
515
- padding: "14px 28px",
516
- borderBottom: "1px solid #252525",
517
- transition: "background-color 0.15s"
797
+ padding: "12px 24px",
798
+ borderBottom: "1px solid #222",
799
+ transition: "background-color 0.1s",
800
+ cursor: "pointer"
801
+ },
802
+ frameFirst: {
803
+ backgroundColor: "rgba(255, 80, 80, 0.08)",
804
+ borderLeft: "2px solid #ff6b6b"
518
805
  },
519
- frameFirst: { backgroundColor: "rgba(220, 38, 38, 0.1)" },
520
806
  frameIndex: {
521
807
  width: "28px",
522
808
  flexShrink: 0,
523
- fontSize: "12px",
809
+ fontSize: "11px",
524
810
  fontWeight: 500,
525
- color: "#555",
526
- fontFamily: "monospace"
811
+ fontFamily: MONO_FONT
527
812
  },
528
813
  frameContent: {
529
814
  flex: 1,
530
815
  minWidth: 0
531
816
  },
532
817
  fnName: {
533
- fontSize: "14px",
818
+ fontSize: "13px",
534
819
  fontWeight: 500,
535
- color: "#e5e5e5",
536
820
  marginBottom: "4px",
537
- fontFamily: "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace"
821
+ fontFamily: MONO_FONT
538
822
  },
539
823
  filePath: {
540
- fontSize: "13px",
824
+ fontSize: "12px",
541
825
  color: "#888",
542
- fontFamily: "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace",
826
+ fontFamily: MONO_FONT,
543
827
  wordBreak: "break-all"
544
828
  },
545
- dirPath: { color: "#555" },
546
- fileName: { color: "#0ea5e9" },
547
- lineCol: { color: "#eab308" },
829
+ dirPath: { color: "#666" },
830
+ fileName: { color: "#7cc4eb" },
831
+ lineCol: { color: "#e5b83a" },
548
832
  expandBtn: {
549
- padding: "16px 28px",
833
+ width: "100%",
834
+ padding: "14px 24px",
550
835
  backgroundColor: "transparent",
836
+ color: "#777",
837
+ fontSize: "12px",
838
+ fontWeight: 500,
839
+ border: "none",
840
+ borderTop: "1px solid #2a2a2a",
841
+ cursor: "pointer",
842
+ textAlign: "left",
843
+ transition: "all 0.15s",
844
+ fontFamily: MONO_FONT
845
+ },
846
+ nodeModulesToggle: {
847
+ display: "flex",
848
+ alignItems: "center",
849
+ gap: "10px",
850
+ width: "100%",
851
+ padding: "12px 24px",
852
+ backgroundColor: "#0a0a0a",
551
853
  color: "#666",
552
- fontSize: "13px",
854
+ fontSize: "11px",
553
855
  fontWeight: 500,
554
856
  border: "none",
555
- borderTop: "1px solid #252525",
857
+ borderTop: "1px solid #2a2a2a",
556
858
  cursor: "pointer",
557
859
  textAlign: "left",
558
- transition: "all 0.15s"
860
+ fontFamily: MONO_FONT
861
+ },
862
+ nodeModulesIcon: {
863
+ fontSize: "8px",
864
+ color: "#555"
865
+ },
866
+ nodeModulesLabel: {
867
+ flex: 1,
868
+ letterSpacing: "0.5px"
869
+ },
870
+ nodeModulesCount: { color: "#555" },
871
+ nodeModulesFrames: { backgroundColor: "#080808" },
872
+ footer: {
873
+ padding: "14px 24px",
874
+ borderTop: "1px solid #2a2a2a",
875
+ backgroundColor: "#0a0a0a"
876
+ },
877
+ footerText: {
878
+ fontSize: "11px",
879
+ color: "#555"
559
880
  },
560
881
  prodContainer: {
561
882
  textAlign: "center",
562
883
  padding: "60px 40px",
563
- backgroundColor: "#1a1a1a",
564
- borderRadius: "12px",
565
- maxWidth: "400px"
884
+ backgroundColor: "#0d0d0d",
885
+ borderRadius: "8px",
886
+ maxWidth: "400px",
887
+ border: "1px solid #333"
566
888
  },
567
889
  prodIcon: {
568
890
  width: "64px",
569
891
  height: "64px",
570
892
  margin: "0 auto 24px",
571
- backgroundColor: "#dc2626",
572
- borderRadius: "50%",
893
+ color: "#666",
573
894
  display: "flex",
574
895
  alignItems: "center",
575
- justifyContent: "center",
576
- fontSize: "32px",
577
- fontWeight: 700,
578
- color: "#fff"
896
+ justifyContent: "center"
579
897
  },
580
898
  prodTitle: {
581
899
  margin: "0 0 12px",
582
- fontSize: "24px",
583
- fontWeight: 600,
584
- color: "#fff"
900
+ fontSize: "18px",
901
+ fontWeight: 500,
902
+ color: "#f0f0f0",
903
+ fontFamily: MONO_FONT
585
904
  },
586
905
  prodMessage: {
587
906
  margin: "0 0 28px",
588
- fontSize: "15px",
907
+ fontSize: "13px",
589
908
  color: "#888",
590
- lineHeight: 1.6
909
+ lineHeight: 1.6,
910
+ fontFamily: MONO_FONT
591
911
  },
592
912
  prodButton: {
593
913
  padding: "12px 24px",
594
- backgroundColor: "#fff",
595
- color: "#000",
596
- fontSize: "14px",
597
- fontWeight: 600,
598
- border: "none",
599
- borderRadius: "8px",
600
- cursor: "pointer"
914
+ backgroundColor: "#222",
915
+ color: "#bbb",
916
+ fontSize: "13px",
917
+ fontWeight: 500,
918
+ borderWidth: "1px",
919
+ borderStyle: "solid",
920
+ borderColor: "#444",
921
+ borderRadius: "6px",
922
+ cursor: "pointer",
923
+ transition: "all 0.15s",
924
+ fontFamily: MONO_FONT
601
925
  }
602
926
  };
603
927
 
@@ -675,11 +999,11 @@ const NestedView = (props) => {
675
999
  const animationExitDuration = useRef(0);
676
1000
  const animationExitNow = useRef(0);
677
1001
  useEvents({
678
- "react:transition:begin": async ({ previous, state: state$1 }) => {
1002
+ "react:transition:begin": async ({ previous, state }) => {
679
1003
  const layer = previous.layers[index];
680
1004
  if (!layer) return;
681
- if (`${state$1.url.pathname}/`.startsWith(`${layer.path}/`)) return;
682
- const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
1005
+ if (`${state.url.pathname}/`.startsWith(`${layer.path}/`)) return;
1006
+ const animationExit = parseAnimation(layer.route?.animation, state, "exit");
683
1007
  if (animationExit) {
684
1008
  const duration = animationExit.duration || 200;
685
1009
  animationExitNow.current = Date.now();
@@ -691,8 +1015,8 @@ const NestedView = (props) => {
691
1015
  setAnimation("");
692
1016
  }
693
1017
  },
694
- "react:transition:end": async ({ state: state$1 }) => {
695
- const layer = state$1.layers[index];
1018
+ "react:transition:end": async ({ state }) => {
1019
+ const layer = state.layers[index];
696
1020
  if (animationExitNow.current) {
697
1021
  const duration = animationExitDuration.current;
698
1022
  const diff = Date.now() - animationExitNow.current;
@@ -700,7 +1024,7 @@ const NestedView = (props) => {
700
1024
  }
701
1025
  if (!layer?.cache) {
702
1026
  setView(layer?.element);
703
- const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
1027
+ const animationEnter = parseAnimation(layer?.route?.animation, state, "enter");
704
1028
  if (animationEnter) setAnimation(animationEnter.animation);
705
1029
  else setAnimation("");
706
1030
  }
@@ -772,34 +1096,71 @@ function parseAnimation(animationLike, state, type = "enter") {
772
1096
  }
773
1097
 
774
1098
  //#endregion
775
- //#region ../../src/router/providers/ReactPageProvider.ts
776
- const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
1099
+ //#region ../../src/router/components/NotFound.tsx
777
1100
  /**
778
- * Handle page routes for React applications. (Browser and Server)
1101
+ * Default 404 Not Found page component.
779
1102
  */
780
- var ReactPageProvider = class {
781
- log = $logger();
782
- env = $env(envSchema$1);
783
- alepha = $inject(Alepha);
784
- pages = [];
785
- getPages() {
786
- return this.pages;
787
- }
788
- getConcretePages() {
789
- const pages = [];
790
- for (const page of this.pages) {
791
- if (page.children && page.children.length > 0) continue;
792
- const fullPath = this.pathname(page.name);
793
- if (fullPath.includes(":") || fullPath.includes("*")) {
794
- if (typeof page.static === "object") {
795
- const entries = page.static.entries;
796
- if (entries && entries.length > 0) for (const entry of entries) {
797
- const params = entry.params;
798
- const path = this.compile(page.path ?? "", params);
799
- if (!path.includes(":") && !path.includes("*")) pages.push({
800
- ...page,
801
- name: params[Object.keys(params)[0]],
802
- staticName: page.name,
1103
+ const NotFound = (props) => /* @__PURE__ */ jsxs("div", {
1104
+ style: {
1105
+ width: "100%",
1106
+ minHeight: "90vh",
1107
+ boxSizing: "border-box",
1108
+ display: "flex",
1109
+ flexDirection: "column",
1110
+ justifyContent: "center",
1111
+ alignItems: "center",
1112
+ textAlign: "center",
1113
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
1114
+ padding: "2rem",
1115
+ ...props.style
1116
+ },
1117
+ children: [/* @__PURE__ */ jsx("div", {
1118
+ style: {
1119
+ fontSize: "6rem",
1120
+ fontWeight: 200,
1121
+ lineHeight: 1
1122
+ },
1123
+ children: "404"
1124
+ }), /* @__PURE__ */ jsx("div", {
1125
+ style: {
1126
+ fontSize: "0.875rem",
1127
+ marginTop: "1rem",
1128
+ opacity: .6
1129
+ },
1130
+ children: "Page not found"
1131
+ })]
1132
+ });
1133
+ var NotFound_default = NotFound;
1134
+
1135
+ //#endregion
1136
+ //#region ../../src/router/providers/ReactPageProvider.ts
1137
+ const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
1138
+ /**
1139
+ * Handle page routes for React applications. (Browser and Server)
1140
+ */
1141
+ var ReactPageProvider = class {
1142
+ log = $logger();
1143
+ env = $env(envSchema$1);
1144
+ alepha = $inject(Alepha);
1145
+ pages = [];
1146
+ getPages() {
1147
+ return this.pages;
1148
+ }
1149
+ getConcretePages() {
1150
+ const pages = [];
1151
+ for (const page of this.pages) {
1152
+ if (page.children && page.children.length > 0) continue;
1153
+ const fullPath = this.pathname(page.name);
1154
+ if (fullPath.includes(":") || fullPath.includes("*")) {
1155
+ if (typeof page.static === "object") {
1156
+ const entries = page.static.entries;
1157
+ if (entries && entries.length > 0) for (const entry of entries) {
1158
+ const params = entry.params;
1159
+ const path = this.compile(page.path ?? "", params);
1160
+ if (!path.includes(":") && !path.includes("*")) pages.push({
1161
+ ...page,
1162
+ name: params[Object.keys(params)[0]],
1163
+ staticName: page.name,
803
1164
  path,
804
1165
  ...entry
805
1166
  });
@@ -863,29 +1224,29 @@ var ReactPageProvider = class {
863
1224
  let forceRefresh = false;
864
1225
  for (let i = 0; i < stack.length; i++) {
865
1226
  const it = stack[i];
866
- const route$1 = it.route;
1227
+ const route = it.route;
867
1228
  const config = {};
868
1229
  try {
869
- this.convertStringObjectToObject(route$1.schema?.query, state.query);
870
- config.query = route$1.schema?.query ? this.alepha.codec.decode(route$1.schema.query, state.query) : {};
1230
+ this.convertStringObjectToObject(route.schema?.query, state.query);
1231
+ config.query = route.schema?.query ? this.alepha.codec.decode(route.schema.query, state.query) : {};
871
1232
  } catch (e) {
872
1233
  it.error = e;
873
1234
  break;
874
1235
  }
875
1236
  try {
876
- config.params = route$1.schema?.params ? this.alepha.codec.decode(route$1.schema.params, state.params) : {};
1237
+ config.params = route.schema?.params ? this.alepha.codec.decode(route.schema.params, state.params) : {};
877
1238
  } catch (e) {
878
1239
  it.error = e;
879
1240
  break;
880
1241
  }
881
1242
  it.config = { ...config };
882
- if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
1243
+ if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
883
1244
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
884
1245
  if (JSON.stringify({
885
1246
  part: url(previous[i].part),
886
1247
  params: previous[i].config?.params ?? {}
887
1248
  }) === JSON.stringify({
888
- part: url(route$1.path),
1249
+ part: url(route.path),
889
1250
  params: config.params ?? {}
890
1251
  })) {
891
1252
  it.props = previous[i].props;
@@ -899,11 +1260,11 @@ var ReactPageProvider = class {
899
1260
  }
900
1261
  forceRefresh = true;
901
1262
  }
902
- if (!route$1.loader) continue;
1263
+ if (!route.loader) continue;
903
1264
  try {
904
1265
  const args = Object.create(state);
905
1266
  Object.assign(args, config, context);
906
- const props = await route$1.loader?.(args) ?? {};
1267
+ const props = await route.loader?.(args) ?? {};
907
1268
  it.props = { ...props };
908
1269
  context = {
909
1270
  ...context,
@@ -928,9 +1289,9 @@ var ReactPageProvider = class {
928
1289
  const localErrorHandler = this.getErrorHandler(it.route);
929
1290
  if (localErrorHandler) {
930
1291
  const onErrorParent = state.onError;
931
- state.onError = (error, context$1) => {
932
- const result = localErrorHandler(error, context$1);
933
- if (result === void 0) return onErrorParent(error, context$1);
1292
+ state.onError = (error, context) => {
1293
+ const result = localErrorHandler(error, context);
1294
+ if (result === void 0) return onErrorParent(error, context);
934
1295
  return result;
935
1296
  };
936
1297
  }
@@ -1053,9 +1414,9 @@ var ReactPageProvider = class {
1053
1414
  map(pages, target) {
1054
1415
  const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
1055
1416
  const getChildrenFromParent = (it) => {
1056
- const children$1 = [];
1057
- for (const page of pages) if (page.options.parent === it) children$1.push(page);
1058
- return children$1;
1417
+ const children = [];
1418
+ for (const page of pages) if (page.options.parent === it) children.push(page);
1419
+ return children;
1059
1420
  };
1060
1421
  children.push(...getChildrenFromParent(target));
1061
1422
  return {
@@ -1098,468 +1459,79 @@ const isPageRoute = (it) => {
1098
1459
  };
1099
1460
 
1100
1461
  //#endregion
1101
- //#region ../../src/router/providers/ReactBrowserRouterProvider.ts
1102
- /**
1103
- * Implementation of AlephaRouter for React in browser environment.
1104
- */
1105
- var ReactBrowserRouterProvider = class extends RouterProvider {
1106
- log = $logger();
1107
- alepha = $inject(Alepha);
1108
- pageApi = $inject(ReactPageProvider);
1109
- browserHeadProvider = $inject(BrowserHeadProvider);
1110
- add(entry) {
1111
- this.pageApi.add(entry);
1112
- }
1113
- configure = $hook({
1114
- on: "configure",
1115
- handler: async () => {
1116
- for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
1117
- path: page.match,
1118
- page
1119
- });
1120
- }
1121
- });
1122
- async transition(url, previous = [], meta = {}) {
1123
- const { pathname, search } = url;
1124
- const state = {
1125
- url,
1126
- query: {},
1127
- params: {},
1128
- layers: [],
1129
- onError: () => null,
1130
- meta
1131
- };
1132
- await this.alepha.events.emit("react:action:begin", { type: "transition" });
1133
- await this.alepha.events.emit("react:transition:begin", {
1134
- previous: this.alepha.store.get("alepha.react.router.state"),
1135
- state
1136
- });
1137
- try {
1138
- const { route, params } = this.match(pathname);
1139
- const query = {};
1140
- if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
1141
- state.name = route?.page.name;
1142
- state.query = query;
1143
- state.params = params ?? {};
1144
- if (isPageRoute(route)) {
1145
- const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
1146
- if (redirect) return redirect;
1147
- }
1148
- if (state.layers.length === 0) state.layers.push({
1149
- name: "not-found",
1150
- element: createElement(NotFound_default),
1151
- index: 0,
1152
- path: "/"
1153
- });
1154
- await this.alepha.events.emit("react:action:success", { type: "transition" });
1155
- await this.alepha.events.emit("react:transition:success", { state });
1156
- } catch (e) {
1157
- this.log.error("Transition has failed", e);
1158
- state.layers = [{
1159
- name: "error",
1160
- element: this.pageApi.renderError(e),
1161
- index: 0,
1162
- path: "/"
1163
- }];
1164
- await this.alepha.events.emit("react:action:error", {
1165
- type: "transition",
1166
- error: e
1167
- });
1168
- await this.alepha.events.emit("react:transition:error", {
1169
- error: e,
1170
- state
1171
- });
1172
- }
1173
- if (previous) for (let i = 0; i < previous.length; i++) {
1174
- const layer = previous[i];
1175
- if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1176
- }
1177
- this.alepha.store.set("alepha.react.router.state", state);
1178
- await this.alepha.events.emit("react:action:end", { type: "transition" });
1179
- await this.alepha.events.emit("react:transition:end", { state });
1180
- this.browserHeadProvider.fillAndRenderHead(state);
1181
- }
1182
- root(state) {
1183
- return this.pageApi.root(state);
1184
- }
1185
- };
1186
-
1187
- //#endregion
1188
- //#region ../../src/router/providers/ReactBrowserProvider.ts
1462
+ //#region ../../src/router/providers/ReactServerTemplateProvider.ts
1189
1463
  /**
1190
- * React browser renderer configuration atom
1464
+ * Handles HTML template parsing, preprocessing, and streaming for SSR.
1465
+ *
1466
+ * Responsibilities:
1467
+ * - Parse template once at startup into logical slots
1468
+ * - Pre-encode static parts as Uint8Array for zero-copy streaming
1469
+ * - Render dynamic parts (attributes, head content) efficiently
1470
+ * - Build hydration data for client-side rehydration
1471
+ *
1472
+ * This provider is injected into ReactServerProvider to handle all
1473
+ * template-related operations, keeping ReactServerProvider focused
1474
+ * on request handling and React rendering coordination.
1191
1475
  */
1192
- const reactBrowserOptions = $atom({
1193
- name: "alepha.react.browser.options",
1194
- schema: t.object({ scrollRestoration: t.enum(["top", "manual"]) }),
1195
- default: { scrollRestoration: "top" }
1196
- });
1197
- var ReactBrowserProvider = class {
1476
+ var ReactServerTemplateProvider = class {
1198
1477
  log = $logger();
1199
- client = $inject(LinkProvider);
1200
1478
  alepha = $inject(Alepha);
1201
- router = $inject(ReactBrowserRouterProvider);
1202
- dateTimeProvider = $inject(DateTimeProvider);
1203
- browserHeadProvider = $inject(BrowserHeadProvider);
1204
- options = $use(reactBrowserOptions);
1479
+ /**
1480
+ * Shared TextEncoder instance - reused across all requests.
1481
+ */
1482
+ encoder = new TextEncoder();
1483
+ /**
1484
+ * Pre-encoded common strings for streaming.
1485
+ */
1486
+ ENCODED = {
1487
+ HYDRATION_PREFIX: this.encoder.encode("<script>window.__ssr="),
1488
+ HYDRATION_SUFFIX: this.encoder.encode("<\/script>"),
1489
+ EMPTY: this.encoder.encode("")
1490
+ };
1491
+ /**
1492
+ * Cached template slots - parsed once, reused for all requests.
1493
+ */
1494
+ slots = null;
1495
+ /**
1496
+ * Root element ID for React mounting.
1497
+ */
1205
1498
  get rootId() {
1206
1499
  return "root";
1207
1500
  }
1208
- getRootElement() {
1209
- const root = this.document.getElementById(this.rootId);
1210
- if (root) return root;
1211
- const div = this.document.createElement("div");
1212
- div.id = this.rootId;
1213
- this.document.body.prepend(div);
1214
- return div;
1215
- }
1216
- transitioning;
1217
- get state() {
1218
- return this.alepha.store.get("alepha.react.router.state");
1219
- }
1220
1501
  /**
1221
- * Accessor for Document DOM API.
1502
+ * Regex pattern for matching the root div and extracting its content.
1222
1503
  */
1223
- get document() {
1224
- return window.document;
1504
+ get rootDivRegex() {
1505
+ return new RegExp(`<div([^>]*)\\s+id=["']${this.rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
1225
1506
  }
1226
1507
  /**
1227
- * Accessor for History DOM API.
1508
+ * Extract the content inside the root div from HTML.
1509
+ *
1510
+ * @param html - Full HTML string
1511
+ * @returns The content inside the root div, or undefined if not found
1228
1512
  */
1229
- get history() {
1230
- return window.history;
1513
+ extractRootContent(html) {
1514
+ return html.match(this.rootDivRegex)?.[3];
1231
1515
  }
1232
1516
  /**
1233
- * Accessor for Location DOM API.
1517
+ * Check if template has been parsed and slots are available.
1234
1518
  */
1235
- get location() {
1236
- return window.location;
1237
- }
1238
- get base() {
1239
- const base = import.meta.env?.BASE_URL;
1240
- if (!base || base === "/") return "";
1241
- return base;
1242
- }
1243
- get url() {
1244
- const url = this.location.pathname + this.location.search;
1245
- if (this.base) return url.replace(this.base, "");
1246
- return url;
1519
+ isReady() {
1520
+ return this.slots !== null;
1247
1521
  }
1248
- pushState(path, replace) {
1249
- const url = this.base + path;
1250
- if (replace) this.history.replaceState({}, "", url);
1251
- else this.history.pushState({}, "", url);
1252
- }
1253
- async invalidate(props) {
1254
- const previous = [];
1255
- this.log.trace("Invalidating layers");
1256
- if (props) {
1257
- const [key] = Object.keys(props);
1258
- const value = props[key];
1259
- for (const layer of this.state.layers) {
1260
- if (layer.props?.[key]) {
1261
- previous.push({
1262
- ...layer,
1263
- props: {
1264
- ...layer.props,
1265
- [key]: value
1266
- }
1267
- });
1268
- break;
1269
- }
1270
- previous.push(layer);
1271
- }
1272
- }
1273
- await this.render({ previous });
1274
- }
1275
- async go(url, options = {}) {
1276
- this.log.trace(`Going to ${url}`, {
1277
- url,
1278
- options
1279
- });
1280
- await this.render({
1281
- url,
1282
- previous: options.force ? [] : this.state.layers,
1283
- meta: options.meta
1284
- });
1285
- if (this.state.url.pathname + this.state.url.search !== url) {
1286
- this.pushState(this.state.url.pathname + this.state.url.search);
1287
- return;
1288
- }
1289
- this.pushState(url, options.replace);
1290
- }
1291
- async render(options = {}) {
1292
- const previous = options.previous ?? this.state.layers;
1293
- const url = options.url ?? this.url;
1294
- const start = this.dateTimeProvider.now();
1295
- this.transitioning = {
1296
- to: url,
1297
- from: this.state?.url.pathname
1298
- };
1299
- this.log.debug("Transitioning...", { to: url });
1300
- const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
1301
- if (redirect) {
1302
- this.log.info("Redirecting to", { redirect });
1303
- if (redirect.startsWith("http")) window.location.href = redirect;
1304
- else return await this.render({ url: redirect });
1305
- }
1306
- const ms = this.dateTimeProvider.now().diff(start);
1307
- this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
1308
- this.transitioning = void 0;
1309
- }
1310
- /**
1311
- * Get embedded layers from the server.
1312
- */
1313
- getHydrationState() {
1314
- try {
1315
- if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
1316
- } catch (error) {
1317
- console.error(error);
1318
- }
1319
- }
1320
- onTransitionEnd = $hook({
1321
- on: "react:transition:end",
1322
- handler: () => {
1323
- if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
1324
- this.log.trace("Restoring scroll position to top");
1325
- window.scrollTo(0, 0);
1326
- }
1327
- }
1328
- });
1329
- ready = $hook({
1330
- on: "ready",
1331
- handler: async () => {
1332
- const hydration = this.getHydrationState();
1333
- const previous = hydration?.layers ?? [];
1334
- if (hydration) {
1335
- for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.store.set(key, value);
1336
- }
1337
- await this.render({ previous });
1338
- const element = this.router.root(this.state);
1339
- await this.alepha.events.emit("react:browser:render", {
1340
- element,
1341
- root: this.getRootElement(),
1342
- hydration,
1343
- state: this.state
1344
- });
1345
- this.browserHeadProvider.fillAndRenderHead(this.state);
1346
- window.addEventListener("popstate", () => {
1347
- if (this.base + this.state.url.pathname === this.location.pathname) return;
1348
- this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1349
- this.render();
1350
- });
1351
- }
1352
- });
1353
- };
1354
-
1355
- //#endregion
1356
- //#region ../../src/router/services/ReactRouter.ts
1357
- /**
1358
- * Friendly browser router API.
1359
- *
1360
- * Can be safely used server-side, but most methods will be no-op.
1361
- */
1362
- var ReactRouter = class {
1363
- alepha = $inject(Alepha);
1364
- pageApi = $inject(ReactPageProvider);
1365
- get state() {
1366
- return this.alepha.store.get("alepha.react.router.state");
1367
- }
1368
- get pages() {
1369
- return this.pageApi.getPages();
1370
- }
1371
- get concretePages() {
1372
- return this.pageApi.getConcretePages();
1373
- }
1374
- get browser() {
1375
- if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
1376
- }
1377
- isActive(href, options = {}) {
1378
- const current = this.state.url.pathname;
1379
- let isActive = current === href || current === `${href}/` || `${current}/` === href;
1380
- if (options.startWith && !isActive) isActive = current.startsWith(href);
1381
- return isActive;
1382
- }
1383
- node(name, config = {}) {
1384
- const page = this.pageApi.page(name);
1385
- if (!page.lazy && !page.component) return {
1386
- ...page,
1387
- label: page.label ?? page.name,
1388
- children: void 0
1389
- };
1390
- return {
1391
- ...page,
1392
- label: page.label ?? page.name,
1393
- href: this.path(name, config),
1394
- children: void 0
1395
- };
1396
- }
1397
- path(name, config = {}) {
1398
- return this.pageApi.pathname(name, {
1399
- params: {
1400
- ...this.state?.params,
1401
- ...config.params
1402
- },
1403
- query: config.query
1404
- });
1405
- }
1406
- /**
1407
- * Reload the current page.
1408
- * This is equivalent to calling `go()` with the current pathname and search.
1409
- */
1410
- async reload() {
1411
- if (!this.browser) return;
1412
- await this.go(this.location.pathname + this.location.search, {
1413
- replace: true,
1414
- force: true
1415
- });
1416
- }
1417
- getURL() {
1418
- if (!this.browser) return this.state.url;
1419
- return new URL(this.location.href);
1420
- }
1421
- get location() {
1422
- if (!this.browser) throw new Error("Browser is required");
1423
- return this.browser.location;
1424
- }
1425
- get current() {
1426
- return this.state;
1427
- }
1428
- get pathname() {
1429
- return this.state.url.pathname;
1430
- }
1431
- get query() {
1432
- const query = {};
1433
- for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
1434
- return query;
1435
- }
1436
- async back() {
1437
- this.browser?.history.back();
1438
- }
1439
- async forward() {
1440
- this.browser?.history.forward();
1441
- }
1442
- async invalidate(props) {
1443
- await this.browser?.invalidate(props);
1444
- }
1445
- async go(path, options) {
1446
- for (const page of this.pages) if (page.name === path) {
1447
- await this.browser?.go(this.path(path, options), options);
1448
- return;
1449
- }
1450
- await this.browser?.go(path, options);
1451
- }
1452
- anchor(path, options = {}) {
1453
- let href = path;
1454
- for (const page of this.pages) if (page.name === path) {
1455
- href = this.path(path, options);
1456
- break;
1457
- }
1458
- return {
1459
- href: this.base(href),
1460
- onClick: (ev) => {
1461
- ev.stopPropagation();
1462
- ev.preventDefault();
1463
- this.go(href, options).catch(console.error);
1464
- }
1465
- };
1466
- }
1467
- base(path) {
1468
- const base = import.meta.env?.BASE_URL;
1469
- if (!base || base === "/") return path;
1470
- return base + path;
1471
- }
1472
- /**
1473
- * Set query params.
1474
- *
1475
- * @param record
1476
- * @param options
1477
- */
1478
- setQueryParams(record, options = {}) {
1479
- const func = typeof record === "function" ? record : () => record;
1480
- const search = new URLSearchParams(func(this.query)).toString();
1481
- const state = search ? `${this.pathname}?${search}` : this.pathname;
1482
- if (options.push) window.history.pushState({}, "", state);
1483
- else window.history.replaceState({}, "", state);
1484
- }
1485
- };
1486
-
1487
- //#endregion
1488
- //#region ../../src/router/providers/ReactServerTemplateProvider.ts
1489
- /**
1490
- * Handles HTML template parsing, preprocessing, and streaming for SSR.
1491
- *
1492
- * Responsibilities:
1493
- * - Parse template once at startup into logical slots
1494
- * - Pre-encode static parts as Uint8Array for zero-copy streaming
1495
- * - Render dynamic parts (attributes, head content) efficiently
1496
- * - Build hydration data for client-side rehydration
1497
- *
1498
- * This provider is injected into ReactServerProvider to handle all
1499
- * template-related operations, keeping ReactServerProvider focused
1500
- * on request handling and React rendering coordination.
1501
- */
1502
- var ReactServerTemplateProvider = class {
1503
- log = $logger();
1504
- alepha = $inject(Alepha);
1505
- /**
1506
- * Shared TextEncoder instance - reused across all requests.
1507
- */
1508
- encoder = new TextEncoder();
1509
- /**
1510
- * Pre-encoded common strings for streaming.
1511
- */
1512
- ENCODED = {
1513
- HYDRATION_PREFIX: this.encoder.encode("<script>window.__ssr="),
1514
- HYDRATION_SUFFIX: this.encoder.encode("<\/script>"),
1515
- EMPTY: this.encoder.encode("")
1516
- };
1517
- /**
1518
- * Cached template slots - parsed once, reused for all requests.
1519
- */
1520
- slots = null;
1521
- /**
1522
- * Root element ID for React mounting.
1523
- */
1524
- get rootId() {
1525
- return "root";
1526
- }
1527
- /**
1528
- * Regex pattern for matching the root div and extracting its content.
1529
- */
1530
- get rootDivRegex() {
1531
- return new RegExp(`<div([^>]*)\\s+id=["']${this.rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
1532
- }
1533
- /**
1534
- * Extract the content inside the root div from HTML.
1535
- *
1536
- * @param html - Full HTML string
1537
- * @returns The content inside the root div, or undefined if not found
1538
- */
1539
- extractRootContent(html) {
1540
- return html.match(this.rootDivRegex)?.[3];
1541
- }
1542
- /**
1543
- * Check if template has been parsed and slots are available.
1544
- */
1545
- isReady() {
1546
- return this.slots !== null;
1547
- }
1548
- /**
1549
- * Get the parsed template slots.
1550
- * Throws if template hasn't been parsed yet.
1551
- */
1552
- getSlots() {
1553
- if (!this.slots) throw new AlephaError("Template not parsed. Call parseTemplate() during configuration.");
1554
- return this.slots;
1522
+ /**
1523
+ * Get the parsed template slots.
1524
+ * Throws if template hasn't been parsed yet.
1525
+ */
1526
+ getSlots() {
1527
+ if (!this.slots) throw new AlephaError("Template not parsed. Call parseTemplate() during configuration.");
1528
+ return this.slots;
1555
1529
  }
1556
1530
  /**
1557
1531
  * Parse an HTML template into logical slots for efficient streaming.
1558
1532
  *
1559
1533
  * This should be called once during server startup/configuration.
1560
1534
  * The parsed slots are cached and reused for all requests.
1561
- *
1562
- * @param template - The HTML template string (typically index.html)
1563
1535
  */
1564
1536
  parseTemplate(template) {
1565
1537
  this.log.debug("Parsing template into slots");
@@ -1596,7 +1568,7 @@ var ReactServerTemplateProvider = class {
1596
1568
  }
1597
1569
  const rootOpenTag = rootAttrs ? `<div ${rootAttrs} id="${rootId}">` : `<div id="${rootId}">`;
1598
1570
  this.slots = {
1599
- doctype: this.encoder.encode(doctype + "\n"),
1571
+ doctype: this.encoder.encode(`${doctype}\n`),
1600
1572
  htmlOpen: this.encoder.encode("<html"),
1601
1573
  htmlClose: this.encoder.encode(">\n"),
1602
1574
  headOpen: this.encoder.encode("<head>"),
@@ -1629,9 +1601,7 @@ var ReactServerTemplateProvider = class {
1629
1601
  parseAttributes(attrStr) {
1630
1602
  const attrs = {};
1631
1603
  if (!attrStr) return attrs;
1632
- const attrRegex = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
1633
- let match;
1634
- while (match = attrRegex.exec(attrStr)) {
1604
+ for (const match of attrStr.matchAll(/([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)) {
1635
1605
  const key = match[1];
1636
1606
  attrs[key] = match[2] ?? match[3] ?? match[4] ?? "";
1637
1607
  }
@@ -1700,6 +1670,7 @@ var ReactServerTemplateProvider = class {
1700
1670
  */
1701
1671
  renderLinkTag(link) {
1702
1672
  let tag = `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}"`;
1673
+ if (link.type) tag += ` type="${this.escapeHtml(link.type)}"`;
1703
1674
  if (link.as) tag += ` as="${this.escapeHtml(link.as)}"`;
1704
1675
  if (link.crossorigin != null) tag += " crossorigin=\"\"";
1705
1676
  tag += ">\n";
@@ -1709,10 +1680,14 @@ var ReactServerTemplateProvider = class {
1709
1680
  * Render a script tag.
1710
1681
  */
1711
1682
  renderScriptTag(script) {
1712
- return `<script ${Object.entries(script).filter(([, value]) => value !== false).map(([key, value]) => {
1683
+ if (typeof script === "string") return `<script>${script}<\/script>\n`;
1684
+ const { content, ...rest } = script;
1685
+ const attrs = Object.entries(rest).filter(([, value]) => value !== false && value !== void 0).map(([key, value]) => {
1713
1686
  if (value === true) return key;
1714
1687
  return `${key}="${this.escapeHtml(String(value))}"`;
1715
- }).join(" ")}><\/script>\n`;
1688
+ }).join(" ");
1689
+ if (content) return attrs ? `<script ${attrs}>${content}<\/script>\n` : `<script>${content}<\/script>\n`;
1690
+ return `<script ${attrs}><\/script>\n`;
1716
1691
  }
1717
1692
  /**
1718
1693
  * Escape HTML special characters.
@@ -1735,41 +1710,66 @@ var ReactServerTemplateProvider = class {
1735
1710
  */
1736
1711
  buildHydrationData(state) {
1737
1712
  const { request, context, ...store } = this.alepha.context.als?.getStore() ?? {};
1738
- return {
1739
- ...store,
1740
- "alepha.react.router.state": void 0,
1741
- layers: state.layers.map((layer) => ({
1742
- ...layer,
1743
- error: layer.error ? {
1744
- ...layer.error,
1745
- name: layer.error.name,
1746
- message: layer.error.message,
1747
- stack: !this.alepha.isProduction() ? layer.error.stack : void 0
1748
- } : void 0,
1749
- index: void 0,
1750
- path: void 0,
1751
- element: void 0,
1752
- route: void 0
1753
- }))
1754
- };
1755
- }
1756
- /**
1757
- * Encode a string to Uint8Array using the shared encoder.
1758
- */
1759
- encode(str) {
1760
- return this.encoder.encode(str);
1761
- }
1762
- /**
1763
- * Get the pre-encoded hydration script prefix.
1764
- */
1765
- get hydrationPrefix() {
1766
- return this.ENCODED.HYDRATION_PREFIX;
1767
- }
1768
- /**
1769
- * Get the pre-encoded hydration script suffix.
1713
+ const hydrationData = { layers: state.layers.map((layer) => ({
1714
+ part: layer.part,
1715
+ name: layer.name,
1716
+ config: layer.config,
1717
+ props: layer.props,
1718
+ error: layer.error ? {
1719
+ ...layer.error,
1720
+ name: layer.error.name,
1721
+ message: layer.error.message,
1722
+ stack: !this.alepha.isProduction() ? layer.error.stack : void 0
1723
+ } : void 0
1724
+ })) };
1725
+ for (const [key, value] of Object.entries(store)) if (key.charAt(0) !== "_" && key !== "alepha.react.router.state" && key !== "registry") hydrationData[key] = value;
1726
+ return hydrationData;
1727
+ }
1728
+ /**
1729
+ * Stream the body content: body tag, root div, React content, hydration, and closing tags.
1730
+ *
1731
+ * If an error occurs during React streaming, it injects error HTML instead of aborting,
1732
+ * ensuring users see an error message rather than a white screen.
1770
1733
  */
1771
- get hydrationSuffix() {
1772
- return this.ENCODED.HYDRATION_SUFFIX;
1734
+ async streamBodyContent(controller, reactStream, state, hydration) {
1735
+ const slots = this.getSlots();
1736
+ const encoder = this.encoder;
1737
+ const head = state.head;
1738
+ controller.enqueue(slots.bodyOpen);
1739
+ controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(head?.bodyAttributes)));
1740
+ controller.enqueue(slots.bodyClose);
1741
+ if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
1742
+ controller.enqueue(slots.rootOpen);
1743
+ const reader = reactStream.getReader();
1744
+ let streamError = null;
1745
+ try {
1746
+ while (true) {
1747
+ const { done, value } = await reader.read();
1748
+ if (done) break;
1749
+ controller.enqueue(value);
1750
+ }
1751
+ } catch (error) {
1752
+ streamError = error;
1753
+ this.log.error("Error during React stream reading", error);
1754
+ } finally {
1755
+ reader.releaseLock();
1756
+ }
1757
+ if (streamError) {
1758
+ this.injectErrorHtml(controller, encoder, slots, streamError, state, {
1759
+ headClosed: true,
1760
+ bodyStarted: true
1761
+ });
1762
+ return;
1763
+ }
1764
+ controller.enqueue(slots.rootClose);
1765
+ if (slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
1766
+ if (hydration) {
1767
+ const hydrationData = this.buildHydrationData(state);
1768
+ controller.enqueue(this.ENCODED.HYDRATION_PREFIX);
1769
+ controller.enqueue(encoder.encode(this.safeJsonSerialize(hydrationData)));
1770
+ controller.enqueue(this.ENCODED.HYDRATION_SUFFIX);
1771
+ }
1772
+ controller.enqueue(slots.scriptClose);
1773
1773
  }
1774
1774
  /**
1775
1775
  * Create a ReadableStream that streams the HTML template with React content.
@@ -1798,30 +1798,7 @@ var ReactServerTemplateProvider = class {
1798
1798
  if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
1799
1799
  controller.enqueue(encoder.encode(this.renderHeadContent(head)));
1800
1800
  controller.enqueue(slots.headClose);
1801
- controller.enqueue(slots.bodyOpen);
1802
- controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(head?.bodyAttributes)));
1803
- controller.enqueue(slots.bodyClose);
1804
- if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
1805
- controller.enqueue(slots.rootOpen);
1806
- const reader = reactStream.getReader();
1807
- try {
1808
- while (true) {
1809
- const { done, value } = await reader.read();
1810
- if (done) break;
1811
- controller.enqueue(value);
1812
- }
1813
- } finally {
1814
- reader.releaseLock();
1815
- }
1816
- controller.enqueue(slots.rootClose);
1817
- if (slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
1818
- if (hydration) {
1819
- const hydrationData = this.buildHydrationData(state);
1820
- controller.enqueue(this.ENCODED.HYDRATION_PREFIX);
1821
- controller.enqueue(encoder.encode(this.safeJsonSerialize(hydrationData)));
1822
- controller.enqueue(this.ENCODED.HYDRATION_SUFFIX);
1823
- }
1824
- controller.enqueue(slots.scriptClose);
1801
+ await this.streamBodyContent(controller, reactStream, state, hydration);
1825
1802
  controller.close();
1826
1803
  } catch (error) {
1827
1804
  onError?.(error);
@@ -1882,9 +1859,12 @@ var ReactServerTemplateProvider = class {
1882
1859
  * @param options - Streaming options
1883
1860
  */
1884
1861
  createEarlyHtmlStream(globalHead, asyncWork, options = {}) {
1885
- const { hydration = true, onError, onRedirect } = options;
1862
+ const { hydration = true, onError } = options;
1886
1863
  const slots = this.getSlots();
1887
1864
  const encoder = this.encoder;
1865
+ let headClosed = false;
1866
+ let bodyStarted = false;
1867
+ let routerState;
1888
1868
  return new ReadableStream({ start: async (controller) => {
1889
1869
  try {
1890
1870
  controller.enqueue(slots.doctype);
@@ -1894,47 +1874,102 @@ var ReactServerTemplateProvider = class {
1894
1874
  controller.enqueue(slots.headOpen);
1895
1875
  if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
1896
1876
  const result = await asyncWork();
1897
- if (!result) {
1877
+ if (!result || "redirect" in result) {
1878
+ if (result && "redirect" in result) {
1879
+ this.log.debug("Loader redirect detected after streaming started, using meta refresh", { redirect: result.redirect });
1880
+ controller.enqueue(encoder.encode(`<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`));
1881
+ }
1898
1882
  controller.enqueue(slots.headClose);
1899
1883
  controller.enqueue(encoder.encode("<body></body></html>"));
1900
1884
  controller.close();
1901
1885
  return;
1902
1886
  }
1903
1887
  const { state, reactStream } = result;
1904
- const head = state.head;
1905
- controller.enqueue(encoder.encode(this.renderHeadContent(head)));
1888
+ routerState = state;
1889
+ controller.enqueue(encoder.encode(this.renderHeadContent(state.head)));
1906
1890
  controller.enqueue(slots.headClose);
1907
- controller.enqueue(slots.bodyOpen);
1908
- controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(head?.bodyAttributes)));
1909
- controller.enqueue(slots.bodyClose);
1910
- if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
1911
- controller.enqueue(slots.rootOpen);
1912
- const reader = reactStream.getReader();
1913
- try {
1914
- while (true) {
1915
- const { done, value } = await reader.read();
1916
- if (done) break;
1917
- controller.enqueue(value);
1918
- }
1919
- } finally {
1920
- reader.releaseLock();
1921
- }
1922
- controller.enqueue(slots.rootClose);
1923
- if (slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
1924
- if (hydration) {
1925
- const hydrationData = this.buildHydrationData(state);
1926
- controller.enqueue(this.ENCODED.HYDRATION_PREFIX);
1927
- controller.enqueue(encoder.encode(this.safeJsonSerialize(hydrationData)));
1928
- controller.enqueue(this.ENCODED.HYDRATION_SUFFIX);
1929
- }
1930
- controller.enqueue(slots.scriptClose);
1891
+ headClosed = true;
1892
+ bodyStarted = true;
1893
+ await this.streamBodyContent(controller, reactStream, state, hydration);
1931
1894
  controller.close();
1932
1895
  } catch (error) {
1933
1896
  onError?.(error);
1934
- controller.error(error);
1897
+ try {
1898
+ this.injectErrorHtml(controller, encoder, slots, error, routerState, {
1899
+ headClosed,
1900
+ bodyStarted
1901
+ });
1902
+ controller.close();
1903
+ } catch {
1904
+ controller.error(error);
1905
+ }
1935
1906
  }
1936
1907
  } });
1937
1908
  }
1909
+ /**
1910
+ * Inject error HTML into the stream when an error occurs during streaming.
1911
+ *
1912
+ * Uses the router state's onError handler to render the error component,
1913
+ * falling back to ErrorViewer if no custom handler is defined.
1914
+ * Renders using renderToString to produce static HTML.
1915
+ *
1916
+ * Since we may have already sent partial HTML (DOCTYPE, <html>, <head>),
1917
+ * we need to complete the document with an error message instead of aborting.
1918
+ *
1919
+ * Handles different states:
1920
+ * - headClosed=false, bodyStarted=false: Need to add head content, close head, open body, add error, close all
1921
+ * - headClosed=true, bodyStarted=false: Need to open body, add error, close all
1922
+ * - headClosed=true, bodyStarted=true: Already inside root div, add error, close all
1923
+ */
1924
+ injectErrorHtml(controller, encoder, slots, error, routerState, streamState) {
1925
+ if (!streamState.headClosed) {
1926
+ const headContent = this.renderHeadContent(routerState?.head);
1927
+ if (headContent) controller.enqueue(encoder.encode(headContent));
1928
+ controller.enqueue(slots.headClose);
1929
+ }
1930
+ if (!streamState.bodyStarted) {
1931
+ controller.enqueue(slots.bodyOpen);
1932
+ controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(routerState?.head?.bodyAttributes)));
1933
+ controller.enqueue(slots.bodyClose);
1934
+ if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
1935
+ controller.enqueue(slots.rootOpen);
1936
+ }
1937
+ const errorHtml = this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), routerState);
1938
+ controller.enqueue(encoder.encode(errorHtml));
1939
+ controller.enqueue(slots.rootClose);
1940
+ if (!streamState.bodyStarted && slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
1941
+ controller.enqueue(slots.scriptClose);
1942
+ }
1943
+ /**
1944
+ * Render an error to HTML string using the router's error handler.
1945
+ *
1946
+ * Falls back to ErrorViewer if:
1947
+ * - No router state is available
1948
+ * - The error handler returns null/undefined
1949
+ * - The error handler itself throws
1950
+ */
1951
+ renderErrorToString(error, routerState) {
1952
+ this.log.error("SSR rendering error", error);
1953
+ let errorElement;
1954
+ if (routerState?.onError) try {
1955
+ const result = routerState.onError(error, routerState);
1956
+ if (result instanceof Redirection) this.log.warn("Error handler returned Redirection but headers already sent", { redirect: result.redirect });
1957
+ else if (result !== null && result !== void 0) errorElement = result;
1958
+ } catch (handlerError) {
1959
+ this.log.error("Error handler threw an exception", handlerError);
1960
+ }
1961
+ if (!errorElement) errorElement = createElement(ErrorViewer_default, {
1962
+ error,
1963
+ alepha: this.alepha
1964
+ });
1965
+ const wrappedElement = createElement(AlephaContext.Provider, { value: this.alepha }, errorElement);
1966
+ try {
1967
+ return renderToString(wrappedElement);
1968
+ } catch (renderError) {
1969
+ this.log.error("Failed to render error component", renderError);
1970
+ return error.message;
1971
+ }
1972
+ }
1938
1973
  };
1939
1974
 
1940
1975
  //#endregion
@@ -1944,16 +1979,11 @@ var ReactServerTemplateProvider = class {
1944
1979
  */
1945
1980
  const ssrManifestAtomSchema = t.object({
1946
1981
  preload: t.optional(t.record(t.string(), t.string())),
1947
- ssr: t.optional(t.record(t.string(), t.array(t.string()))),
1948
1982
  client: t.optional(t.record(t.string(), t.object({
1949
1983
  file: t.string(),
1950
- src: t.optional(t.string()),
1951
1984
  isEntry: t.optional(t.boolean()),
1952
- isDynamicEntry: t.optional(t.boolean()),
1953
1985
  imports: t.optional(t.array(t.string())),
1954
- dynamicImports: t.optional(t.array(t.string())),
1955
- css: t.optional(t.array(t.string())),
1956
- assets: t.optional(t.array(t.string()))
1986
+ css: t.optional(t.array(t.string()))
1957
1987
  })))
1958
1988
  });
1959
1989
  /**
@@ -1965,8 +1995,7 @@ const ssrManifestAtomSchema = t.object({
1965
1995
  *
1966
1996
  * The manifest includes:
1967
1997
  * - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
1968
- * - ssr: Maps source files to their required chunks
1969
- * - client: Maps source files to their output info including imports/css
1998
+ * - client: Maps source files to their output info (file, imports, css)
1970
1999
  */
1971
2000
  const ssrManifestAtom = $atom({
1972
2001
  name: "alepha.react.ssr.manifest",
@@ -1986,7 +2015,6 @@ const ssrManifestAtom = $atom({
1986
2015
  *
1987
2016
  * Manifest files are generated during `vite build`:
1988
2017
  * - manifest.json (client manifest)
1989
- * - ssr-manifest.json (SSR manifest)
1990
2018
  * - preload-manifest.json (from viteAlephaSsrPreload plugin)
1991
2019
  */
1992
2020
  var SSRManifestProvider = class {
@@ -2005,12 +2033,6 @@ var SSRManifestProvider = class {
2005
2033
  return this.manifest.preload;
2006
2034
  }
2007
2035
  /**
2008
- * Get the SSR manifest.
2009
- */
2010
- get ssrManifest() {
2011
- return this.manifest.ssr;
2012
- }
2013
- /**
2014
2036
  * Get the client manifest.
2015
2037
  */
2016
2038
  get clientManifest() {
@@ -2031,14 +2053,13 @@ var SSRManifestProvider = class {
2031
2053
  /**
2032
2054
  * Get all chunks required for a source file, including transitive dependencies.
2033
2055
  *
2034
- * Uses the client manifest to recursively resolve all imported chunks,
2035
- * not just the direct chunks from the SSR manifest.
2056
+ * Uses the client manifest to recursively resolve all imported chunks.
2036
2057
  *
2037
2058
  * @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
2038
2059
  * @returns Array of chunk URLs to preload, or empty array if not found
2039
2060
  */
2040
2061
  getChunks(sourcePath) {
2041
- if (!this.clientManifest) return this.getChunksFromSSRManifest(sourcePath);
2062
+ if (!this.clientManifest) return [];
2042
2063
  if (!this.findManifestEntry(sourcePath)) return [];
2043
2064
  const chunks = /* @__PURE__ */ new Set();
2044
2065
  const visited = /* @__PURE__ */ new Set();
@@ -2069,32 +2090,14 @@ var SSRManifestProvider = class {
2069
2090
  if (visited.has(key)) return;
2070
2091
  visited.add(key);
2071
2092
  if (!this.clientManifest) return;
2072
- const entry = this.clientManifest[key];
2073
- if (!entry) return;
2074
- if (entry.file) chunks.add("/" + entry.file);
2075
- if (entry.css) for (const css of entry.css) chunks.add("/" + css);
2076
- if (entry.imports) for (const imp of entry.imports) {
2077
- if (imp === "index.html" || imp.endsWith(".html")) continue;
2078
- this.collectChunksRecursive(imp, chunks, visited);
2079
- }
2080
- }
2081
- /**
2082
- * Fallback to SSR manifest for chunk lookup.
2083
- */
2084
- getChunksFromSSRManifest(sourcePath) {
2085
- if (!this.ssrManifest) return [];
2086
- if (this.ssrManifest[sourcePath]) return this.ssrManifest[sourcePath];
2087
- const basePath = sourcePath.replace(/\.[^.]+$/, "");
2088
- for (const ext of [
2089
- ".tsx",
2090
- ".ts",
2091
- ".jsx",
2092
- ".js"
2093
- ]) {
2094
- const pathWithExt = basePath + ext;
2095
- if (this.ssrManifest[pathWithExt]) return this.ssrManifest[pathWithExt];
2093
+ const entry = this.clientManifest[key];
2094
+ if (!entry) return;
2095
+ if (entry.file) chunks.add(`/${entry.file}`);
2096
+ if (entry.css) for (const css of entry.css) chunks.add(`/${css}`);
2097
+ if (entry.imports) for (const imp of entry.imports) {
2098
+ if (imp === "index.html" || imp.endsWith(".html")) continue;
2099
+ this.collectChunksRecursive(imp, chunks, visited);
2096
2100
  }
2097
- return [];
2098
2101
  }
2099
2102
  /**
2100
2103
  * Collect modulepreload links for a route and its parent chain.
@@ -2140,10 +2143,10 @@ var SSRManifestProvider = class {
2140
2143
  return Array.from(allChunks);
2141
2144
  }
2142
2145
  /**
2143
- * Check if manifests are loaded and available.
2146
+ * Check if manifest is loaded and available.
2144
2147
  */
2145
2148
  isAvailable() {
2146
- return this.clientManifest !== void 0 || this.ssrManifest !== void 0;
2149
+ return this.clientManifest !== void 0;
2147
2150
  }
2148
2151
  /**
2149
2152
  * Cached entry assets - computed once at first access.
@@ -2162,8 +2165,8 @@ var SSRManifestProvider = class {
2162
2165
  if (!this.clientManifest) return null;
2163
2166
  for (const [key, entry] of Object.entries(this.clientManifest)) if (entry.isEntry) {
2164
2167
  this.cachedEntryAssets = {
2165
- js: "/" + entry.file,
2166
- css: entry.css?.map((css) => "/" + css) ?? []
2168
+ js: `/${entry.file}`,
2169
+ css: entry.css?.map((css) => `/${css}`) ?? []
2167
2170
  };
2168
2171
  return this.cachedEntryAssets;
2169
2172
  }
@@ -2223,7 +2226,6 @@ var ReactServerProvider = class {
2223
2226
  serverHeadProvider = $inject(ServerHeadProvider);
2224
2227
  serverStaticProvider = $inject(ServerStaticProvider);
2225
2228
  serverRouterProvider = $inject(ServerRouterProvider);
2226
- serverTimingProvider = $inject(ServerTimingProvider);
2227
2229
  ssrManifestProvider = $inject(SSRManifestProvider);
2228
2230
  /**
2229
2231
  * Cached check for ServerLinksProvider - avoids has() lookup per request.
@@ -2238,13 +2240,8 @@ var ReactServerProvider = class {
2238
2240
  handler: async () => {
2239
2241
  const ssrEnabled = this.alepha.primitives($page).length > 0 && this.env.REACT_SSR_ENABLED !== false;
2240
2242
  this.alepha.store.set("alepha.react.server.ssr", ssrEnabled);
2241
- if (ssrEnabled) this.log.info("SSR streaming enabled");
2242
- if (this.alepha.isViteDev()) {
2243
- await this.configureVite(ssrEnabled);
2244
- return;
2245
- }
2246
2243
  let root = "";
2247
- if (!this.alepha.isServerless()) {
2244
+ if (!this.alepha.isServerless() && !this.alepha.isViteDev()) {
2248
2245
  root = await this.getPublicDirectory();
2249
2246
  if (!root) this.log.warn("Missing static files, static file server will be disabled");
2250
2247
  else {
@@ -2317,7 +2314,7 @@ var ReactServerProvider = class {
2317
2314
  for (const css of assets.css) parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
2318
2315
  if (assets.js) parts.push(`<script type="module" crossorigin="" src="${assets.js}"><\/script>`);
2319
2316
  if (parts.length > 0) {
2320
- this.templateProvider.setEarlyHeadContent(parts.join("\n") + "\n", assets);
2317
+ this.templateProvider.setEarlyHeadContent(`${parts.join("\n")}\n`, assets);
2321
2318
  this.log.debug("Early head content set", {
2322
2319
  css: assets.css.length,
2323
2320
  js: assets.js ? 1 : 0
@@ -2346,15 +2343,6 @@ var ReactServerProvider = class {
2346
2343
  });
2347
2344
  }
2348
2345
  /**
2349
- * Configure Vite for SSR in development mode.
2350
- */
2351
- async configureVite(ssrEnabled) {
2352
- if (!ssrEnabled) return;
2353
- const url = `http://localhost:${this.alepha.env.SERVER_PORT ?? "5173"}`;
2354
- this.log.info("SSR (dev) OK", { url });
2355
- await this.registerPages(() => fetch(`${url}/index.html`).then((it) => it.text()).catch(() => void 0));
2356
- }
2357
- /**
2358
2346
  * Create the request handler for a page route.
2359
2347
  */
2360
2348
  createHandler(route, templateLoader) {
@@ -2399,10 +2387,7 @@ var ReactServerProvider = class {
2399
2387
  const globalHead = this.serverHeadProvider.resolveGlobalHead();
2400
2388
  const htmlStream = this.templateProvider.createEarlyHtmlStream(globalHead, async () => {
2401
2389
  const result = await this.renderPage(route, state);
2402
- if (result.redirect) {
2403
- reply.redirect(result.redirect);
2404
- return null;
2405
- }
2390
+ if (result.redirect) return { redirect: result.redirect };
2406
2391
  return {
2407
2392
  state,
2408
2393
  reactStream: result.reactStream
@@ -2411,7 +2396,6 @@ var ReactServerProvider = class {
2411
2396
  hydration: true,
2412
2397
  onError: (error) => {
2413
2398
  if (error instanceof Redirection) this.log.debug("Streaming resulted in redirection", { redirect: error.redirect });
2414
- else this.log.error("HTML stream error", error);
2415
2399
  }
2416
2400
  });
2417
2401
  this.log.trace("Page streaming started (early head optimization)");
@@ -2434,9 +2418,7 @@ var ReactServerProvider = class {
2434
2418
  * @returns Render result with redirect or React stream
2435
2419
  */
2436
2420
  async renderPage(route, state) {
2437
- this.serverTimingProvider.beginTiming("createLayers");
2438
2421
  const { redirect } = await this.pageApi.createLayers(route, state);
2439
- this.serverTimingProvider.endTiming("createLayers");
2440
2422
  if (redirect) {
2441
2423
  this.log.debug("Resolver resulted in redirection", { redirect });
2442
2424
  return { redirect };
@@ -2447,15 +2429,11 @@ var ReactServerProvider = class {
2447
2429
  state.head ??= {};
2448
2430
  state.head.link = [...state.head.link ?? [], ...preloadLinks];
2449
2431
  }
2450
- this.serverTimingProvider.beginTiming("renderToStream");
2451
2432
  const element = this.pageApi.root(state);
2452
2433
  this.alepha.store.set("alepha.react.router.state", state);
2453
- const reactStream = await renderToReadableStream(element, { onError: (error) => {
2434
+ return { reactStream: await renderToReadableStream(element, { onError: (error) => {
2454
2435
  if (error instanceof Redirection) this.log.warn("Redirect during streaming ignored", { redirect: error.redirect });
2455
- else this.log.error("Streaming render error", error);
2456
- } });
2457
- this.serverTimingProvider.endTiming("renderToStream");
2458
- return { reactStream };
2436
+ } }) };
2459
2437
  }
2460
2438
  /**
2461
2439
  * For testing purposes, renders a page to HTML string.
@@ -2499,78 +2477,465 @@ var ReactServerProvider = class {
2499
2477
  state,
2500
2478
  html
2501
2479
  });
2502
- return {
2503
- state,
2504
- html
2480
+ return {
2481
+ state,
2482
+ html
2483
+ };
2484
+ }
2485
+ /**
2486
+ * Collect a ReadableStream into a string.
2487
+ */
2488
+ async streamToString(stream) {
2489
+ const reader = stream.getReader();
2490
+ const decoder = new TextDecoder();
2491
+ const chunks = [];
2492
+ try {
2493
+ while (true) {
2494
+ const { done, value } = await reader.read();
2495
+ if (done) break;
2496
+ chunks.push(decoder.decode(value, { stream: true }));
2497
+ }
2498
+ chunks.push(decoder.decode());
2499
+ } finally {
2500
+ reader.releaseLock();
2501
+ }
2502
+ return chunks.join("");
2503
+ }
2504
+ };
2505
+ const envSchema = t.object({ REACT_SSR_ENABLED: t.optional(t.boolean()) });
2506
+ /**
2507
+ * React server provider configuration atom
2508
+ */
2509
+ const reactServerOptions = $atom({
2510
+ name: "alepha.react.server.options",
2511
+ schema: t.object({
2512
+ publicDir: t.string(),
2513
+ staticServer: t.object({
2514
+ disabled: t.boolean(),
2515
+ path: t.string({ description: "URL path where static files will be served." })
2516
+ })
2517
+ }),
2518
+ default: {
2519
+ publicDir: "public",
2520
+ staticServer: {
2521
+ disabled: false,
2522
+ path: "/"
2523
+ }
2524
+ }
2525
+ });
2526
+
2527
+ //#endregion
2528
+ //#region ../../src/router/services/ReactPageServerService.ts
2529
+ /**
2530
+ * $page methods for server-side.
2531
+ */
2532
+ var ReactPageServerService = class extends ReactPageService {
2533
+ reactServerProvider = $inject(ReactServerProvider);
2534
+ templateProvider = $inject(ReactServerTemplateProvider);
2535
+ serverProvider = $inject(ServerProvider);
2536
+ async render(name, options = {}) {
2537
+ return this.reactServerProvider.render(name, options);
2538
+ }
2539
+ async fetch(pathname, options = {}) {
2540
+ const response = await fetch(`${this.serverProvider.hostname}/${pathname}`);
2541
+ const html = await response.text();
2542
+ if (options?.html) return {
2543
+ html,
2544
+ response
2545
+ };
2546
+ const rootContent = this.templateProvider.extractRootContent(html);
2547
+ if (rootContent !== void 0) return {
2548
+ html: rootContent,
2549
+ response
2550
+ };
2551
+ throw new AlephaError("Invalid HTML response");
2552
+ }
2553
+ };
2554
+
2555
+ //#endregion
2556
+ //#region ../../src/router/providers/ReactBrowserRouterProvider.ts
2557
+ /**
2558
+ * Implementation of AlephaRouter for React in browser environment.
2559
+ */
2560
+ var ReactBrowserRouterProvider = class extends RouterProvider {
2561
+ log = $logger();
2562
+ alepha = $inject(Alepha);
2563
+ pageApi = $inject(ReactPageProvider);
2564
+ browserHeadProvider = $inject(BrowserHeadProvider);
2565
+ add(entry) {
2566
+ this.pageApi.add(entry);
2567
+ }
2568
+ configure = $hook({
2569
+ on: "configure",
2570
+ handler: async () => {
2571
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
2572
+ path: page.match,
2573
+ page
2574
+ });
2575
+ }
2576
+ });
2577
+ async transition(url, previous = [], meta = {}) {
2578
+ const { pathname, search } = url;
2579
+ const state = {
2580
+ url,
2581
+ query: {},
2582
+ params: {},
2583
+ layers: [],
2584
+ onError: () => null,
2585
+ meta
2586
+ };
2587
+ await this.alepha.events.emit("react:action:begin", { type: "transition" });
2588
+ await this.alepha.events.emit("react:transition:begin", {
2589
+ previous: this.alepha.store.get("alepha.react.router.state"),
2590
+ state
2591
+ });
2592
+ try {
2593
+ const { route, params } = this.match(pathname);
2594
+ const query = {};
2595
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
2596
+ state.name = route?.page.name;
2597
+ state.query = query;
2598
+ state.params = params ?? {};
2599
+ if (isPageRoute(route)) {
2600
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
2601
+ if (redirect) return redirect;
2602
+ }
2603
+ if (state.layers.length === 0) state.layers.push({
2604
+ name: "not-found",
2605
+ element: createElement(NotFound_default),
2606
+ index: 0,
2607
+ path: "/"
2608
+ });
2609
+ await this.alepha.events.emit("react:action:success", { type: "transition" });
2610
+ await this.alepha.events.emit("react:transition:success", { state });
2611
+ } catch (e) {
2612
+ this.log.error("Transition has failed", e);
2613
+ state.layers = [{
2614
+ name: "error",
2615
+ element: this.pageApi.renderError(e),
2616
+ index: 0,
2617
+ path: "/"
2618
+ }];
2619
+ await this.alepha.events.emit("react:action:error", {
2620
+ type: "transition",
2621
+ error: e
2622
+ });
2623
+ await this.alepha.events.emit("react:transition:error", {
2624
+ error: e,
2625
+ state
2626
+ });
2627
+ }
2628
+ if (previous) for (let i = 0; i < previous.length; i++) {
2629
+ const layer = previous[i];
2630
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
2631
+ }
2632
+ this.alepha.store.set("alepha.react.router.state", state);
2633
+ await this.alepha.events.emit("react:action:end", { type: "transition" });
2634
+ await this.alepha.events.emit("react:transition:end", { state });
2635
+ this.browserHeadProvider.fillAndRenderHead(state);
2636
+ }
2637
+ root(state) {
2638
+ return this.pageApi.root(state);
2639
+ }
2640
+ };
2641
+
2642
+ //#endregion
2643
+ //#region ../../src/router/providers/ReactBrowserProvider.ts
2644
+ /**
2645
+ * React browser renderer configuration atom
2646
+ */
2647
+ const reactBrowserOptions = $atom({
2648
+ name: "alepha.react.browser.options",
2649
+ schema: t.object({ scrollRestoration: t.enum(["top", "manual"]) }),
2650
+ default: { scrollRestoration: "top" }
2651
+ });
2652
+ var ReactBrowserProvider = class {
2653
+ log = $logger();
2654
+ client = $inject(LinkProvider);
2655
+ alepha = $inject(Alepha);
2656
+ router = $inject(ReactBrowserRouterProvider);
2657
+ dateTimeProvider = $inject(DateTimeProvider);
2658
+ browserHeadProvider = $inject(BrowserHeadProvider);
2659
+ options = $use(reactBrowserOptions);
2660
+ get rootId() {
2661
+ return "root";
2662
+ }
2663
+ getRootElement() {
2664
+ const root = this.document.getElementById(this.rootId);
2665
+ if (root) return root;
2666
+ const div = this.document.createElement("div");
2667
+ div.id = this.rootId;
2668
+ this.document.body.prepend(div);
2669
+ return div;
2670
+ }
2671
+ transitioning;
2672
+ get state() {
2673
+ return this.alepha.store.get("alepha.react.router.state");
2674
+ }
2675
+ /**
2676
+ * Accessor for Document DOM API.
2677
+ */
2678
+ get document() {
2679
+ return window.document;
2680
+ }
2681
+ /**
2682
+ * Accessor for History DOM API.
2683
+ */
2684
+ get history() {
2685
+ return window.history;
2686
+ }
2687
+ /**
2688
+ * Accessor for Location DOM API.
2689
+ */
2690
+ get location() {
2691
+ return window.location;
2692
+ }
2693
+ get base() {
2694
+ const base = import.meta.env?.BASE_URL;
2695
+ if (!base || base === "/") return "";
2696
+ return base;
2697
+ }
2698
+ get url() {
2699
+ const url = this.location.pathname + this.location.search;
2700
+ if (this.base) return url.replace(this.base, "");
2701
+ return url;
2702
+ }
2703
+ pushState(path, replace) {
2704
+ const url = this.base + path;
2705
+ if (replace) this.history.replaceState({}, "", url);
2706
+ else this.history.pushState({}, "", url);
2707
+ }
2708
+ async invalidate(props) {
2709
+ const previous = [];
2710
+ this.log.trace("Invalidating layers");
2711
+ if (props) {
2712
+ const [key] = Object.keys(props);
2713
+ const value = props[key];
2714
+ for (const layer of this.state.layers) {
2715
+ if (layer.props?.[key]) {
2716
+ previous.push({
2717
+ ...layer,
2718
+ props: {
2719
+ ...layer.props,
2720
+ [key]: value
2721
+ }
2722
+ });
2723
+ break;
2724
+ }
2725
+ previous.push(layer);
2726
+ }
2727
+ }
2728
+ await this.render({ previous });
2729
+ }
2730
+ async go(url, options = {}) {
2731
+ this.log.trace(`Going to ${url}`, {
2732
+ url,
2733
+ options
2734
+ });
2735
+ await this.render({
2736
+ url,
2737
+ previous: options.force ? [] : this.state.layers,
2738
+ meta: options.meta
2739
+ });
2740
+ if (this.state.url.pathname + this.state.url.search !== url) {
2741
+ this.pushState(this.state.url.pathname + this.state.url.search);
2742
+ return;
2743
+ }
2744
+ this.pushState(url, options.replace);
2745
+ }
2746
+ async render(options = {}) {
2747
+ const previous = options.previous ?? this.state.layers;
2748
+ const url = options.url ?? this.url;
2749
+ const start = this.dateTimeProvider.now();
2750
+ this.transitioning = {
2751
+ to: url,
2752
+ from: this.state?.url.pathname
2505
2753
  };
2754
+ this.log.debug("Transitioning...", { to: url });
2755
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
2756
+ if (redirect) {
2757
+ this.log.info("Redirecting to", { redirect });
2758
+ if (redirect.startsWith("http")) window.location.href = redirect;
2759
+ else return await this.render({ url: redirect });
2760
+ }
2761
+ const ms = this.dateTimeProvider.now().diff(start);
2762
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
2763
+ this.transitioning = void 0;
2506
2764
  }
2507
2765
  /**
2508
- * Collect a ReadableStream into a string.
2766
+ * Get embedded layers from the server.
2509
2767
  */
2510
- async streamToString(stream) {
2511
- const reader = stream.getReader();
2512
- const decoder = new TextDecoder();
2513
- const chunks = [];
2768
+ getHydrationState() {
2514
2769
  try {
2515
- while (true) {
2516
- const { done, value } = await reader.read();
2517
- if (done) break;
2518
- chunks.push(decoder.decode(value, { stream: true }));
2519
- }
2520
- chunks.push(decoder.decode());
2521
- } finally {
2522
- reader.releaseLock();
2770
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
2771
+ } catch (error) {
2772
+ console.error(error);
2523
2773
  }
2524
- return chunks.join("");
2525
2774
  }
2526
- };
2527
- const envSchema = t.object({ REACT_SSR_ENABLED: t.optional(t.boolean()) });
2528
- /**
2529
- * React server provider configuration atom
2530
- */
2531
- const reactServerOptions = $atom({
2532
- name: "alepha.react.server.options",
2533
- schema: t.object({
2534
- publicDir: t.string(),
2535
- staticServer: t.object({
2536
- disabled: t.boolean(),
2537
- path: t.string({ description: "URL path where static files will be served." })
2538
- })
2539
- }),
2540
- default: {
2541
- publicDir: "public",
2542
- staticServer: {
2543
- disabled: false,
2544
- path: "/"
2775
+ onTransitionEnd = $hook({
2776
+ on: "react:transition:end",
2777
+ handler: () => {
2778
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
2779
+ this.log.trace("Restoring scroll position to top");
2780
+ window.scrollTo(0, 0);
2781
+ }
2545
2782
  }
2546
- }
2547
- });
2783
+ });
2784
+ ready = $hook({
2785
+ on: "ready",
2786
+ handler: async () => {
2787
+ const hydration = this.getHydrationState();
2788
+ const previous = hydration?.layers ?? [];
2789
+ if (hydration) {
2790
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.set(key, value);
2791
+ }
2792
+ await this.render({ previous });
2793
+ const element = this.router.root(this.state);
2794
+ await this.alepha.events.emit("react:browser:render", {
2795
+ element,
2796
+ root: this.getRootElement(),
2797
+ hydration,
2798
+ state: this.state
2799
+ });
2800
+ this.browserHeadProvider.fillAndRenderHead(this.state);
2801
+ window.addEventListener("popstate", () => {
2802
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
2803
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
2804
+ this.render();
2805
+ });
2806
+ }
2807
+ });
2808
+ };
2548
2809
 
2549
2810
  //#endregion
2550
- //#region ../../src/router/services/ReactPageServerService.ts
2811
+ //#region ../../src/router/services/ReactRouter.ts
2551
2812
  /**
2552
- * $page methods for server-side.
2813
+ * Friendly browser router API.
2814
+ *
2815
+ * Can be safely used server-side, but most methods will be no-op.
2553
2816
  */
2554
- var ReactPageServerService = class extends ReactPageService {
2555
- reactServerProvider = $inject(ReactServerProvider);
2556
- templateProvider = $inject(ReactServerTemplateProvider);
2557
- serverProvider = $inject(ServerProvider);
2558
- async render(name, options = {}) {
2559
- return this.reactServerProvider.render(name, options);
2817
+ var ReactRouter = class {
2818
+ alepha = $inject(Alepha);
2819
+ pageApi = $inject(ReactPageProvider);
2820
+ get state() {
2821
+ return this.alepha.store.get("alepha.react.router.state");
2560
2822
  }
2561
- async fetch(pathname, options = {}) {
2562
- const response = await fetch(`${this.serverProvider.hostname}/${pathname}`);
2563
- const html = await response.text();
2564
- if (options?.html) return {
2565
- html,
2566
- response
2823
+ get pages() {
2824
+ return this.pageApi.getPages();
2825
+ }
2826
+ get concretePages() {
2827
+ return this.pageApi.getConcretePages();
2828
+ }
2829
+ get browser() {
2830
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
2831
+ }
2832
+ isActive(href, options = {}) {
2833
+ const current = this.state.url.pathname;
2834
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
2835
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
2836
+ return isActive;
2837
+ }
2838
+ node(name, config = {}) {
2839
+ const page = this.pageApi.page(name);
2840
+ if (!page.lazy && !page.component) return {
2841
+ ...page,
2842
+ label: page.label ?? page.name,
2843
+ children: void 0
2567
2844
  };
2568
- const rootContent = this.templateProvider.extractRootContent(html);
2569
- if (rootContent !== void 0) return {
2570
- html: rootContent,
2571
- response
2845
+ return {
2846
+ ...page,
2847
+ label: page.label ?? page.name,
2848
+ href: this.path(name, config),
2849
+ children: void 0
2572
2850
  };
2573
- throw new AlephaError("Invalid HTML response");
2851
+ }
2852
+ path(name, config = {}) {
2853
+ return this.pageApi.pathname(name, {
2854
+ params: {
2855
+ ...this.state?.params,
2856
+ ...config.params
2857
+ },
2858
+ query: config.query
2859
+ });
2860
+ }
2861
+ /**
2862
+ * Reload the current page.
2863
+ * This is equivalent to calling `go()` with the current pathname and search.
2864
+ */
2865
+ async reload() {
2866
+ if (!this.browser) return;
2867
+ await this.go(this.location.pathname + this.location.search, {
2868
+ replace: true,
2869
+ force: true
2870
+ });
2871
+ }
2872
+ getURL() {
2873
+ if (!this.browser) return this.state.url;
2874
+ return new URL(this.location.href);
2875
+ }
2876
+ get location() {
2877
+ if (!this.browser) throw new Error("Browser is required");
2878
+ return this.browser.location;
2879
+ }
2880
+ get current() {
2881
+ return this.state;
2882
+ }
2883
+ get pathname() {
2884
+ return this.state.url.pathname;
2885
+ }
2886
+ get query() {
2887
+ const query = {};
2888
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
2889
+ return query;
2890
+ }
2891
+ async back() {
2892
+ this.browser?.history.back();
2893
+ }
2894
+ async forward() {
2895
+ this.browser?.history.forward();
2896
+ }
2897
+ async invalidate(props) {
2898
+ await this.browser?.invalidate(props);
2899
+ }
2900
+ async go(path, options) {
2901
+ for (const page of this.pages) if (page.name === path) {
2902
+ await this.browser?.go(this.path(path, options), options);
2903
+ return;
2904
+ }
2905
+ await this.browser?.go(path, options);
2906
+ }
2907
+ anchor(path, options = {}) {
2908
+ let href = path;
2909
+ for (const page of this.pages) if (page.name === path) {
2910
+ href = this.path(path, options);
2911
+ break;
2912
+ }
2913
+ return {
2914
+ href: this.base(href),
2915
+ onClick: (ev) => {
2916
+ ev.stopPropagation();
2917
+ ev.preventDefault();
2918
+ this.go(href, options).catch(console.error);
2919
+ }
2920
+ };
2921
+ }
2922
+ base(path) {
2923
+ const base = import.meta.env?.BASE_URL;
2924
+ if (!base || base === "/") return path;
2925
+ return base + path;
2926
+ }
2927
+ /**
2928
+ * Set query params.
2929
+ *
2930
+ * @param record
2931
+ * @param options
2932
+ */
2933
+ setQueryParams(record, options = {}) {
2934
+ const func = typeof record === "function" ? record : () => record;
2935
+ const search = new URLSearchParams(func(this.query)).toString();
2936
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
2937
+ if (options.push) window.history.pushState({}, "", state);
2938
+ else window.history.replaceState({}, "", state);
2574
2939
  }
2575
2940
  };
2576
2941