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