@alepha/react 0.14.1 → 0.14.3
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/dist/auth/index.browser.js +1488 -4
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1827 -4
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +58 -937
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +139 -2014
- package/dist/core/index.js.map +1 -1
- package/dist/form/index.d.ts.map +1 -1
- package/dist/form/index.js +6 -1
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.browser.js +3 -1
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +552 -8
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +17 -2
- package/dist/head/index.js.map +1 -1
- package/dist/{core → router}/index.browser.js +126 -516
- package/dist/router/index.browser.js.map +1 -0
- package/dist/router/index.d.ts +1334 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1939 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +12 -6
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/services/ReactAuth.ts +1 -1
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/components/ClientOnly.tsx +14 -0
- package/src/core/components/ErrorBoundary.tsx +3 -2
- package/src/core/contexts/AlephaContext.ts +3 -0
- package/src/core/contexts/AlephaProvider.tsx +2 -1
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/core/index.ts +13 -102
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/form/services/FormModel.ts +5 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +18 -8
- package/src/head/interfaces/Head.ts +3 -0
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/HeadProvider.ts +6 -1
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/head/providers/ServerHeadProvider.ts +20 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/{core → router}/components/ErrorViewer.tsx +2 -0
- package/src/router/components/Link.tsx +21 -0
- package/src/{core → router}/components/NestedView.tsx +3 -5
- package/src/router/components/NotFound.tsx +30 -0
- package/src/router/errors/Redirection.ts +28 -0
- package/src/{core → router}/hooks/useActive.ts +6 -2
- package/src/{core → router}/hooks/useQueryParams.ts +2 -2
- package/src/{core → router}/hooks/useRouter.ts +1 -1
- package/src/{core → router}/hooks/useRouterState.ts +1 -1
- package/src/{core → router}/index.browser.ts +14 -12
- package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
- package/src/router/index.ts +125 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/{core → router}/primitives/$page.ts +1 -1
- package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
- package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
- package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
- package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
- package/src/{core → router}/services/ReactPageServerService.ts +3 -0
- package/src/{core → router}/services/ReactPageService.ts +5 -5
- package/src/{core → router}/services/ReactRouter.ts +26 -5
- package/dist/core/index.browser.js.map +0 -1
- package/dist/core/index.native.js +0 -403
- package/dist/core/index.native.js.map +0 -1
- package/src/core/components/Link.tsx +0 -18
- package/src/core/components/NotFound.tsx +0 -27
- package/src/core/errors/Redirection.ts +0 -13
- package/src/core/hooks/useSchema.ts +0 -88
- package/src/core/index.native.ts +0 -21
- package/src/core/index.shared.ts +0 -9
- /package/src/{core → router}/contexts/RouterLayerContext.ts +0 -0
|
@@ -1,10 +1,1494 @@
|
|
|
1
|
-
import { $hook, $inject, $module, Alepha } from "alepha";
|
|
2
|
-
import {
|
|
1
|
+
import { $atom, $env, $hook, $inject, $module, $use, Alepha, AlephaError, KIND, Primitive, createPrimitive, t } from "alepha";
|
|
2
|
+
import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
|
|
3
3
|
import { $logger } from "alepha/logger";
|
|
4
|
-
import {
|
|
4
|
+
import { AlephaServerLinks, LinkProvider } from "alepha/server/links";
|
|
5
|
+
import { RouterProvider } from "alepha/router";
|
|
6
|
+
import { StrictMode, createContext, createElement, memo, use, useRef, useState } from "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, useAlepha, useEvents, useStore } from "@alepha/react";
|
|
9
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
10
|
+
import { AlephaServer, HttpClient } from "alepha/server";
|
|
5
11
|
import { alephaServerAuthRoutes, tokenResponseSchema, userinfoResponseSchema } from "alepha/server/auth";
|
|
6
|
-
import { LinkProvider } from "alepha/server/links";
|
|
7
12
|
|
|
13
|
+
//#region ../../src/router/services/ReactPageService.ts
|
|
14
|
+
/**
|
|
15
|
+
* $page methods interface.
|
|
16
|
+
*/
|
|
17
|
+
var ReactPageService = class {
|
|
18
|
+
fetch(pathname, options = {}) {
|
|
19
|
+
throw new AlephaError("Fetch is not available for this environment.");
|
|
20
|
+
}
|
|
21
|
+
render(name, options = {}) {
|
|
22
|
+
throw new AlephaError("Render is not available for this environment.");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region ../../src/router/primitives/$page.ts
|
|
28
|
+
/**
|
|
29
|
+
* Main primitive for defining a React route in the application.
|
|
30
|
+
*
|
|
31
|
+
* The $page primitive is the core building block for creating type-safe, SSR-enabled React routes.
|
|
32
|
+
* It provides a declarative way to define pages with powerful features:
|
|
33
|
+
*
|
|
34
|
+
* **Routing & Navigation**
|
|
35
|
+
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
36
|
+
* - Nested routing with parent-child relationships
|
|
37
|
+
* - Type-safe URL parameter and query string validation
|
|
38
|
+
*
|
|
39
|
+
* **Data Loading**
|
|
40
|
+
* - Server-side data fetching with the `resolve` function
|
|
41
|
+
* - Automatic serialization and hydration for SSR
|
|
42
|
+
* - Access to request context, URL params, and parent data
|
|
43
|
+
*
|
|
44
|
+
* **Component Loading**
|
|
45
|
+
* - Direct component rendering or lazy loading for code splitting
|
|
46
|
+
* - Client-only rendering when browser APIs are needed
|
|
47
|
+
* - Automatic fallback handling during hydration
|
|
48
|
+
*
|
|
49
|
+
* **Performance Optimization**
|
|
50
|
+
* - Static generation for pre-rendered pages at build time
|
|
51
|
+
* - Server-side caching with configurable TTL and providers
|
|
52
|
+
* - Code splitting through lazy component loading
|
|
53
|
+
*
|
|
54
|
+
* **Error Handling**
|
|
55
|
+
* - Custom error handlers with support for redirects
|
|
56
|
+
* - Hierarchical error handling (child → parent)
|
|
57
|
+
* - HTTP status code handling (404, 401, etc.)
|
|
58
|
+
*
|
|
59
|
+
* **Page Animations**
|
|
60
|
+
* - CSS-based enter/exit animations
|
|
61
|
+
* - Dynamic animations based on page state
|
|
62
|
+
* - Custom timing and easing functions
|
|
63
|
+
*
|
|
64
|
+
* **Lifecycle Management**
|
|
65
|
+
* - Server response hooks for headers and status codes
|
|
66
|
+
* - Page leave handlers for cleanup (browser only)
|
|
67
|
+
* - Permission-based access control
|
|
68
|
+
*
|
|
69
|
+
* @example Simple page with data fetching
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const userProfile = $page({
|
|
72
|
+
* path: "/users/:id",
|
|
73
|
+
* schema: {
|
|
74
|
+
* params: t.object({ id: t.integer() }),
|
|
75
|
+
* query: t.object({ tab: t.optional(t.text()) })
|
|
76
|
+
* },
|
|
77
|
+
* resolve: async ({ params }) => {
|
|
78
|
+
* const user = await userApi.getUser(params.id);
|
|
79
|
+
* return { user };
|
|
80
|
+
* },
|
|
81
|
+
* lazy: () => import("./UserProfile.tsx")
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example Nested routing with error handling
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const projectSection = $page({
|
|
88
|
+
* path: "/projects/:id",
|
|
89
|
+
* children: () => [projectBoard, projectSettings],
|
|
90
|
+
* resolve: async ({ params }) => {
|
|
91
|
+
* const project = await projectApi.get(params.id);
|
|
92
|
+
* return { project };
|
|
93
|
+
* },
|
|
94
|
+
* errorHandler: (error) => {
|
|
95
|
+
* if (HttpError.is(error, 404)) {
|
|
96
|
+
* return <ProjectNotFound />;
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @example Static generation with caching
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const blogPost = $page({
|
|
105
|
+
* path: "/blog/:slug",
|
|
106
|
+
* static: {
|
|
107
|
+
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
108
|
+
* },
|
|
109
|
+
* resolve: async ({ params }) => {
|
|
110
|
+
* const post = await loadPost(params.slug);
|
|
111
|
+
* return { post };
|
|
112
|
+
* }
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
const $page = (options) => {
|
|
117
|
+
return createPrimitive(PagePrimitive, options);
|
|
118
|
+
};
|
|
119
|
+
var PagePrimitive = class extends Primitive {
|
|
120
|
+
reactPageService = $inject(ReactPageService);
|
|
121
|
+
onInit() {
|
|
122
|
+
if (this.options.static) this.options.cache ??= { store: {
|
|
123
|
+
provider: "memory",
|
|
124
|
+
ttl: [1, "week"]
|
|
125
|
+
} };
|
|
126
|
+
}
|
|
127
|
+
get name() {
|
|
128
|
+
return this.options.name ?? this.config.propertyKey;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* For testing or build purposes.
|
|
132
|
+
*
|
|
133
|
+
* This will render the page (HTML layout included or not) and return the HTML + context.
|
|
134
|
+
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
135
|
+
*/
|
|
136
|
+
async render(options) {
|
|
137
|
+
return this.reactPageService.render(this.name, options);
|
|
138
|
+
}
|
|
139
|
+
async fetch(options) {
|
|
140
|
+
return this.reactPageService.fetch(this.options.path || "", options);
|
|
141
|
+
}
|
|
142
|
+
match(url) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
pathname(config) {
|
|
146
|
+
return this.options.path || "";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
$page[KIND] = PagePrimitive;
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region ../../src/router/components/NotFound.tsx
|
|
153
|
+
/**
|
|
154
|
+
* Default 404 Not Found page component.
|
|
155
|
+
*/
|
|
156
|
+
const NotFound = (props) => /* @__PURE__ */ jsxs("div", {
|
|
157
|
+
style: {
|
|
158
|
+
width: "100%",
|
|
159
|
+
minHeight: "90vh",
|
|
160
|
+
boxSizing: "border-box",
|
|
161
|
+
display: "flex",
|
|
162
|
+
flexDirection: "column",
|
|
163
|
+
justifyContent: "center",
|
|
164
|
+
alignItems: "center",
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
|
|
167
|
+
padding: "2rem",
|
|
168
|
+
...props.style
|
|
169
|
+
},
|
|
170
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
171
|
+
style: {
|
|
172
|
+
fontSize: "6rem",
|
|
173
|
+
fontWeight: 200,
|
|
174
|
+
lineHeight: 1
|
|
175
|
+
},
|
|
176
|
+
children: "404"
|
|
177
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
178
|
+
style: {
|
|
179
|
+
fontSize: "0.875rem",
|
|
180
|
+
marginTop: "1rem",
|
|
181
|
+
opacity: .6
|
|
182
|
+
},
|
|
183
|
+
children: "Page not found"
|
|
184
|
+
})]
|
|
185
|
+
});
|
|
186
|
+
var NotFound_default = NotFound;
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region ../../src/router/components/ErrorViewer.tsx
|
|
190
|
+
/**
|
|
191
|
+
* Error viewer component that displays error details in development mode
|
|
192
|
+
*/
|
|
193
|
+
const ErrorViewer = ({ error, alepha }) => {
|
|
194
|
+
const [expanded, setExpanded] = useState(false);
|
|
195
|
+
if (alepha.isProduction()) return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
|
|
196
|
+
const frames = parseStackTrace(error.stack);
|
|
197
|
+
const visibleFrames = expanded ? frames : frames.slice(0, 6);
|
|
198
|
+
const hiddenCount = frames.length - 6;
|
|
199
|
+
return /* @__PURE__ */ jsx("div", {
|
|
200
|
+
style: styles.overlay,
|
|
201
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
202
|
+
style: styles.container,
|
|
203
|
+
children: [/* @__PURE__ */ jsx(Header, { error }), /* @__PURE__ */ jsx(StackTraceSection, {
|
|
204
|
+
frames,
|
|
205
|
+
visibleFrames,
|
|
206
|
+
expanded,
|
|
207
|
+
hiddenCount,
|
|
208
|
+
onToggle: () => setExpanded(!expanded)
|
|
209
|
+
})]
|
|
210
|
+
})
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
var ErrorViewer_default = ErrorViewer;
|
|
214
|
+
/**
|
|
215
|
+
* Parse stack trace string into structured frames
|
|
216
|
+
*/
|
|
217
|
+
function parseStackTrace(stack) {
|
|
218
|
+
if (!stack) return [];
|
|
219
|
+
const lines = stack.split("\n").slice(1);
|
|
220
|
+
const frames = [];
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
const trimmed = line.trim();
|
|
223
|
+
if (!trimmed.startsWith("at ")) continue;
|
|
224
|
+
const frame = parseStackLine(trimmed);
|
|
225
|
+
if (frame) frames.push(frame);
|
|
226
|
+
}
|
|
227
|
+
return frames;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Parse a single stack trace line into a structured frame
|
|
231
|
+
*/
|
|
232
|
+
function parseStackLine(line) {
|
|
233
|
+
const withFn = line.match(/^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/);
|
|
234
|
+
if (withFn) return {
|
|
235
|
+
fn: withFn[1],
|
|
236
|
+
file: withFn[2],
|
|
237
|
+
line: withFn[3],
|
|
238
|
+
col: withFn[4],
|
|
239
|
+
raw: line
|
|
240
|
+
};
|
|
241
|
+
const withoutFn = line.match(/^at\s+(.+):(\d+):(\d+)$/);
|
|
242
|
+
if (withoutFn) return {
|
|
243
|
+
fn: "<anonymous>",
|
|
244
|
+
file: withoutFn[1],
|
|
245
|
+
line: withoutFn[2],
|
|
246
|
+
col: withoutFn[3],
|
|
247
|
+
raw: line
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
fn: "",
|
|
251
|
+
file: line.replace(/^at\s+/, ""),
|
|
252
|
+
line: "",
|
|
253
|
+
col: "",
|
|
254
|
+
raw: line
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Copy text to clipboard
|
|
259
|
+
*/
|
|
260
|
+
function copyToClipboard(text) {
|
|
261
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
262
|
+
console.error("Clipboard error:", err);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Header section with error type and message
|
|
267
|
+
*/
|
|
268
|
+
function Header({ error }) {
|
|
269
|
+
const [copied, setCopied] = useState(false);
|
|
270
|
+
const handleCopy = () => {
|
|
271
|
+
copyToClipboard(error.stack || error.message);
|
|
272
|
+
setCopied(true);
|
|
273
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
274
|
+
};
|
|
275
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
276
|
+
style: styles.header,
|
|
277
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
278
|
+
style: styles.headerTop,
|
|
279
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
280
|
+
style: styles.badge,
|
|
281
|
+
children: error.name
|
|
282
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
283
|
+
type: "button",
|
|
284
|
+
onClick: handleCopy,
|
|
285
|
+
style: styles.copyBtn,
|
|
286
|
+
children: copied ? "Copied" : "Copy Stack"
|
|
287
|
+
})]
|
|
288
|
+
}), /* @__PURE__ */ jsx("h1", {
|
|
289
|
+
style: styles.message,
|
|
290
|
+
children: error.message
|
|
291
|
+
})]
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Stack trace section with expandable frames
|
|
296
|
+
*/
|
|
297
|
+
function StackTraceSection({ frames, visibleFrames, expanded, hiddenCount, onToggle }) {
|
|
298
|
+
if (frames.length === 0) return null;
|
|
299
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
300
|
+
style: styles.stackSection,
|
|
301
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
302
|
+
style: styles.stackHeader,
|
|
303
|
+
children: "Call Stack"
|
|
304
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
305
|
+
style: styles.frameList,
|
|
306
|
+
children: [
|
|
307
|
+
visibleFrames.map((frame, i) => /* @__PURE__ */ jsx(StackFrameRow, {
|
|
308
|
+
frame,
|
|
309
|
+
index: i
|
|
310
|
+
}, i)),
|
|
311
|
+
!expanded && hiddenCount > 0 && /* @__PURE__ */ jsxs("button", {
|
|
312
|
+
type: "button",
|
|
313
|
+
onClick: onToggle,
|
|
314
|
+
style: styles.expandBtn,
|
|
315
|
+
children: [
|
|
316
|
+
"Show ",
|
|
317
|
+
hiddenCount,
|
|
318
|
+
" more frames"
|
|
319
|
+
]
|
|
320
|
+
}),
|
|
321
|
+
expanded && hiddenCount > 0 && /* @__PURE__ */ jsx("button", {
|
|
322
|
+
type: "button",
|
|
323
|
+
onClick: onToggle,
|
|
324
|
+
style: styles.expandBtn,
|
|
325
|
+
children: "Show less"
|
|
326
|
+
})
|
|
327
|
+
]
|
|
328
|
+
})]
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Single stack frame row
|
|
333
|
+
*/
|
|
334
|
+
function StackFrameRow({ frame, index }) {
|
|
335
|
+
const isFirst = index === 0;
|
|
336
|
+
const fileName = frame.file.split("/").pop() || frame.file;
|
|
337
|
+
const dirPath = frame.file.substring(0, frame.file.length - fileName.length);
|
|
338
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
339
|
+
style: {
|
|
340
|
+
...styles.frame,
|
|
341
|
+
...isFirst ? styles.frameFirst : {}
|
|
342
|
+
},
|
|
343
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
344
|
+
style: styles.frameIndex,
|
|
345
|
+
children: index + 1
|
|
346
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
347
|
+
style: styles.frameContent,
|
|
348
|
+
children: [frame.fn && /* @__PURE__ */ jsx("div", {
|
|
349
|
+
style: styles.fnName,
|
|
350
|
+
children: frame.fn
|
|
351
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
352
|
+
style: styles.filePath,
|
|
353
|
+
children: [
|
|
354
|
+
/* @__PURE__ */ jsx("span", {
|
|
355
|
+
style: styles.dirPath,
|
|
356
|
+
children: dirPath
|
|
357
|
+
}),
|
|
358
|
+
/* @__PURE__ */ jsx("span", {
|
|
359
|
+
style: styles.fileName,
|
|
360
|
+
children: fileName
|
|
361
|
+
}),
|
|
362
|
+
frame.line && /* @__PURE__ */ jsxs("span", {
|
|
363
|
+
style: styles.lineCol,
|
|
364
|
+
children: [
|
|
365
|
+
":",
|
|
366
|
+
frame.line,
|
|
367
|
+
":",
|
|
368
|
+
frame.col
|
|
369
|
+
]
|
|
370
|
+
})
|
|
371
|
+
]
|
|
372
|
+
})]
|
|
373
|
+
})]
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Production error view - minimal information
|
|
378
|
+
*/
|
|
379
|
+
function ErrorViewerProduction() {
|
|
380
|
+
return /* @__PURE__ */ jsx("div", {
|
|
381
|
+
style: styles.overlay,
|
|
382
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
383
|
+
style: styles.prodContainer,
|
|
384
|
+
children: [
|
|
385
|
+
/* @__PURE__ */ jsx("div", {
|
|
386
|
+
style: styles.prodIcon,
|
|
387
|
+
children: "!"
|
|
388
|
+
}),
|
|
389
|
+
/* @__PURE__ */ jsx("h1", {
|
|
390
|
+
style: styles.prodTitle,
|
|
391
|
+
children: "Application Error"
|
|
392
|
+
}),
|
|
393
|
+
/* @__PURE__ */ jsx("p", {
|
|
394
|
+
style: styles.prodMessage,
|
|
395
|
+
children: "An unexpected error occurred. Please try again later."
|
|
396
|
+
}),
|
|
397
|
+
/* @__PURE__ */ jsx("button", {
|
|
398
|
+
type: "button",
|
|
399
|
+
onClick: () => window.location.reload(),
|
|
400
|
+
style: styles.prodButton,
|
|
401
|
+
children: "Reload Page"
|
|
402
|
+
})
|
|
403
|
+
]
|
|
404
|
+
})
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const styles = {
|
|
408
|
+
overlay: {
|
|
409
|
+
position: "fixed",
|
|
410
|
+
inset: 0,
|
|
411
|
+
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
412
|
+
display: "flex",
|
|
413
|
+
alignItems: "flex-start",
|
|
414
|
+
justifyContent: "center",
|
|
415
|
+
padding: "40px 20px",
|
|
416
|
+
overflow: "auto",
|
|
417
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
|
|
418
|
+
zIndex: 99999
|
|
419
|
+
},
|
|
420
|
+
container: {
|
|
421
|
+
width: "100%",
|
|
422
|
+
maxWidth: "960px",
|
|
423
|
+
backgroundColor: "#1a1a1a",
|
|
424
|
+
borderRadius: "12px",
|
|
425
|
+
overflow: "hidden",
|
|
426
|
+
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)"
|
|
427
|
+
},
|
|
428
|
+
header: {
|
|
429
|
+
padding: "24px 28px",
|
|
430
|
+
borderBottom: "1px solid #333",
|
|
431
|
+
background: "linear-gradient(to bottom, #1f1f1f, #1a1a1a)"
|
|
432
|
+
},
|
|
433
|
+
headerTop: {
|
|
434
|
+
display: "flex",
|
|
435
|
+
alignItems: "center",
|
|
436
|
+
justifyContent: "space-between",
|
|
437
|
+
marginBottom: "16px"
|
|
438
|
+
},
|
|
439
|
+
badge: {
|
|
440
|
+
display: "inline-block",
|
|
441
|
+
padding: "6px 12px",
|
|
442
|
+
backgroundColor: "#dc2626",
|
|
443
|
+
color: "#fff",
|
|
444
|
+
fontSize: "12px",
|
|
445
|
+
fontWeight: 600,
|
|
446
|
+
borderRadius: "6px",
|
|
447
|
+
letterSpacing: "0.025em"
|
|
448
|
+
},
|
|
449
|
+
copyBtn: {
|
|
450
|
+
padding: "8px 16px",
|
|
451
|
+
backgroundColor: "transparent",
|
|
452
|
+
color: "#888",
|
|
453
|
+
fontSize: "13px",
|
|
454
|
+
fontWeight: 500,
|
|
455
|
+
border: "1px solid #444",
|
|
456
|
+
borderRadius: "6px",
|
|
457
|
+
cursor: "pointer",
|
|
458
|
+
transition: "all 0.15s"
|
|
459
|
+
},
|
|
460
|
+
message: {
|
|
461
|
+
margin: 0,
|
|
462
|
+
fontSize: "20px",
|
|
463
|
+
fontWeight: 500,
|
|
464
|
+
color: "#fff",
|
|
465
|
+
lineHeight: 1.5,
|
|
466
|
+
wordBreak: "break-word"
|
|
467
|
+
},
|
|
468
|
+
stackSection: { padding: "0" },
|
|
469
|
+
stackHeader: {
|
|
470
|
+
padding: "16px 28px",
|
|
471
|
+
fontSize: "11px",
|
|
472
|
+
fontWeight: 600,
|
|
473
|
+
color: "#666",
|
|
474
|
+
textTransform: "uppercase",
|
|
475
|
+
letterSpacing: "0.1em",
|
|
476
|
+
borderBottom: "1px solid #2a2a2a"
|
|
477
|
+
},
|
|
478
|
+
frameList: {
|
|
479
|
+
display: "flex",
|
|
480
|
+
flexDirection: "column"
|
|
481
|
+
},
|
|
482
|
+
frame: {
|
|
483
|
+
display: "flex",
|
|
484
|
+
alignItems: "flex-start",
|
|
485
|
+
padding: "14px 28px",
|
|
486
|
+
borderBottom: "1px solid #252525",
|
|
487
|
+
transition: "background-color 0.15s"
|
|
488
|
+
},
|
|
489
|
+
frameFirst: { backgroundColor: "rgba(220, 38, 38, 0.1)" },
|
|
490
|
+
frameIndex: {
|
|
491
|
+
width: "28px",
|
|
492
|
+
flexShrink: 0,
|
|
493
|
+
fontSize: "12px",
|
|
494
|
+
fontWeight: 500,
|
|
495
|
+
color: "#555",
|
|
496
|
+
fontFamily: "monospace"
|
|
497
|
+
},
|
|
498
|
+
frameContent: {
|
|
499
|
+
flex: 1,
|
|
500
|
+
minWidth: 0
|
|
501
|
+
},
|
|
502
|
+
fnName: {
|
|
503
|
+
fontSize: "14px",
|
|
504
|
+
fontWeight: 500,
|
|
505
|
+
color: "#e5e5e5",
|
|
506
|
+
marginBottom: "4px",
|
|
507
|
+
fontFamily: "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace"
|
|
508
|
+
},
|
|
509
|
+
filePath: {
|
|
510
|
+
fontSize: "13px",
|
|
511
|
+
color: "#888",
|
|
512
|
+
fontFamily: "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace",
|
|
513
|
+
wordBreak: "break-all"
|
|
514
|
+
},
|
|
515
|
+
dirPath: { color: "#555" },
|
|
516
|
+
fileName: { color: "#0ea5e9" },
|
|
517
|
+
lineCol: { color: "#eab308" },
|
|
518
|
+
expandBtn: {
|
|
519
|
+
padding: "16px 28px",
|
|
520
|
+
backgroundColor: "transparent",
|
|
521
|
+
color: "#666",
|
|
522
|
+
fontSize: "13px",
|
|
523
|
+
fontWeight: 500,
|
|
524
|
+
border: "none",
|
|
525
|
+
borderTop: "1px solid #252525",
|
|
526
|
+
cursor: "pointer",
|
|
527
|
+
textAlign: "left",
|
|
528
|
+
transition: "all 0.15s"
|
|
529
|
+
},
|
|
530
|
+
prodContainer: {
|
|
531
|
+
textAlign: "center",
|
|
532
|
+
padding: "60px 40px",
|
|
533
|
+
backgroundColor: "#1a1a1a",
|
|
534
|
+
borderRadius: "12px",
|
|
535
|
+
maxWidth: "400px"
|
|
536
|
+
},
|
|
537
|
+
prodIcon: {
|
|
538
|
+
width: "64px",
|
|
539
|
+
height: "64px",
|
|
540
|
+
margin: "0 auto 24px",
|
|
541
|
+
backgroundColor: "#dc2626",
|
|
542
|
+
borderRadius: "50%",
|
|
543
|
+
display: "flex",
|
|
544
|
+
alignItems: "center",
|
|
545
|
+
justifyContent: "center",
|
|
546
|
+
fontSize: "32px",
|
|
547
|
+
fontWeight: 700,
|
|
548
|
+
color: "#fff"
|
|
549
|
+
},
|
|
550
|
+
prodTitle: {
|
|
551
|
+
margin: "0 0 12px",
|
|
552
|
+
fontSize: "24px",
|
|
553
|
+
fontWeight: 600,
|
|
554
|
+
color: "#fff"
|
|
555
|
+
},
|
|
556
|
+
prodMessage: {
|
|
557
|
+
margin: "0 0 28px",
|
|
558
|
+
fontSize: "15px",
|
|
559
|
+
color: "#888",
|
|
560
|
+
lineHeight: 1.6
|
|
561
|
+
},
|
|
562
|
+
prodButton: {
|
|
563
|
+
padding: "12px 24px",
|
|
564
|
+
backgroundColor: "#fff",
|
|
565
|
+
color: "#000",
|
|
566
|
+
fontSize: "14px",
|
|
567
|
+
fontWeight: 600,
|
|
568
|
+
border: "none",
|
|
569
|
+
borderRadius: "8px",
|
|
570
|
+
cursor: "pointer"
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region ../../src/router/contexts/RouterLayerContext.ts
|
|
576
|
+
const RouterLayerContext = createContext(void 0);
|
|
577
|
+
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region ../../src/router/errors/Redirection.ts
|
|
580
|
+
/**
|
|
581
|
+
* Used for Redirection during the page loading.
|
|
582
|
+
*
|
|
583
|
+
* Depends on the context, it can be thrown or just returned.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```ts
|
|
587
|
+
* import { Redirection } from "@alepha/react";
|
|
588
|
+
*
|
|
589
|
+
* const MyPage = $page({
|
|
590
|
+
* resolve: async () => {
|
|
591
|
+
* if (needRedirect) {
|
|
592
|
+
* throw new Redirection("/new-path");
|
|
593
|
+
* }
|
|
594
|
+
* },
|
|
595
|
+
* });
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
598
|
+
var Redirection = class extends AlephaError {
|
|
599
|
+
redirect;
|
|
600
|
+
constructor(redirect) {
|
|
601
|
+
super("Redirection");
|
|
602
|
+
this.redirect = redirect;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region ../../src/router/hooks/useRouterState.ts
|
|
608
|
+
const useRouterState = () => {
|
|
609
|
+
const [state] = useStore("alepha.react.router.state");
|
|
610
|
+
if (!state) throw new AlephaError("Missing react router state");
|
|
611
|
+
return state;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region ../../src/router/components/NestedView.tsx
|
|
616
|
+
/**
|
|
617
|
+
* A component that renders the current view of the nested router layer.
|
|
618
|
+
*
|
|
619
|
+
* To be simple, it renders the `element` of the current child page of a parent page.
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* ```tsx
|
|
623
|
+
* import { NestedView } from "@alepha/react";
|
|
624
|
+
*
|
|
625
|
+
* class App {
|
|
626
|
+
* parent = $page({
|
|
627
|
+
* component: () => <NestedView />,
|
|
628
|
+
* });
|
|
629
|
+
*
|
|
630
|
+
* child = $page({
|
|
631
|
+
* parent: this.root,
|
|
632
|
+
* component: () => <div>Child Page</div>,
|
|
633
|
+
* });
|
|
634
|
+
* }
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
const NestedView = (props) => {
|
|
638
|
+
const routerLayer = use(RouterLayerContext);
|
|
639
|
+
const index = routerLayer?.index ?? 0;
|
|
640
|
+
const onError = routerLayer?.onError;
|
|
641
|
+
const state = useRouterState();
|
|
642
|
+
const alepha = useAlepha();
|
|
643
|
+
const [view, setView] = useState(state.layers[index]?.element);
|
|
644
|
+
const [animation, setAnimation] = useState("");
|
|
645
|
+
const animationExitDuration = useRef(0);
|
|
646
|
+
const animationExitNow = useRef(0);
|
|
647
|
+
useEvents({
|
|
648
|
+
"react:transition:begin": async ({ previous, state: state$1 }) => {
|
|
649
|
+
const layer = previous.layers[index];
|
|
650
|
+
if (!layer) return;
|
|
651
|
+
if (`${state$1.url.pathname}/`.startsWith(`${layer.path}/`)) return;
|
|
652
|
+
const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
|
|
653
|
+
if (animationExit) {
|
|
654
|
+
const duration = animationExit.duration || 200;
|
|
655
|
+
animationExitNow.current = Date.now();
|
|
656
|
+
animationExitDuration.current = duration;
|
|
657
|
+
setAnimation(animationExit.animation);
|
|
658
|
+
} else {
|
|
659
|
+
animationExitNow.current = 0;
|
|
660
|
+
animationExitDuration.current = 0;
|
|
661
|
+
setAnimation("");
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
"react:transition:end": async ({ state: state$1 }) => {
|
|
665
|
+
const layer = state$1.layers[index];
|
|
666
|
+
if (animationExitNow.current) {
|
|
667
|
+
const duration = animationExitDuration.current;
|
|
668
|
+
const diff = Date.now() - animationExitNow.current;
|
|
669
|
+
if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
|
|
670
|
+
}
|
|
671
|
+
if (!layer?.cache) {
|
|
672
|
+
setView(layer?.element);
|
|
673
|
+
const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
|
|
674
|
+
if (animationEnter) setAnimation(animationEnter.animation);
|
|
675
|
+
else setAnimation("");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}, []);
|
|
679
|
+
let element = view ?? props.children ?? null;
|
|
680
|
+
if (animation) element = /* @__PURE__ */ jsx("div", {
|
|
681
|
+
style: {
|
|
682
|
+
display: "flex",
|
|
683
|
+
flex: 1,
|
|
684
|
+
height: "100%",
|
|
685
|
+
width: "100%",
|
|
686
|
+
position: "relative",
|
|
687
|
+
overflow: "hidden"
|
|
688
|
+
},
|
|
689
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
690
|
+
style: {
|
|
691
|
+
height: "100%",
|
|
692
|
+
width: "100%",
|
|
693
|
+
display: "flex",
|
|
694
|
+
animation
|
|
695
|
+
},
|
|
696
|
+
children: element
|
|
697
|
+
})
|
|
698
|
+
});
|
|
699
|
+
if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
|
|
700
|
+
if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
701
|
+
fallback: props.errorBoundary,
|
|
702
|
+
children: element
|
|
703
|
+
});
|
|
704
|
+
const fallback = (error) => {
|
|
705
|
+
const result = onError?.(error, state) ?? /* @__PURE__ */ jsx(ErrorViewer_default, {
|
|
706
|
+
error,
|
|
707
|
+
alepha
|
|
708
|
+
});
|
|
709
|
+
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
710
|
+
return result;
|
|
711
|
+
};
|
|
712
|
+
return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
713
|
+
fallback,
|
|
714
|
+
children: element
|
|
715
|
+
});
|
|
716
|
+
};
|
|
717
|
+
var NestedView_default = memo(NestedView);
|
|
718
|
+
function parseAnimation(animationLike, state, type = "enter") {
|
|
719
|
+
if (!animationLike) return;
|
|
720
|
+
const DEFAULT_DURATION = 300;
|
|
721
|
+
const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
722
|
+
if (typeof animation === "string") {
|
|
723
|
+
if (type === "exit") return;
|
|
724
|
+
return {
|
|
725
|
+
duration: DEFAULT_DURATION,
|
|
726
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (typeof animation === "object") {
|
|
730
|
+
const anim = animation[type];
|
|
731
|
+
const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
|
|
732
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
733
|
+
if (type === "exit") return {
|
|
734
|
+
duration,
|
|
735
|
+
animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
|
|
736
|
+
};
|
|
737
|
+
return {
|
|
738
|
+
duration,
|
|
739
|
+
animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region ../../src/router/providers/ReactPageProvider.ts
|
|
746
|
+
const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
747
|
+
/**
|
|
748
|
+
* Handle page routes for React applications. (Browser and Server)
|
|
749
|
+
*/
|
|
750
|
+
var ReactPageProvider = class {
|
|
751
|
+
log = $logger();
|
|
752
|
+
env = $env(envSchema$1);
|
|
753
|
+
alepha = $inject(Alepha);
|
|
754
|
+
pages = [];
|
|
755
|
+
getPages() {
|
|
756
|
+
return this.pages;
|
|
757
|
+
}
|
|
758
|
+
getConcretePages() {
|
|
759
|
+
const pages = [];
|
|
760
|
+
for (const page of this.pages) {
|
|
761
|
+
if (page.children && page.children.length > 0) continue;
|
|
762
|
+
const fullPath = this.pathname(page.name);
|
|
763
|
+
if (fullPath.includes(":") || fullPath.includes("*")) {
|
|
764
|
+
if (typeof page.static === "object") {
|
|
765
|
+
const entries = page.static.entries;
|
|
766
|
+
if (entries && entries.length > 0) for (const entry of entries) {
|
|
767
|
+
const params = entry.params;
|
|
768
|
+
const path = this.compile(page.path ?? "", params);
|
|
769
|
+
if (!path.includes(":") && !path.includes("*")) pages.push({
|
|
770
|
+
...page,
|
|
771
|
+
name: params[Object.keys(params)[0]],
|
|
772
|
+
staticName: page.name,
|
|
773
|
+
path,
|
|
774
|
+
...entry
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
pages.push(page);
|
|
781
|
+
}
|
|
782
|
+
return pages;
|
|
783
|
+
}
|
|
784
|
+
page(name) {
|
|
785
|
+
for (const page of this.pages) if (page.name === name) return page;
|
|
786
|
+
throw new AlephaError(`Page '${name}' not found`);
|
|
787
|
+
}
|
|
788
|
+
pathname(name, options = {}) {
|
|
789
|
+
const page = this.page(name);
|
|
790
|
+
if (!page) throw new Error(`Page ${name} not found`);
|
|
791
|
+
let url = page.path ?? "";
|
|
792
|
+
let parent = page.parent;
|
|
793
|
+
while (parent) {
|
|
794
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
795
|
+
parent = parent.parent;
|
|
796
|
+
}
|
|
797
|
+
url = this.compile(url, options.params ?? {});
|
|
798
|
+
if (options.query) {
|
|
799
|
+
const query = new URLSearchParams(options.query);
|
|
800
|
+
if (query.toString()) url += `?${query.toString()}`;
|
|
801
|
+
}
|
|
802
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
803
|
+
}
|
|
804
|
+
url(name, options = {}) {
|
|
805
|
+
return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
|
|
806
|
+
}
|
|
807
|
+
root(state) {
|
|
808
|
+
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
|
|
809
|
+
if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
|
|
810
|
+
return root;
|
|
811
|
+
}
|
|
812
|
+
convertStringObjectToObject = (schema, value) => {
|
|
813
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
814
|
+
for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
|
|
815
|
+
value[key] = this.alepha.codec.decode(schema.properties[key], decodeURIComponent(value[key]));
|
|
816
|
+
} catch (e) {}
|
|
817
|
+
}
|
|
818
|
+
return value;
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Create a new RouterState based on a given route and request.
|
|
822
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
823
|
+
* It also handles errors and redirects.
|
|
824
|
+
*/
|
|
825
|
+
async createLayers(route, state, previous = []) {
|
|
826
|
+
let context = {};
|
|
827
|
+
const stack = [{ route }];
|
|
828
|
+
let parent = route.parent;
|
|
829
|
+
while (parent) {
|
|
830
|
+
stack.unshift({ route: parent });
|
|
831
|
+
parent = parent.parent;
|
|
832
|
+
}
|
|
833
|
+
let forceRefresh = false;
|
|
834
|
+
for (let i = 0; i < stack.length; i++) {
|
|
835
|
+
const it = stack[i];
|
|
836
|
+
const route$1 = it.route;
|
|
837
|
+
const config = {};
|
|
838
|
+
try {
|
|
839
|
+
this.convertStringObjectToObject(route$1.schema?.query, state.query);
|
|
840
|
+
config.query = route$1.schema?.query ? this.alepha.codec.decode(route$1.schema.query, state.query) : {};
|
|
841
|
+
} catch (e) {
|
|
842
|
+
it.error = e;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
config.params = route$1.schema?.params ? this.alepha.codec.decode(route$1.schema.params, state.params) : {};
|
|
847
|
+
} catch (e) {
|
|
848
|
+
it.error = e;
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
it.config = { ...config };
|
|
852
|
+
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
853
|
+
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
854
|
+
if (JSON.stringify({
|
|
855
|
+
part: url(previous[i].part),
|
|
856
|
+
params: previous[i].config?.params ?? {}
|
|
857
|
+
}) === JSON.stringify({
|
|
858
|
+
part: url(route$1.path),
|
|
859
|
+
params: config.params ?? {}
|
|
860
|
+
})) {
|
|
861
|
+
it.props = previous[i].props;
|
|
862
|
+
it.error = previous[i].error;
|
|
863
|
+
it.cache = true;
|
|
864
|
+
context = {
|
|
865
|
+
...context,
|
|
866
|
+
...it.props
|
|
867
|
+
};
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
forceRefresh = true;
|
|
871
|
+
}
|
|
872
|
+
if (!route$1.resolve) continue;
|
|
873
|
+
try {
|
|
874
|
+
const args = Object.create(state);
|
|
875
|
+
Object.assign(args, config, context);
|
|
876
|
+
const props = await route$1.resolve?.(args) ?? {};
|
|
877
|
+
it.props = { ...props };
|
|
878
|
+
context = {
|
|
879
|
+
...context,
|
|
880
|
+
...props
|
|
881
|
+
};
|
|
882
|
+
} catch (e) {
|
|
883
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
884
|
+
this.log.error("Page resolver has failed", e);
|
|
885
|
+
it.error = e;
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
let acc = "";
|
|
890
|
+
for (let i = 0; i < stack.length; i++) {
|
|
891
|
+
const it = stack[i];
|
|
892
|
+
const props = it.props ?? {};
|
|
893
|
+
const params = { ...it.config?.params };
|
|
894
|
+
for (const key of Object.keys(params)) params[key] = String(params[key]);
|
|
895
|
+
acc += "/";
|
|
896
|
+
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
897
|
+
const path = acc.replace(/\/+/, "/");
|
|
898
|
+
const localErrorHandler = this.getErrorHandler(it.route);
|
|
899
|
+
if (localErrorHandler) {
|
|
900
|
+
const onErrorParent = state.onError;
|
|
901
|
+
state.onError = (error, context$1) => {
|
|
902
|
+
const result = localErrorHandler(error, context$1);
|
|
903
|
+
if (result === void 0) return onErrorParent(error, context$1);
|
|
904
|
+
return result;
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
if (!it.error) try {
|
|
908
|
+
const element = await this.createElement(it.route, {
|
|
909
|
+
...it.route.props ? it.route.props() : {},
|
|
910
|
+
...props,
|
|
911
|
+
...context
|
|
912
|
+
});
|
|
913
|
+
state.layers.push({
|
|
914
|
+
name: it.route.name,
|
|
915
|
+
props,
|
|
916
|
+
part: it.route.path,
|
|
917
|
+
config: it.config,
|
|
918
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
919
|
+
index: i + 1,
|
|
920
|
+
path,
|
|
921
|
+
route: it.route,
|
|
922
|
+
cache: it.cache
|
|
923
|
+
});
|
|
924
|
+
} catch (e) {
|
|
925
|
+
it.error = e;
|
|
926
|
+
}
|
|
927
|
+
if (it.error) try {
|
|
928
|
+
let element = await state.onError(it.error, state);
|
|
929
|
+
if (element === void 0) throw it.error;
|
|
930
|
+
if (element instanceof Redirection) return { redirect: element.redirect };
|
|
931
|
+
if (element === null) element = this.renderError(it.error);
|
|
932
|
+
state.layers.push({
|
|
933
|
+
props,
|
|
934
|
+
error: it.error,
|
|
935
|
+
name: it.route.name,
|
|
936
|
+
part: it.route.path,
|
|
937
|
+
config: it.config,
|
|
938
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
939
|
+
index: i + 1,
|
|
940
|
+
path,
|
|
941
|
+
route: it.route
|
|
942
|
+
});
|
|
943
|
+
break;
|
|
944
|
+
} catch (e) {
|
|
945
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return { state };
|
|
950
|
+
}
|
|
951
|
+
getErrorHandler(route) {
|
|
952
|
+
if (route.errorHandler) return route.errorHandler;
|
|
953
|
+
let parent = route.parent;
|
|
954
|
+
while (parent) {
|
|
955
|
+
if (parent.errorHandler) return parent.errorHandler;
|
|
956
|
+
parent = parent.parent;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async createElement(page, props) {
|
|
960
|
+
if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
|
|
961
|
+
if (page.lazy) return createElement((await page.lazy()).default, props);
|
|
962
|
+
if (page.component) return createElement(page.component, props);
|
|
963
|
+
}
|
|
964
|
+
renderError(error) {
|
|
965
|
+
return createElement(ErrorViewer_default, {
|
|
966
|
+
error,
|
|
967
|
+
alepha: this.alepha
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
renderEmptyView() {
|
|
971
|
+
return createElement(NestedView_default, {});
|
|
972
|
+
}
|
|
973
|
+
href(page, params = {}) {
|
|
974
|
+
const found = this.pages.find((it) => it.name === page.options.name);
|
|
975
|
+
if (!found) throw new AlephaError(`Page ${page.options.name} not found`);
|
|
976
|
+
let url = found.path ?? "";
|
|
977
|
+
let parent = found.parent;
|
|
978
|
+
while (parent) {
|
|
979
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
980
|
+
parent = parent.parent;
|
|
981
|
+
}
|
|
982
|
+
url = this.compile(url, params);
|
|
983
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
984
|
+
}
|
|
985
|
+
compile(path, params = {}) {
|
|
986
|
+
for (const [key, value] of Object.entries(params)) path = path.replace(`:${key}`, value);
|
|
987
|
+
return path;
|
|
988
|
+
}
|
|
989
|
+
renderView(index, path, view, page) {
|
|
990
|
+
view ??= this.renderEmptyView();
|
|
991
|
+
const element = page.client ? createElement(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
|
|
992
|
+
return createElement(RouterLayerContext.Provider, { value: {
|
|
993
|
+
index,
|
|
994
|
+
path,
|
|
995
|
+
onError: this.getErrorHandler(page) ?? ((error) => this.renderError(error))
|
|
996
|
+
} }, element);
|
|
997
|
+
}
|
|
998
|
+
configure = $hook({
|
|
999
|
+
on: "configure",
|
|
1000
|
+
handler: () => {
|
|
1001
|
+
let hasNotFoundHandler = false;
|
|
1002
|
+
const pages = this.alepha.primitives($page);
|
|
1003
|
+
const hasParent = (it) => {
|
|
1004
|
+
if (it.options.parent) return true;
|
|
1005
|
+
for (const page of pages) if ((page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : []).includes(it)) return true;
|
|
1006
|
+
};
|
|
1007
|
+
for (const page of pages) {
|
|
1008
|
+
if (page.options.path === "/*") hasNotFoundHandler = true;
|
|
1009
|
+
if (hasParent(page)) continue;
|
|
1010
|
+
this.add(this.map(pages, page));
|
|
1011
|
+
}
|
|
1012
|
+
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
1013
|
+
path: "/*",
|
|
1014
|
+
name: "notFound",
|
|
1015
|
+
cache: true,
|
|
1016
|
+
component: NotFound_default,
|
|
1017
|
+
onServerResponse: ({ reply }) => {
|
|
1018
|
+
reply.status = 404;
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
map(pages, target) {
|
|
1024
|
+
const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
|
|
1025
|
+
const getChildrenFromParent = (it) => {
|
|
1026
|
+
const children$1 = [];
|
|
1027
|
+
for (const page of pages) if (page.options.parent === it) children$1.push(page);
|
|
1028
|
+
return children$1;
|
|
1029
|
+
};
|
|
1030
|
+
children.push(...getChildrenFromParent(target));
|
|
1031
|
+
return {
|
|
1032
|
+
...target.options,
|
|
1033
|
+
name: target.name,
|
|
1034
|
+
parent: void 0,
|
|
1035
|
+
children: children.map((it) => this.map(pages, it))
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
add(entry) {
|
|
1039
|
+
if (this.alepha.isReady()) throw new AlephaError("Router is already initialized");
|
|
1040
|
+
entry.name ??= this.nextId();
|
|
1041
|
+
const page = entry;
|
|
1042
|
+
page.match = this.createMatch(page);
|
|
1043
|
+
this.pages.push(page);
|
|
1044
|
+
if (page.children) for (const child of page.children) {
|
|
1045
|
+
child.parent = page;
|
|
1046
|
+
this.add(child);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
createMatch(page) {
|
|
1050
|
+
let url = page.path ?? "/";
|
|
1051
|
+
let target = page.parent;
|
|
1052
|
+
while (target) {
|
|
1053
|
+
url = `${target.path ?? ""}/${url}`;
|
|
1054
|
+
target = target.parent;
|
|
1055
|
+
}
|
|
1056
|
+
let path = url.replace(/\/\/+/g, "/");
|
|
1057
|
+
if (path.endsWith("/") && path !== "/") path = path.slice(0, -1);
|
|
1058
|
+
return path;
|
|
1059
|
+
}
|
|
1060
|
+
_next = 0;
|
|
1061
|
+
nextId() {
|
|
1062
|
+
this._next += 1;
|
|
1063
|
+
return `P${this._next}`;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
const isPageRoute = (it) => {
|
|
1067
|
+
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region ../../src/router/providers/ReactBrowserRouterProvider.ts
|
|
1072
|
+
/**
|
|
1073
|
+
* Implementation of AlephaRouter for React in browser environment.
|
|
1074
|
+
*/
|
|
1075
|
+
var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
1076
|
+
log = $logger();
|
|
1077
|
+
alepha = $inject(Alepha);
|
|
1078
|
+
pageApi = $inject(ReactPageProvider);
|
|
1079
|
+
add(entry) {
|
|
1080
|
+
this.pageApi.add(entry);
|
|
1081
|
+
}
|
|
1082
|
+
configure = $hook({
|
|
1083
|
+
on: "configure",
|
|
1084
|
+
handler: async () => {
|
|
1085
|
+
for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
|
|
1086
|
+
path: page.match,
|
|
1087
|
+
page
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
async transition(url, previous = [], meta = {}) {
|
|
1092
|
+
const { pathname, search } = url;
|
|
1093
|
+
const state = {
|
|
1094
|
+
url,
|
|
1095
|
+
query: {},
|
|
1096
|
+
params: {},
|
|
1097
|
+
layers: [],
|
|
1098
|
+
onError: () => null,
|
|
1099
|
+
meta
|
|
1100
|
+
};
|
|
1101
|
+
await this.alepha.events.emit("react:action:begin", { type: "transition" });
|
|
1102
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
1103
|
+
previous: this.alepha.store.get("alepha.react.router.state"),
|
|
1104
|
+
state
|
|
1105
|
+
});
|
|
1106
|
+
try {
|
|
1107
|
+
const { route, params } = this.match(pathname);
|
|
1108
|
+
const query = {};
|
|
1109
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
1110
|
+
state.name = route?.page.name;
|
|
1111
|
+
state.query = query;
|
|
1112
|
+
state.params = params ?? {};
|
|
1113
|
+
if (isPageRoute(route)) {
|
|
1114
|
+
const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
|
|
1115
|
+
if (redirect) return redirect;
|
|
1116
|
+
}
|
|
1117
|
+
if (state.layers.length === 0) state.layers.push({
|
|
1118
|
+
name: "not-found",
|
|
1119
|
+
element: createElement(NotFound_default),
|
|
1120
|
+
index: 0,
|
|
1121
|
+
path: "/"
|
|
1122
|
+
});
|
|
1123
|
+
await this.alepha.events.emit("react:action:success", { type: "transition" });
|
|
1124
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
this.log.error("Transition has failed", e);
|
|
1127
|
+
state.layers = [{
|
|
1128
|
+
name: "error",
|
|
1129
|
+
element: this.pageApi.renderError(e),
|
|
1130
|
+
index: 0,
|
|
1131
|
+
path: "/"
|
|
1132
|
+
}];
|
|
1133
|
+
await this.alepha.events.emit("react:action:error", {
|
|
1134
|
+
type: "transition",
|
|
1135
|
+
error: e
|
|
1136
|
+
});
|
|
1137
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
1138
|
+
error: e,
|
|
1139
|
+
state
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
if (previous) for (let i = 0; i < previous.length; i++) {
|
|
1143
|
+
const layer = previous[i];
|
|
1144
|
+
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
1145
|
+
}
|
|
1146
|
+
this.alepha.store.set("alepha.react.router.state", state);
|
|
1147
|
+
await this.alepha.events.emit("react:action:end", { type: "transition" });
|
|
1148
|
+
await this.alepha.events.emit("react:transition:end", { state });
|
|
1149
|
+
}
|
|
1150
|
+
root(state) {
|
|
1151
|
+
return this.pageApi.root(state);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region ../../src/router/providers/ReactBrowserProvider.ts
|
|
1157
|
+
const envSchema = t.object({ REACT_ROOT_ID: t.text({ default: "root" }) });
|
|
1158
|
+
/**
|
|
1159
|
+
* React browser renderer configuration atom
|
|
1160
|
+
*/
|
|
1161
|
+
const reactBrowserOptions = $atom({
|
|
1162
|
+
name: "alepha.react.browser.options",
|
|
1163
|
+
schema: t.object({ scrollRestoration: t.enum(["top", "manual"]) }),
|
|
1164
|
+
default: { scrollRestoration: "top" }
|
|
1165
|
+
});
|
|
1166
|
+
var ReactBrowserProvider = class {
|
|
1167
|
+
env = $env(envSchema);
|
|
1168
|
+
log = $logger();
|
|
1169
|
+
client = $inject(LinkProvider);
|
|
1170
|
+
alepha = $inject(Alepha);
|
|
1171
|
+
router = $inject(ReactBrowserRouterProvider);
|
|
1172
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
1173
|
+
options = $use(reactBrowserOptions);
|
|
1174
|
+
getRootElement() {
|
|
1175
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
1176
|
+
if (root) return root;
|
|
1177
|
+
const div = this.document.createElement("div");
|
|
1178
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
1179
|
+
this.document.body.prepend(div);
|
|
1180
|
+
return div;
|
|
1181
|
+
}
|
|
1182
|
+
transitioning;
|
|
1183
|
+
get state() {
|
|
1184
|
+
return this.alepha.store.get("alepha.react.router.state");
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Accessor for Document DOM API.
|
|
1188
|
+
*/
|
|
1189
|
+
get document() {
|
|
1190
|
+
return window.document;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Accessor for History DOM API.
|
|
1194
|
+
*/
|
|
1195
|
+
get history() {
|
|
1196
|
+
return window.history;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Accessor for Location DOM API.
|
|
1200
|
+
*/
|
|
1201
|
+
get location() {
|
|
1202
|
+
return window.location;
|
|
1203
|
+
}
|
|
1204
|
+
get base() {
|
|
1205
|
+
const base = import.meta.env?.BASE_URL;
|
|
1206
|
+
if (!base || base === "/") return "";
|
|
1207
|
+
return base;
|
|
1208
|
+
}
|
|
1209
|
+
get url() {
|
|
1210
|
+
const url = this.location.pathname + this.location.search;
|
|
1211
|
+
if (this.base) return url.replace(this.base, "");
|
|
1212
|
+
return url;
|
|
1213
|
+
}
|
|
1214
|
+
pushState(path, replace) {
|
|
1215
|
+
const url = this.base + path;
|
|
1216
|
+
if (replace) this.history.replaceState({}, "", url);
|
|
1217
|
+
else this.history.pushState({}, "", url);
|
|
1218
|
+
}
|
|
1219
|
+
async invalidate(props) {
|
|
1220
|
+
const previous = [];
|
|
1221
|
+
this.log.trace("Invalidating layers");
|
|
1222
|
+
if (props) {
|
|
1223
|
+
const [key] = Object.keys(props);
|
|
1224
|
+
const value = props[key];
|
|
1225
|
+
for (const layer of this.state.layers) {
|
|
1226
|
+
if (layer.props?.[key]) {
|
|
1227
|
+
previous.push({
|
|
1228
|
+
...layer,
|
|
1229
|
+
props: {
|
|
1230
|
+
...layer.props,
|
|
1231
|
+
[key]: value
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
previous.push(layer);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
await this.render({ previous });
|
|
1240
|
+
}
|
|
1241
|
+
async go(url, options = {}) {
|
|
1242
|
+
this.log.trace(`Going to ${url}`, {
|
|
1243
|
+
url,
|
|
1244
|
+
options
|
|
1245
|
+
});
|
|
1246
|
+
await this.render({
|
|
1247
|
+
url,
|
|
1248
|
+
previous: options.force ? [] : this.state.layers,
|
|
1249
|
+
meta: options.meta
|
|
1250
|
+
});
|
|
1251
|
+
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
1252
|
+
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
this.pushState(url, options.replace);
|
|
1256
|
+
}
|
|
1257
|
+
async render(options = {}) {
|
|
1258
|
+
const previous = options.previous ?? this.state.layers;
|
|
1259
|
+
const url = options.url ?? this.url;
|
|
1260
|
+
const start = this.dateTimeProvider.now();
|
|
1261
|
+
this.transitioning = {
|
|
1262
|
+
to: url,
|
|
1263
|
+
from: this.state?.url.pathname
|
|
1264
|
+
};
|
|
1265
|
+
this.log.debug("Transitioning...", { to: url });
|
|
1266
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
|
|
1267
|
+
if (redirect) {
|
|
1268
|
+
this.log.info("Redirecting to", { redirect });
|
|
1269
|
+
if (redirect.startsWith("http")) window.location.href = redirect;
|
|
1270
|
+
else return await this.render({ url: redirect });
|
|
1271
|
+
}
|
|
1272
|
+
const ms = this.dateTimeProvider.now().diff(start);
|
|
1273
|
+
this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
|
|
1274
|
+
this.transitioning = void 0;
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Get embedded layers from the server.
|
|
1278
|
+
*/
|
|
1279
|
+
getHydrationState() {
|
|
1280
|
+
try {
|
|
1281
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
console.error(error);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
onTransitionEnd = $hook({
|
|
1287
|
+
on: "react:transition:end",
|
|
1288
|
+
handler: () => {
|
|
1289
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
|
|
1290
|
+
this.log.trace("Restoring scroll position to top");
|
|
1291
|
+
window.scrollTo(0, 0);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
ready = $hook({
|
|
1296
|
+
on: "ready",
|
|
1297
|
+
handler: async () => {
|
|
1298
|
+
const hydration = this.getHydrationState();
|
|
1299
|
+
const previous = hydration?.layers ?? [];
|
|
1300
|
+
if (hydration) {
|
|
1301
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.store.set(key, value);
|
|
1302
|
+
}
|
|
1303
|
+
await this.render({ previous });
|
|
1304
|
+
const element = this.router.root(this.state);
|
|
1305
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
1306
|
+
element,
|
|
1307
|
+
root: this.getRootElement(),
|
|
1308
|
+
hydration,
|
|
1309
|
+
state: this.state
|
|
1310
|
+
});
|
|
1311
|
+
window.addEventListener("popstate", () => {
|
|
1312
|
+
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1313
|
+
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
1314
|
+
this.render();
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
//#endregion
|
|
1321
|
+
//#region ../../src/router/services/ReactRouter.ts
|
|
1322
|
+
/**
|
|
1323
|
+
* Friendly browser router API.
|
|
1324
|
+
*
|
|
1325
|
+
* Can be safely used server-side, but most methods will be no-op.
|
|
1326
|
+
*/
|
|
1327
|
+
var ReactRouter = class {
|
|
1328
|
+
alepha = $inject(Alepha);
|
|
1329
|
+
pageApi = $inject(ReactPageProvider);
|
|
1330
|
+
get state() {
|
|
1331
|
+
return this.alepha.store.get("alepha.react.router.state");
|
|
1332
|
+
}
|
|
1333
|
+
get pages() {
|
|
1334
|
+
return this.pageApi.getPages();
|
|
1335
|
+
}
|
|
1336
|
+
get concretePages() {
|
|
1337
|
+
return this.pageApi.getConcretePages();
|
|
1338
|
+
}
|
|
1339
|
+
get browser() {
|
|
1340
|
+
if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
|
|
1341
|
+
}
|
|
1342
|
+
isActive(href, options = {}) {
|
|
1343
|
+
const current = this.state.url.pathname;
|
|
1344
|
+
let isActive = current === href || current === `${href}/` || `${current}/` === href;
|
|
1345
|
+
if (options.startWith && !isActive) isActive = current.startsWith(href);
|
|
1346
|
+
return isActive;
|
|
1347
|
+
}
|
|
1348
|
+
node(name, config = {}) {
|
|
1349
|
+
const page = this.pageApi.page(name);
|
|
1350
|
+
if (!page.lazy && !page.component) return {
|
|
1351
|
+
...page,
|
|
1352
|
+
label: page.label ?? page.name,
|
|
1353
|
+
children: void 0
|
|
1354
|
+
};
|
|
1355
|
+
return {
|
|
1356
|
+
...page,
|
|
1357
|
+
label: page.label ?? page.name,
|
|
1358
|
+
href: this.path(name, config),
|
|
1359
|
+
children: void 0
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
path(name, config = {}) {
|
|
1363
|
+
return this.pageApi.pathname(name, {
|
|
1364
|
+
params: {
|
|
1365
|
+
...this.state?.params,
|
|
1366
|
+
...config.params
|
|
1367
|
+
},
|
|
1368
|
+
query: config.query
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Reload the current page.
|
|
1373
|
+
* This is equivalent to calling `go()` with the current pathname and search.
|
|
1374
|
+
*/
|
|
1375
|
+
async reload() {
|
|
1376
|
+
if (!this.browser) return;
|
|
1377
|
+
await this.go(this.location.pathname + this.location.search, {
|
|
1378
|
+
replace: true,
|
|
1379
|
+
force: true
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
getURL() {
|
|
1383
|
+
if (!this.browser) return this.state.url;
|
|
1384
|
+
return new URL(this.location.href);
|
|
1385
|
+
}
|
|
1386
|
+
get location() {
|
|
1387
|
+
if (!this.browser) throw new Error("Browser is required");
|
|
1388
|
+
return this.browser.location;
|
|
1389
|
+
}
|
|
1390
|
+
get current() {
|
|
1391
|
+
return this.state;
|
|
1392
|
+
}
|
|
1393
|
+
get pathname() {
|
|
1394
|
+
return this.state.url.pathname;
|
|
1395
|
+
}
|
|
1396
|
+
get query() {
|
|
1397
|
+
const query = {};
|
|
1398
|
+
for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
|
|
1399
|
+
return query;
|
|
1400
|
+
}
|
|
1401
|
+
async back() {
|
|
1402
|
+
this.browser?.history.back();
|
|
1403
|
+
}
|
|
1404
|
+
async forward() {
|
|
1405
|
+
this.browser?.history.forward();
|
|
1406
|
+
}
|
|
1407
|
+
async invalidate(props) {
|
|
1408
|
+
await this.browser?.invalidate(props);
|
|
1409
|
+
}
|
|
1410
|
+
async go(path, options) {
|
|
1411
|
+
for (const page of this.pages) if (page.name === path) {
|
|
1412
|
+
await this.browser?.go(this.path(path, options), options);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
await this.browser?.go(path, options);
|
|
1416
|
+
}
|
|
1417
|
+
anchor(path, options = {}) {
|
|
1418
|
+
let href = path;
|
|
1419
|
+
for (const page of this.pages) if (page.name === path) {
|
|
1420
|
+
href = this.path(path, options);
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
return {
|
|
1424
|
+
href: this.base(href),
|
|
1425
|
+
onClick: (ev) => {
|
|
1426
|
+
ev.stopPropagation();
|
|
1427
|
+
ev.preventDefault();
|
|
1428
|
+
this.go(href, options).catch(console.error);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
base(path) {
|
|
1433
|
+
const base = import.meta.env?.BASE_URL;
|
|
1434
|
+
if (!base || base === "/") return path;
|
|
1435
|
+
return base + path;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Set query params.
|
|
1439
|
+
*
|
|
1440
|
+
* @param record
|
|
1441
|
+
* @param options
|
|
1442
|
+
*/
|
|
1443
|
+
setQueryParams(record, options = {}) {
|
|
1444
|
+
const func = typeof record === "function" ? record : () => record;
|
|
1445
|
+
const search = new URLSearchParams(func(this.query)).toString();
|
|
1446
|
+
const state = search ? `${this.pathname}?${search}` : this.pathname;
|
|
1447
|
+
if (options.push) window.history.pushState({}, "", state);
|
|
1448
|
+
else window.history.replaceState({}, "", state);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
//#endregion
|
|
1453
|
+
//#region ../../src/router/providers/ReactBrowserRendererProvider.ts
|
|
1454
|
+
/**
|
|
1455
|
+
* Browser specific React renderer (react-dom/client interface)
|
|
1456
|
+
*/
|
|
1457
|
+
var ReactBrowserRendererProvider = class {
|
|
1458
|
+
log = $logger();
|
|
1459
|
+
root;
|
|
1460
|
+
onBrowserRender = $hook({
|
|
1461
|
+
on: "react:browser:render",
|
|
1462
|
+
handler: async ({ hydration, root, element }) => {
|
|
1463
|
+
if (hydration?.layers) {
|
|
1464
|
+
this.root = hydrateRoot(root, element);
|
|
1465
|
+
this.log.info("Hydrated root element");
|
|
1466
|
+
} else {
|
|
1467
|
+
this.root ??= createRoot(root);
|
|
1468
|
+
this.root.render(element);
|
|
1469
|
+
this.log.info("Created root element");
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region ../../src/router/index.browser.ts
|
|
1477
|
+
const AlephaReactRouter = $module({
|
|
1478
|
+
name: "alepha.react.router",
|
|
1479
|
+
primitives: [$page],
|
|
1480
|
+
services: [
|
|
1481
|
+
ReactPageProvider,
|
|
1482
|
+
ReactBrowserRouterProvider,
|
|
1483
|
+
ReactBrowserProvider,
|
|
1484
|
+
ReactRouter,
|
|
1485
|
+
ReactBrowserRendererProvider,
|
|
1486
|
+
ReactPageService
|
|
1487
|
+
],
|
|
1488
|
+
register: (alepha) => alepha.with(AlephaReact).with(AlephaDateTime).with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactBrowserRendererProvider).with(ReactRouter)
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
//#endregion
|
|
8
1492
|
//#region ../../src/auth/services/ReactAuth.ts
|
|
9
1493
|
/**
|
|
10
1494
|
* Browser, SSR friendly, service to handle authentication.
|