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