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