@alepha/react 0.7.4 → 0.7.6
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/index.browser.d.ts +578 -0
- package/dist/index.browser.js +1091 -49
- package/dist/index.browser.js.map +1 -0
- package/dist/index.cjs +1333 -571
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +678 -0
- package/dist/index.d.ts +530 -570
- package/dist/index.js +1297 -557
- package/dist/index.js.map +1 -0
- package/package.json +11 -8
- package/src/components/NotFound.tsx +0 -10
- package/src/descriptors/$page.ts +5 -2
- package/src/index.browser.ts +1 -0
- package/src/index.shared.ts +0 -4
- package/src/index.ts +0 -2
- package/dist/ReactBrowserProvider-CXDElhnK.cjs +0 -960
- package/dist/ReactBrowserProvider-ufHSOTmv.js +0 -946
- package/dist/index.browser.cjs +0 -58
package/dist/index.cjs
CHANGED
|
@@ -1,613 +1,1375 @@
|
|
|
1
|
-
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const __alepha_core = __toESM(require("@alepha/core"));
|
|
25
|
+
const __alepha_server = __toESM(require("@alepha/server"));
|
|
26
|
+
const __alepha_server_cache = __toESM(require("@alepha/server-cache"));
|
|
27
|
+
const react = __toESM(require("react"));
|
|
28
|
+
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
|
|
29
|
+
const node_fs = __toESM(require("node:fs"));
|
|
30
|
+
const node_path = __toESM(require("node:path"));
|
|
31
|
+
const __alepha_server_static = __toESM(require("@alepha/server-static"));
|
|
32
|
+
const react_dom_server = __toESM(require("react-dom/server"));
|
|
33
|
+
const __alepha_router = __toESM(require("@alepha/router"));
|
|
34
|
+
|
|
35
|
+
//#region src/descriptors/$page.ts
|
|
36
|
+
const KEY = "PAGE";
|
|
37
|
+
/**
|
|
38
|
+
* Main descriptor for defining a React route in the application.
|
|
39
|
+
*/
|
|
40
|
+
const $page = (options) => {
|
|
41
|
+
(0, __alepha_core.__descriptor)(KEY);
|
|
42
|
+
if (options.children) for (const child of options.children) child[__alepha_core.OPTIONS].parent = { [__alepha_core.OPTIONS]: options };
|
|
43
|
+
if (options.parent) {
|
|
44
|
+
options.parent[__alepha_core.OPTIONS].children ??= [];
|
|
45
|
+
options.parent[__alepha_core.OPTIONS].children.push({ [__alepha_core.OPTIONS]: options });
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
[__alepha_core.KIND]: KEY,
|
|
49
|
+
[__alepha_core.OPTIONS]: options,
|
|
50
|
+
render: () => {
|
|
51
|
+
throw new __alepha_core.NotImplementedError(KEY);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
$page[__alepha_core.KIND] = KEY;
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/components/ClientOnly.tsx
|
|
59
|
+
/**
|
|
60
|
+
* A small utility component that renders its children only on the client side.
|
|
61
|
+
*
|
|
62
|
+
* Optionally, you can provide a fallback React node that will be rendered.
|
|
63
|
+
*
|
|
64
|
+
* You should use this component when
|
|
65
|
+
* - you have code that relies on browser-specific APIs
|
|
66
|
+
* - you want to avoid server-side rendering for a specific part of your application
|
|
67
|
+
* - you want to prevent pre-rendering of a component
|
|
68
|
+
*/
|
|
69
|
+
const ClientOnly = (props) => {
|
|
70
|
+
const [mounted, setMounted] = (0, react.useState)(false);
|
|
71
|
+
(0, react.useEffect)(() => setMounted(true), []);
|
|
72
|
+
if (props.disabled) return props.children;
|
|
73
|
+
return mounted ? props.children : props.fallback;
|
|
74
|
+
};
|
|
75
|
+
var ClientOnly_default = ClientOnly;
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/contexts/RouterContext.ts
|
|
79
|
+
const RouterContext = (0, react.createContext)(void 0);
|
|
2
80
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var server$1 = require('react-dom/server');
|
|
11
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
12
|
-
var React = require('react');
|
|
13
|
-
require('@alepha/router');
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/hooks/useAlepha.ts
|
|
83
|
+
const useAlepha = () => {
|
|
84
|
+
const routerContext = (0, react.useContext)(RouterContext);
|
|
85
|
+
if (!routerContext) throw new Error("useAlepha must be used within a RouterProvider");
|
|
86
|
+
return routerContext.alepha;
|
|
87
|
+
};
|
|
14
88
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/components/ErrorViewer.tsx
|
|
91
|
+
const ErrorViewer = ({ error }) => {
|
|
92
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
93
|
+
const isProduction = useAlepha().isProduction();
|
|
94
|
+
if (isProduction) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorViewerProduction, {});
|
|
95
|
+
const stackLines = error.stack?.split("\n") ?? [];
|
|
96
|
+
const previewLines = stackLines.slice(0, 5);
|
|
97
|
+
const hiddenLineCount = stackLines.length - previewLines.length;
|
|
98
|
+
const copyToClipboard = (text) => {
|
|
99
|
+
navigator.clipboard.writeText(text).catch((err) => {
|
|
100
|
+
console.error("Clipboard error:", err);
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const styles = {
|
|
104
|
+
container: {
|
|
105
|
+
padding: "24px",
|
|
106
|
+
backgroundColor: "#FEF2F2",
|
|
107
|
+
color: "#7F1D1D",
|
|
108
|
+
border: "1px solid #FECACA",
|
|
109
|
+
borderRadius: "16px",
|
|
110
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
|
|
111
|
+
fontFamily: "monospace",
|
|
112
|
+
maxWidth: "768px",
|
|
113
|
+
margin: "40px auto"
|
|
114
|
+
},
|
|
115
|
+
heading: {
|
|
116
|
+
fontSize: "20px",
|
|
117
|
+
fontWeight: "bold",
|
|
118
|
+
marginBottom: "4px"
|
|
119
|
+
},
|
|
120
|
+
name: {
|
|
121
|
+
fontSize: "16px",
|
|
122
|
+
fontWeight: 600
|
|
123
|
+
},
|
|
124
|
+
message: {
|
|
125
|
+
fontSize: "14px",
|
|
126
|
+
marginBottom: "16px"
|
|
127
|
+
},
|
|
128
|
+
sectionHeader: {
|
|
129
|
+
display: "flex",
|
|
130
|
+
justifyContent: "space-between",
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
fontSize: "12px",
|
|
133
|
+
marginBottom: "4px",
|
|
134
|
+
color: "#991B1B"
|
|
135
|
+
},
|
|
136
|
+
copyButton: {
|
|
137
|
+
fontSize: "12px",
|
|
138
|
+
color: "#DC2626",
|
|
139
|
+
background: "none",
|
|
140
|
+
border: "none",
|
|
141
|
+
cursor: "pointer",
|
|
142
|
+
textDecoration: "underline"
|
|
143
|
+
},
|
|
144
|
+
stackContainer: {
|
|
145
|
+
backgroundColor: "#FEE2E2",
|
|
146
|
+
padding: "12px",
|
|
147
|
+
borderRadius: "8px",
|
|
148
|
+
fontSize: "13px",
|
|
149
|
+
lineHeight: "1.4",
|
|
150
|
+
overflowX: "auto",
|
|
151
|
+
whiteSpace: "pre-wrap"
|
|
152
|
+
},
|
|
153
|
+
expandLine: {
|
|
154
|
+
color: "#F87171",
|
|
155
|
+
cursor: "pointer",
|
|
156
|
+
marginTop: "8px"
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
160
|
+
style: styles.container,
|
|
161
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
163
|
+
style: styles.heading,
|
|
164
|
+
children: "🔥 Error"
|
|
165
|
+
}),
|
|
166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
167
|
+
style: styles.name,
|
|
168
|
+
children: error.name
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
171
|
+
style: styles.message,
|
|
172
|
+
children: error.message
|
|
173
|
+
})
|
|
174
|
+
] }), stackLines.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
175
|
+
style: styles.sectionHeader,
|
|
176
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Stack trace" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
177
|
+
onClick: () => copyToClipboard(error.stack),
|
|
178
|
+
style: styles.copyButton,
|
|
179
|
+
children: "Copy all"
|
|
180
|
+
})]
|
|
181
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("pre", {
|
|
182
|
+
style: styles.stackContainer,
|
|
183
|
+
children: [(expanded ? stackLines : previewLines).map((line, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: line }, i)), !expanded && hiddenLineCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
184
|
+
style: styles.expandLine,
|
|
185
|
+
onClick: () => setExpanded(true),
|
|
186
|
+
children: [
|
|
187
|
+
"+ ",
|
|
188
|
+
hiddenLineCount,
|
|
189
|
+
" more lines..."
|
|
190
|
+
]
|
|
191
|
+
})]
|
|
192
|
+
})] })]
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
var ErrorViewer_default = ErrorViewer;
|
|
196
|
+
const ErrorViewerProduction = () => {
|
|
197
|
+
const styles = {
|
|
198
|
+
container: {
|
|
199
|
+
padding: "24px",
|
|
200
|
+
backgroundColor: "#FEF2F2",
|
|
201
|
+
color: "#7F1D1D",
|
|
202
|
+
border: "1px solid #FECACA",
|
|
203
|
+
borderRadius: "16px",
|
|
204
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
|
|
205
|
+
fontFamily: "monospace",
|
|
206
|
+
maxWidth: "768px",
|
|
207
|
+
margin: "40px auto",
|
|
208
|
+
textAlign: "center"
|
|
209
|
+
},
|
|
210
|
+
heading: {
|
|
211
|
+
fontSize: "20px",
|
|
212
|
+
fontWeight: "bold",
|
|
213
|
+
marginBottom: "8px"
|
|
214
|
+
},
|
|
215
|
+
name: {
|
|
216
|
+
fontSize: "16px",
|
|
217
|
+
fontWeight: 600,
|
|
218
|
+
marginBottom: "4px"
|
|
219
|
+
},
|
|
220
|
+
message: {
|
|
221
|
+
fontSize: "14px",
|
|
222
|
+
opacity: .85
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
226
|
+
style: styles.container,
|
|
227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
228
|
+
style: styles.heading,
|
|
229
|
+
children: "🚨 An error occurred"
|
|
230
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
231
|
+
style: styles.message,
|
|
232
|
+
children: "Something went wrong. Please try again later."
|
|
233
|
+
})]
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/contexts/RouterLayerContext.ts
|
|
239
|
+
const RouterLayerContext = (0, react.createContext)(void 0);
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/hooks/useRouterEvents.ts
|
|
243
|
+
const useRouterEvents = (opts = {}, deps = []) => {
|
|
244
|
+
const ctx = (0, react.useContext)(RouterContext);
|
|
245
|
+
if (!ctx) throw new Error("useRouter must be used within a RouterProvider");
|
|
246
|
+
(0, react.useEffect)(() => {
|
|
247
|
+
if (!ctx.alepha.isBrowser()) return;
|
|
248
|
+
const subs = [];
|
|
249
|
+
const onBegin = opts.onBegin;
|
|
250
|
+
const onEnd = opts.onEnd;
|
|
251
|
+
const onError = opts.onError;
|
|
252
|
+
if (onBegin) subs.push(ctx.alepha.on("react:transition:begin", { callback: onBegin }));
|
|
253
|
+
if (onEnd) subs.push(ctx.alepha.on("react:transition:end", { callback: onEnd }));
|
|
254
|
+
if (onError) subs.push(ctx.alepha.on("react:transition:error", { callback: onError }));
|
|
255
|
+
return () => {
|
|
256
|
+
for (const sub of subs) sub();
|
|
257
|
+
};
|
|
258
|
+
}, deps);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/components/ErrorBoundary.tsx
|
|
263
|
+
/**
|
|
264
|
+
* A reusable error boundary for catching rendering errors
|
|
265
|
+
* in any part of the React component tree.
|
|
266
|
+
*/
|
|
267
|
+
var ErrorBoundary = class extends react.default.Component {
|
|
268
|
+
constructor(props) {
|
|
269
|
+
super(props);
|
|
270
|
+
this.state = {};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Update state so the next render shows the fallback UI.
|
|
274
|
+
*/
|
|
275
|
+
static getDerivedStateFromError(error) {
|
|
276
|
+
return { error };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Lifecycle method called when an error is caught.
|
|
280
|
+
* You can log the error or perform side effects here.
|
|
281
|
+
*/
|
|
282
|
+
componentDidCatch(error, info) {
|
|
283
|
+
if (this.props.onError) this.props.onError(error, info);
|
|
284
|
+
}
|
|
285
|
+
render() {
|
|
286
|
+
if (this.state.error) return this.props.fallback(this.state.error);
|
|
287
|
+
return this.props.children;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var ErrorBoundary_default = ErrorBoundary;
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/components/NestedView.tsx
|
|
294
|
+
/**
|
|
295
|
+
* A component that renders the current view of the nested router layer.
|
|
296
|
+
*
|
|
297
|
+
* To be simple, it renders the `element` of the current child page of a parent page.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```tsx
|
|
301
|
+
* import { NestedView } from "@alepha/react";
|
|
302
|
+
*
|
|
303
|
+
* class App {
|
|
304
|
+
* parent = $page({
|
|
305
|
+
* component: () => <NestedView />,
|
|
306
|
+
* });
|
|
307
|
+
*
|
|
308
|
+
* child = $page({
|
|
309
|
+
* parent: this.root,
|
|
310
|
+
* component: () => <div>Child Page</div>,
|
|
311
|
+
* });
|
|
312
|
+
* }
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
const NestedView = (props) => {
|
|
316
|
+
const app = (0, react.useContext)(RouterContext);
|
|
317
|
+
const layer = (0, react.useContext)(RouterLayerContext);
|
|
318
|
+
const index = layer?.index ?? 0;
|
|
319
|
+
const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
|
|
320
|
+
useRouterEvents({ onEnd: ({ state }) => {
|
|
321
|
+
setView(state.layers[index]?.element);
|
|
322
|
+
} }, [app]);
|
|
323
|
+
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
324
|
+
const element = view ?? props.children ?? null;
|
|
325
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
|
|
326
|
+
fallback: app.context.onError,
|
|
327
|
+
children: element
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
var NestedView_default = NestedView;
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/components/NotFound.tsx
|
|
334
|
+
function NotFoundPage() {
|
|
335
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
336
|
+
style: {
|
|
337
|
+
height: "100vh",
|
|
338
|
+
display: "flex",
|
|
339
|
+
flexDirection: "column",
|
|
340
|
+
justifyContent: "center",
|
|
341
|
+
alignItems: "center",
|
|
342
|
+
textAlign: "center",
|
|
343
|
+
fontFamily: "sans-serif",
|
|
344
|
+
padding: "1rem"
|
|
345
|
+
},
|
|
346
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
347
|
+
style: {
|
|
348
|
+
fontSize: "1rem",
|
|
349
|
+
marginBottom: "0.5rem"
|
|
350
|
+
},
|
|
351
|
+
children: "This page does not exist"
|
|
352
|
+
})
|
|
353
|
+
});
|
|
75
354
|
}
|
|
76
355
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/errors/RedirectionError.ts
|
|
358
|
+
var RedirectionError = class extends Error {
|
|
359
|
+
page;
|
|
360
|
+
constructor(page) {
|
|
361
|
+
super("Redirection");
|
|
362
|
+
this.page = page;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/providers/PageDescriptorProvider.ts
|
|
368
|
+
const envSchema$1 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
|
|
369
|
+
var PageDescriptorProvider = class {
|
|
370
|
+
log = (0, __alepha_core.$logger)();
|
|
371
|
+
env = (0, __alepha_core.$inject)(envSchema$1);
|
|
372
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
373
|
+
pages = [];
|
|
374
|
+
getPages() {
|
|
375
|
+
return this.pages;
|
|
376
|
+
}
|
|
377
|
+
page(name) {
|
|
378
|
+
for (const page of this.pages) if (page.name === name) return page;
|
|
379
|
+
throw new Error(`Page ${name} not found`);
|
|
380
|
+
}
|
|
381
|
+
url(name, options = {}) {
|
|
382
|
+
const page = this.page(name);
|
|
383
|
+
if (!page) throw new Error(`Page ${name} not found`);
|
|
384
|
+
let url = page.path ?? "";
|
|
385
|
+
let parent = page.parent;
|
|
386
|
+
while (parent) {
|
|
387
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
388
|
+
parent = parent.parent;
|
|
389
|
+
}
|
|
390
|
+
url = this.compile(url, options.params ?? {});
|
|
391
|
+
return new URL(url.replace(/\/\/+/g, "/") || "/", options.base ?? `http://localhost`);
|
|
392
|
+
}
|
|
393
|
+
root(state, context) {
|
|
394
|
+
const root = (0, react.createElement)(RouterContext.Provider, { value: {
|
|
395
|
+
alepha: this.alepha,
|
|
396
|
+
state,
|
|
397
|
+
context
|
|
398
|
+
} }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element));
|
|
399
|
+
if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
|
|
400
|
+
return root;
|
|
401
|
+
}
|
|
402
|
+
async createLayers(route, request) {
|
|
403
|
+
const { pathname, search } = request.url;
|
|
404
|
+
const layers = [];
|
|
405
|
+
let context = {};
|
|
406
|
+
const stack = [{ route }];
|
|
407
|
+
request.onError = (error) => this.renderError(error);
|
|
408
|
+
let parent = route.parent;
|
|
409
|
+
while (parent) {
|
|
410
|
+
stack.unshift({ route: parent });
|
|
411
|
+
parent = parent.parent;
|
|
412
|
+
}
|
|
413
|
+
let forceRefresh = false;
|
|
414
|
+
for (let i = 0; i < stack.length; i++) {
|
|
415
|
+
const it = stack[i];
|
|
416
|
+
const route$1 = it.route;
|
|
417
|
+
const config = {};
|
|
418
|
+
try {
|
|
419
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : request.query;
|
|
420
|
+
} catch (e) {
|
|
421
|
+
it.error = e;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : request.params;
|
|
426
|
+
} catch (e) {
|
|
427
|
+
it.error = e;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
it.config = { ...config };
|
|
431
|
+
if (!route$1.resolve) continue;
|
|
432
|
+
const previous = request.previous;
|
|
433
|
+
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
434
|
+
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
435
|
+
const prev = JSON.stringify({
|
|
436
|
+
part: url(previous[i].part),
|
|
437
|
+
params: previous[i].config?.params ?? {}
|
|
438
|
+
});
|
|
439
|
+
const curr = JSON.stringify({
|
|
440
|
+
part: url(route$1.path),
|
|
441
|
+
params: config.params ?? {}
|
|
442
|
+
});
|
|
443
|
+
if (prev === curr) {
|
|
444
|
+
it.props = previous[i].props;
|
|
445
|
+
it.error = previous[i].error;
|
|
446
|
+
context = {
|
|
447
|
+
...context,
|
|
448
|
+
...it.props
|
|
449
|
+
};
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
forceRefresh = true;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const props = await route$1.resolve?.({
|
|
456
|
+
...request,
|
|
457
|
+
...config,
|
|
458
|
+
...context
|
|
459
|
+
}) ?? {};
|
|
460
|
+
it.props = { ...props };
|
|
461
|
+
context = {
|
|
462
|
+
...context,
|
|
463
|
+
...props
|
|
464
|
+
};
|
|
465
|
+
} catch (e) {
|
|
466
|
+
if (e instanceof RedirectionError) return {
|
|
467
|
+
layers: [],
|
|
468
|
+
redirect: typeof e.page === "string" ? e.page : this.href(e.page),
|
|
469
|
+
pathname,
|
|
470
|
+
search
|
|
471
|
+
};
|
|
472
|
+
this.log.error(e);
|
|
473
|
+
it.error = e;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
let acc = "";
|
|
478
|
+
for (let i = 0; i < stack.length; i++) {
|
|
479
|
+
const it = stack[i];
|
|
480
|
+
const props = it.props ?? {};
|
|
481
|
+
const params = { ...it.config?.params };
|
|
482
|
+
for (const key of Object.keys(params)) params[key] = String(params[key]);
|
|
483
|
+
if (it.route.head && !it.error) this.fillHead(it.route, request, {
|
|
484
|
+
...props,
|
|
485
|
+
...context
|
|
486
|
+
});
|
|
487
|
+
acc += "/";
|
|
488
|
+
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
489
|
+
const path = acc.replace(/\/+/, "/");
|
|
490
|
+
const localErrorHandler = this.getErrorHandler(it.route);
|
|
491
|
+
if (localErrorHandler) request.onError = localErrorHandler;
|
|
492
|
+
if (it.error) {
|
|
493
|
+
let element$1 = await request.onError(it.error);
|
|
494
|
+
if (element$1 === null) element$1 = this.renderError(it.error);
|
|
495
|
+
layers.push({
|
|
496
|
+
props,
|
|
497
|
+
error: it.error,
|
|
498
|
+
name: it.route.name,
|
|
499
|
+
part: it.route.path,
|
|
500
|
+
config: it.config,
|
|
501
|
+
element: this.renderView(i + 1, path, element$1, it.route),
|
|
502
|
+
index: i + 1,
|
|
503
|
+
path
|
|
504
|
+
});
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
const element = await this.createElement(it.route, {
|
|
508
|
+
...props,
|
|
509
|
+
...context
|
|
510
|
+
});
|
|
511
|
+
layers.push({
|
|
512
|
+
name: it.route.name,
|
|
513
|
+
props,
|
|
514
|
+
part: it.route.path,
|
|
515
|
+
config: it.config,
|
|
516
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
517
|
+
index: i + 1,
|
|
518
|
+
path
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
layers,
|
|
523
|
+
pathname,
|
|
524
|
+
search
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
getErrorHandler(route) {
|
|
528
|
+
if (route.errorHandler) return route.errorHandler;
|
|
529
|
+
let parent = route.parent;
|
|
530
|
+
while (parent) {
|
|
531
|
+
if (parent.errorHandler) return parent.errorHandler;
|
|
532
|
+
parent = parent.parent;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async createElement(page, props) {
|
|
536
|
+
if (page.lazy) {
|
|
537
|
+
const component = await page.lazy();
|
|
538
|
+
return (0, react.createElement)(component.default, props);
|
|
539
|
+
}
|
|
540
|
+
if (page.component) return (0, react.createElement)(page.component, props);
|
|
541
|
+
return void 0;
|
|
542
|
+
}
|
|
543
|
+
fillHead(page, ctx, props) {
|
|
544
|
+
if (!page.head) return;
|
|
545
|
+
ctx.head ??= {};
|
|
546
|
+
const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
|
|
547
|
+
if (head.title) {
|
|
548
|
+
ctx.head ??= {};
|
|
549
|
+
if (ctx.head.titleSeparator) ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
|
|
550
|
+
else ctx.head.title = head.title;
|
|
551
|
+
ctx.head.titleSeparator = head.titleSeparator;
|
|
552
|
+
}
|
|
553
|
+
if (head.htmlAttributes) ctx.head.htmlAttributes = {
|
|
554
|
+
...ctx.head.htmlAttributes,
|
|
555
|
+
...head.htmlAttributes
|
|
556
|
+
};
|
|
557
|
+
if (head.bodyAttributes) ctx.head.bodyAttributes = {
|
|
558
|
+
...ctx.head.bodyAttributes,
|
|
559
|
+
...head.bodyAttributes
|
|
560
|
+
};
|
|
561
|
+
if (head.meta) ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
|
|
562
|
+
}
|
|
563
|
+
renderError(error) {
|
|
564
|
+
return (0, react.createElement)(ErrorViewer_default, { error });
|
|
565
|
+
}
|
|
566
|
+
renderEmptyView() {
|
|
567
|
+
return (0, react.createElement)(NestedView_default, {});
|
|
568
|
+
}
|
|
569
|
+
href(page, params = {}) {
|
|
570
|
+
const found = this.pages.find((it) => it.name === page.options.name);
|
|
571
|
+
if (!found) throw new Error(`Page ${page.options.name} not found`);
|
|
572
|
+
let url = found.path ?? "";
|
|
573
|
+
let parent = found.parent;
|
|
574
|
+
while (parent) {
|
|
575
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
576
|
+
parent = parent.parent;
|
|
577
|
+
}
|
|
578
|
+
url = this.compile(url, params);
|
|
579
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
580
|
+
}
|
|
581
|
+
compile(path, params = {}) {
|
|
582
|
+
for (const [key, value] of Object.entries(params)) path = path.replace(`:${key}`, value);
|
|
583
|
+
return path;
|
|
584
|
+
}
|
|
585
|
+
renderView(index, path, view, page) {
|
|
586
|
+
view ??= this.renderEmptyView();
|
|
587
|
+
const element = page.client ? (0, react.createElement)(ClientOnly_default, typeof page.client === "object" ? page.client : {}, view) : view;
|
|
588
|
+
return (0, react.createElement)(RouterLayerContext.Provider, { value: {
|
|
589
|
+
index,
|
|
590
|
+
path
|
|
591
|
+
} }, element);
|
|
592
|
+
}
|
|
593
|
+
configure = (0, __alepha_core.$hook)({
|
|
594
|
+
name: "configure",
|
|
595
|
+
handler: () => {
|
|
596
|
+
let hasNotFoundHandler = false;
|
|
597
|
+
const pages = this.alepha.getDescriptorValues($page);
|
|
598
|
+
for (const { value, key } of pages) value[__alepha_core.OPTIONS].name ??= key;
|
|
599
|
+
for (const { value } of pages) {
|
|
600
|
+
if (value[__alepha_core.OPTIONS].parent) continue;
|
|
601
|
+
if (value[__alepha_core.OPTIONS].path === "/*") hasNotFoundHandler = true;
|
|
602
|
+
this.add(this.map(pages, value));
|
|
603
|
+
}
|
|
604
|
+
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
605
|
+
path: "/*",
|
|
606
|
+
name: "notFound",
|
|
607
|
+
cache: true,
|
|
608
|
+
component: NotFoundPage,
|
|
609
|
+
afterHandler: ({ reply }) => {
|
|
610
|
+
reply.status = 404;
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
map(pages, target) {
|
|
616
|
+
const children = target[__alepha_core.OPTIONS].children ?? [];
|
|
617
|
+
return {
|
|
618
|
+
...target[__alepha_core.OPTIONS],
|
|
619
|
+
parent: void 0,
|
|
620
|
+
children: children.map((it) => this.map(pages, it))
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
add(entry) {
|
|
624
|
+
if (this.alepha.isReady()) throw new Error("Router is already initialized");
|
|
625
|
+
entry.name ??= this.nextId();
|
|
626
|
+
const page = entry;
|
|
627
|
+
page.match = this.createMatch(page);
|
|
628
|
+
this.pages.push(page);
|
|
629
|
+
if (page.children) for (const child of page.children) {
|
|
630
|
+
child.parent = page;
|
|
631
|
+
this.add(child);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
createMatch(page) {
|
|
635
|
+
let url = page.path ?? "/";
|
|
636
|
+
let target = page.parent;
|
|
637
|
+
while (target) {
|
|
638
|
+
url = `${target.path ?? ""}/${url}`;
|
|
639
|
+
target = target.parent;
|
|
640
|
+
}
|
|
641
|
+
let path = url.replace(/\/\/+/g, "/");
|
|
642
|
+
if (path.endsWith("/") && path !== "/") path = path.slice(0, -1);
|
|
643
|
+
return path;
|
|
644
|
+
}
|
|
645
|
+
_next = 0;
|
|
646
|
+
nextId() {
|
|
647
|
+
this._next += 1;
|
|
648
|
+
return `P${this._next}`;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
const isPageRoute = (it) => {
|
|
652
|
+
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/providers/ServerHeadProvider.ts
|
|
657
|
+
var ServerHeadProvider = class {
|
|
658
|
+
renderHead(template, head) {
|
|
659
|
+
let result = template;
|
|
660
|
+
const htmlAttributes = head.htmlAttributes;
|
|
661
|
+
if (htmlAttributes) result = result.replace(/<html([^>]*)>/i, (_, existingAttrs) => `<html${this.mergeAttributes(existingAttrs, htmlAttributes)}>`);
|
|
662
|
+
const bodyAttributes = head.bodyAttributes;
|
|
663
|
+
if (bodyAttributes) result = result.replace(/<body([^>]*)>/i, (_, existingAttrs) => `<body${this.mergeAttributes(existingAttrs, bodyAttributes)}>`);
|
|
664
|
+
let headContent = "";
|
|
665
|
+
const title = head.title;
|
|
666
|
+
if (title) if (template.includes("<title>")) result = result.replace(/<title>(.*?)<\/title>/i, () => `<title>${this.escapeHtml(title)}</title>`);
|
|
667
|
+
else headContent += `<title>${this.escapeHtml(title)}</title>\n`;
|
|
668
|
+
if (head.meta) for (const meta of head.meta) headContent += `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
|
|
669
|
+
result = result.replace(/<head([^>]*)>(.*?)<\/head>/is, (_, existingAttrs, existingHead) => `<head${existingAttrs}>${existingHead}${headContent}</head>`);
|
|
670
|
+
return result.trim();
|
|
671
|
+
}
|
|
672
|
+
mergeAttributes(existing, attrs) {
|
|
673
|
+
const existingAttrs = this.parseAttributes(existing);
|
|
674
|
+
const merged = {
|
|
675
|
+
...existingAttrs,
|
|
676
|
+
...attrs
|
|
677
|
+
};
|
|
678
|
+
return Object.entries(merged).map(([k, v]) => ` ${k}="${this.escapeHtml(v)}"`).join("");
|
|
679
|
+
}
|
|
680
|
+
parseAttributes(attrStr) {
|
|
681
|
+
const attrs = {};
|
|
682
|
+
const attrRegex = /([^\s=]+)(?:="([^"]*)")?/g;
|
|
683
|
+
let match = attrRegex.exec(attrStr);
|
|
684
|
+
while (match) {
|
|
685
|
+
attrs[match[1]] = match[2] ?? "";
|
|
686
|
+
match = attrRegex.exec(attrStr);
|
|
687
|
+
}
|
|
688
|
+
return attrs;
|
|
689
|
+
}
|
|
690
|
+
escapeHtml(str) {
|
|
691
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/providers/ReactServerProvider.ts
|
|
697
|
+
const envSchema = __alepha_core.t.object({
|
|
698
|
+
REACT_SERVER_DIST: __alepha_core.t.string({ default: "public" }),
|
|
699
|
+
REACT_SERVER_PREFIX: __alepha_core.t.string({ default: "" }),
|
|
700
|
+
REACT_SSR_ENABLED: __alepha_core.t.optional(__alepha_core.t.boolean()),
|
|
701
|
+
REACT_ROOT_ID: __alepha_core.t.string({ default: "root" })
|
|
82
702
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
renderToHtml(template, state, context) {
|
|
293
|
-
const element = this.pageDescriptorProvider.root(state, context);
|
|
294
|
-
this.serverTimingProvider.beginTiming("renderToString");
|
|
295
|
-
let app = "";
|
|
296
|
-
try {
|
|
297
|
-
app = server$1.renderToString(element);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
this.log.error("Error during SSR", error);
|
|
300
|
-
app = server$1.renderToString(context.onError(error));
|
|
301
|
-
}
|
|
302
|
-
this.serverTimingProvider.endTiming("renderToString");
|
|
303
|
-
const hydrationData = {
|
|
304
|
-
links: context.links,
|
|
305
|
-
layers: state.layers.map((it) => ({
|
|
306
|
-
...it,
|
|
307
|
-
error: it.error ? {
|
|
308
|
-
...it.error,
|
|
309
|
-
name: it.error.name,
|
|
310
|
-
message: it.error.message,
|
|
311
|
-
stack: it.error.stack
|
|
312
|
-
// TODO: Hide stack in production ?
|
|
313
|
-
} : void 0,
|
|
314
|
-
index: void 0,
|
|
315
|
-
path: void 0,
|
|
316
|
-
element: void 0
|
|
317
|
-
}))
|
|
318
|
-
};
|
|
319
|
-
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
|
|
320
|
-
const response = {
|
|
321
|
-
html: template
|
|
322
|
-
};
|
|
323
|
-
this.fillTemplate(response, app, script);
|
|
324
|
-
if (context.head) {
|
|
325
|
-
response.html = this.headProvider.renderHead(response.html, context.head);
|
|
326
|
-
}
|
|
327
|
-
return response.html;
|
|
328
|
-
}
|
|
329
|
-
fillTemplate(response, app, script) {
|
|
330
|
-
if (this.ROOT_DIV_REGEX.test(response.html)) {
|
|
331
|
-
response.html = response.html.replace(
|
|
332
|
-
this.ROOT_DIV_REGEX,
|
|
333
|
-
(_match, beforeId, afterId) => {
|
|
334
|
-
return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
|
|
335
|
-
}
|
|
336
|
-
);
|
|
337
|
-
} else {
|
|
338
|
-
const bodyOpenTag = /<body([^>]*)>/i;
|
|
339
|
-
if (bodyOpenTag.test(response.html)) {
|
|
340
|
-
response.html = response.html.replace(bodyOpenTag, (match) => {
|
|
341
|
-
return `${match}
|
|
342
|
-
<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const bodyCloseTagRegex = /<\/body>/i;
|
|
347
|
-
if (bodyCloseTagRegex.test(response.html)) {
|
|
348
|
-
response.html = response.html.replace(
|
|
349
|
-
bodyCloseTagRegex,
|
|
350
|
-
`${script}
|
|
351
|
-
</body>`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
703
|
+
var ReactServerProvider = class {
|
|
704
|
+
log = (0, __alepha_core.$logger)();
|
|
705
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
706
|
+
pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
|
|
707
|
+
serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
|
|
708
|
+
serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
|
|
709
|
+
headProvider = (0, __alepha_core.$inject)(ServerHeadProvider);
|
|
710
|
+
serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
|
|
711
|
+
env = (0, __alepha_core.$inject)(envSchema);
|
|
712
|
+
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
713
|
+
onConfigure = (0, __alepha_core.$hook)({
|
|
714
|
+
name: "configure",
|
|
715
|
+
handler: async () => {
|
|
716
|
+
const pages = this.alepha.getDescriptorValues($page);
|
|
717
|
+
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
718
|
+
this.alepha.state("ReactServerProvider.ssr", ssrEnabled);
|
|
719
|
+
for (const { key, instance, value } of pages) {
|
|
720
|
+
const name = value[__alepha_core.OPTIONS].name ?? key;
|
|
721
|
+
instance[key].prerender = this.createRenderFunction(name, true);
|
|
722
|
+
if (this.alepha.isTest()) instance[key].render = this.createRenderFunction(name);
|
|
723
|
+
}
|
|
724
|
+
if (this.alepha.isServerless() === "vite") {
|
|
725
|
+
await this.configureVite(ssrEnabled);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
let root = "";
|
|
729
|
+
if (!this.alepha.isServerless()) {
|
|
730
|
+
root = this.getPublicDirectory();
|
|
731
|
+
if (!root) this.log.warn("Missing static files, static file server will be disabled");
|
|
732
|
+
else {
|
|
733
|
+
this.log.debug(`Using static files from: ${root}`);
|
|
734
|
+
await this.configureStaticServer(root);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (ssrEnabled) {
|
|
738
|
+
await this.registerPages(async () => this.template);
|
|
739
|
+
this.log.info("SSR OK");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.log.info("SSR is disabled, use History API fallback");
|
|
743
|
+
await this.serverRouterProvider.route({
|
|
744
|
+
path: "*",
|
|
745
|
+
handler: async ({ url, reply }) => {
|
|
746
|
+
if (url.pathname.includes(".")) {
|
|
747
|
+
reply.headers["content-type"] = "text/plain";
|
|
748
|
+
reply.body = "Not Found";
|
|
749
|
+
reply.status = 404;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
reply.headers["content-type"] = "text/html";
|
|
753
|
+
return this.template;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
get template() {
|
|
759
|
+
return this.alepha.state("ReactServerProvider.template");
|
|
760
|
+
}
|
|
761
|
+
async registerPages(templateLoader) {
|
|
762
|
+
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
763
|
+
if (page.children?.length) continue;
|
|
764
|
+
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
765
|
+
await this.serverRouterProvider.route({
|
|
766
|
+
...page,
|
|
767
|
+
schema: void 0,
|
|
768
|
+
method: "GET",
|
|
769
|
+
path: page.match,
|
|
770
|
+
handler: this.createHandler(page, templateLoader)
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
getPublicDirectory() {
|
|
775
|
+
const maybe = [(0, node_path.join)(process.cwd(), `dist/${this.env.REACT_SERVER_DIST}`), (0, node_path.join)(process.cwd(), this.env.REACT_SERVER_DIST)];
|
|
776
|
+
for (const it of maybe) if ((0, node_fs.existsSync)(it)) return it;
|
|
777
|
+
return "";
|
|
778
|
+
}
|
|
779
|
+
async configureStaticServer(root) {
|
|
780
|
+
await this.serverStaticProvider.serve({
|
|
781
|
+
root,
|
|
782
|
+
path: this.env.REACT_SERVER_PREFIX
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
async configureVite(ssrEnabled) {
|
|
786
|
+
if (!ssrEnabled) return;
|
|
787
|
+
this.log.info("SSR (vite) OK");
|
|
788
|
+
const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
|
|
789
|
+
await this.registerPages(() => fetch(`${url}/index.html`).then((it) => it.text()).catch(() => void 0));
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* For testing purposes, creates a render function that can be used.
|
|
793
|
+
*/
|
|
794
|
+
createRenderFunction(name, withIndex = false) {
|
|
795
|
+
return async (options = {}) => {
|
|
796
|
+
const page = this.pageDescriptorProvider.page(name);
|
|
797
|
+
const url = new URL(this.pageDescriptorProvider.url(name, options));
|
|
798
|
+
const context = {
|
|
799
|
+
url,
|
|
800
|
+
params: options.params ?? {},
|
|
801
|
+
query: options.query ?? {},
|
|
802
|
+
head: {},
|
|
803
|
+
onError: () => null
|
|
804
|
+
};
|
|
805
|
+
const state = await this.pageDescriptorProvider.createLayers(page, context);
|
|
806
|
+
if (!withIndex) return {
|
|
807
|
+
context,
|
|
808
|
+
html: (0, react_dom_server.renderToString)(this.pageDescriptorProvider.root(state, context))
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
context,
|
|
812
|
+
html: this.renderToHtml(this.template ?? "", state, context)
|
|
813
|
+
};
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
createHandler(page, templateLoader) {
|
|
817
|
+
return async (serverRequest) => {
|
|
818
|
+
const { url, reply, query, params } = serverRequest;
|
|
819
|
+
const template = await templateLoader();
|
|
820
|
+
if (!template) throw new Error("Template not found");
|
|
821
|
+
const context = {
|
|
822
|
+
url,
|
|
823
|
+
params,
|
|
824
|
+
query,
|
|
825
|
+
head: {},
|
|
826
|
+
onError: () => null
|
|
827
|
+
};
|
|
828
|
+
if (this.alepha.has(__alepha_server.ServerLinksProvider)) {
|
|
829
|
+
const srv = this.alepha.get(__alepha_server.ServerLinksProvider);
|
|
830
|
+
const schema = __alepha_server.apiLinksResponseSchema;
|
|
831
|
+
context.links = this.alepha.parse(schema, await srv.getLinks({
|
|
832
|
+
user: serverRequest.user,
|
|
833
|
+
authorization: serverRequest.headers.authorization
|
|
834
|
+
}));
|
|
835
|
+
this.alepha.context.set("links", context.links);
|
|
836
|
+
}
|
|
837
|
+
let target = page;
|
|
838
|
+
while (target) {
|
|
839
|
+
if (page.can && !page.can()) {
|
|
840
|
+
reply.status = 403;
|
|
841
|
+
reply.headers["content-type"] = "text/plain";
|
|
842
|
+
return "Forbidden";
|
|
843
|
+
}
|
|
844
|
+
target = target.parent;
|
|
845
|
+
}
|
|
846
|
+
await this.alepha.emit("react:server:render", {
|
|
847
|
+
request: serverRequest,
|
|
848
|
+
pageRequest: context
|
|
849
|
+
}, { log: false });
|
|
850
|
+
this.serverTimingProvider.beginTiming("createLayers");
|
|
851
|
+
const state = await this.pageDescriptorProvider.createLayers(page, context);
|
|
852
|
+
this.serverTimingProvider.endTiming("createLayers");
|
|
853
|
+
if (state.redirect) return reply.redirect(state.redirect);
|
|
854
|
+
reply.headers["content-type"] = "text/html";
|
|
855
|
+
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
856
|
+
reply.headers.pragma = "no-cache";
|
|
857
|
+
reply.headers.expires = "0";
|
|
858
|
+
if (page.cache && serverRequest.user) delete context.links;
|
|
859
|
+
const html = this.renderToHtml(template, state, context);
|
|
860
|
+
page.afterHandler?.(serverRequest);
|
|
861
|
+
return html;
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
renderToHtml(template, state, context) {
|
|
865
|
+
const element = this.pageDescriptorProvider.root(state, context);
|
|
866
|
+
this.serverTimingProvider.beginTiming("renderToString");
|
|
867
|
+
let app = "";
|
|
868
|
+
try {
|
|
869
|
+
app = (0, react_dom_server.renderToString)(element);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
this.log.error("Error during SSR", error);
|
|
872
|
+
app = (0, react_dom_server.renderToString)(context.onError(error));
|
|
873
|
+
}
|
|
874
|
+
this.serverTimingProvider.endTiming("renderToString");
|
|
875
|
+
const hydrationData = {
|
|
876
|
+
links: context.links,
|
|
877
|
+
layers: state.layers.map((it) => ({
|
|
878
|
+
...it,
|
|
879
|
+
error: it.error ? {
|
|
880
|
+
...it.error,
|
|
881
|
+
name: it.error.name,
|
|
882
|
+
message: it.error.message,
|
|
883
|
+
stack: it.error.stack
|
|
884
|
+
} : void 0,
|
|
885
|
+
index: void 0,
|
|
886
|
+
path: void 0,
|
|
887
|
+
element: void 0
|
|
888
|
+
}))
|
|
889
|
+
};
|
|
890
|
+
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
|
|
891
|
+
const response = { html: template };
|
|
892
|
+
this.fillTemplate(response, app, script);
|
|
893
|
+
if (context.head) response.html = this.headProvider.renderHead(response.html, context.head);
|
|
894
|
+
return response.html;
|
|
895
|
+
}
|
|
896
|
+
fillTemplate(response, app, script) {
|
|
897
|
+
if (this.ROOT_DIV_REGEX.test(response.html)) response.html = response.html.replace(this.ROOT_DIV_REGEX, (_match, beforeId, afterId) => {
|
|
898
|
+
return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
|
|
899
|
+
});
|
|
900
|
+
else {
|
|
901
|
+
const bodyOpenTag = /<body([^>]*)>/i;
|
|
902
|
+
if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
|
|
903
|
+
return `${match}\n<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
const bodyCloseTagRegex = /<\/body>/i;
|
|
907
|
+
if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}\n</body>`);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
356
910
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region src/providers/BrowserHeadProvider.ts
|
|
913
|
+
var BrowserHeadProvider = class {
|
|
914
|
+
renderHead(document, head) {
|
|
915
|
+
if (head.title) document.title = head.title;
|
|
916
|
+
if (head.bodyAttributes) for (const [key, value] of Object.entries(head.bodyAttributes)) if (value) document.body.setAttribute(key, value);
|
|
917
|
+
else document.body.removeAttribute(key);
|
|
918
|
+
if (head.htmlAttributes) for (const [key, value] of Object.entries(head.htmlAttributes)) if (value) document.documentElement.setAttribute(key, value);
|
|
919
|
+
else document.documentElement.removeAttribute(key);
|
|
920
|
+
if (head.meta) for (const [key, value] of Object.entries(head.meta)) {
|
|
921
|
+
const meta = document.querySelector(`meta[name="${key}"]`);
|
|
922
|
+
if (meta) meta.setAttribute("content", value.content);
|
|
923
|
+
else {
|
|
924
|
+
const newMeta = document.createElement("meta");
|
|
925
|
+
newMeta.setAttribute("name", key);
|
|
926
|
+
newMeta.setAttribute("content", value.content);
|
|
927
|
+
document.head.appendChild(newMeta);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
//#endregion
|
|
934
|
+
//#region src/providers/BrowserRouterProvider.ts
|
|
935
|
+
var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
936
|
+
log = (0, __alepha_core.$logger)();
|
|
937
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
938
|
+
pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
|
|
939
|
+
add(entry) {
|
|
940
|
+
this.pageDescriptorProvider.add(entry);
|
|
941
|
+
}
|
|
942
|
+
configure = (0, __alepha_core.$hook)({
|
|
943
|
+
name: "configure",
|
|
944
|
+
handler: async () => {
|
|
945
|
+
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
946
|
+
path: page.match,
|
|
947
|
+
page
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
async transition(url, options = {}) {
|
|
952
|
+
const { pathname, search } = url;
|
|
953
|
+
const state = {
|
|
954
|
+
pathname,
|
|
955
|
+
search,
|
|
956
|
+
layers: []
|
|
957
|
+
};
|
|
958
|
+
const context = {
|
|
959
|
+
url,
|
|
960
|
+
query: {},
|
|
961
|
+
params: {},
|
|
962
|
+
head: {},
|
|
963
|
+
onError: () => null,
|
|
964
|
+
...options.context ?? {}
|
|
965
|
+
};
|
|
966
|
+
await this.alepha.emit("react:transition:begin", {
|
|
967
|
+
state,
|
|
968
|
+
context
|
|
969
|
+
});
|
|
970
|
+
try {
|
|
971
|
+
const previous = options.previous;
|
|
972
|
+
const { route, params } = this.match(pathname);
|
|
973
|
+
const query = {};
|
|
974
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
975
|
+
context.query = query;
|
|
976
|
+
context.params = params ?? {};
|
|
977
|
+
context.previous = previous;
|
|
978
|
+
if (isPageRoute(route)) {
|
|
979
|
+
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
980
|
+
if (result.redirect) return {
|
|
981
|
+
redirect: result.redirect,
|
|
982
|
+
state,
|
|
983
|
+
context
|
|
984
|
+
};
|
|
985
|
+
state.layers = result.layers;
|
|
986
|
+
}
|
|
987
|
+
if (state.layers.length === 0) state.layers.push({
|
|
988
|
+
name: "not-found",
|
|
989
|
+
element: (0, react.createElement)(NotFoundPage),
|
|
990
|
+
index: 0,
|
|
991
|
+
path: "/"
|
|
992
|
+
});
|
|
993
|
+
await this.alepha.emit("react:transition:success", { state });
|
|
994
|
+
} catch (e) {
|
|
995
|
+
this.log.error(e);
|
|
996
|
+
state.layers = [{
|
|
997
|
+
name: "error",
|
|
998
|
+
element: this.pageDescriptorProvider.renderError(e),
|
|
999
|
+
index: 0,
|
|
1000
|
+
path: "/"
|
|
1001
|
+
}];
|
|
1002
|
+
await this.alepha.emit("react:transition:error", {
|
|
1003
|
+
error: e,
|
|
1004
|
+
state,
|
|
1005
|
+
context
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
if (options.state) {
|
|
1009
|
+
options.state.layers = state.layers;
|
|
1010
|
+
options.state.pathname = state.pathname;
|
|
1011
|
+
options.state.search = state.search;
|
|
1012
|
+
}
|
|
1013
|
+
await this.alepha.emit("react:transition:end", {
|
|
1014
|
+
state: options.state,
|
|
1015
|
+
context
|
|
1016
|
+
});
|
|
1017
|
+
return {
|
|
1018
|
+
context,
|
|
1019
|
+
state
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
root(state, context) {
|
|
1023
|
+
return this.pageDescriptorProvider.root(state, context);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/providers/ReactBrowserProvider.ts
|
|
1029
|
+
var ReactBrowserProvider = class {
|
|
1030
|
+
log = (0, __alepha_core.$logger)();
|
|
1031
|
+
client = (0, __alepha_core.$inject)(__alepha_server.HttpClient);
|
|
1032
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
1033
|
+
router = (0, __alepha_core.$inject)(BrowserRouterProvider);
|
|
1034
|
+
headProvider = (0, __alepha_core.$inject)(BrowserHeadProvider);
|
|
1035
|
+
root;
|
|
1036
|
+
transitioning;
|
|
1037
|
+
state = {
|
|
1038
|
+
layers: [],
|
|
1039
|
+
pathname: "",
|
|
1040
|
+
search: ""
|
|
1041
|
+
};
|
|
1042
|
+
get document() {
|
|
1043
|
+
return window.document;
|
|
1044
|
+
}
|
|
1045
|
+
get history() {
|
|
1046
|
+
return window.history;
|
|
1047
|
+
}
|
|
1048
|
+
get url() {
|
|
1049
|
+
return window.location.pathname + window.location.search;
|
|
1050
|
+
}
|
|
1051
|
+
async invalidate(props) {
|
|
1052
|
+
const previous = [];
|
|
1053
|
+
if (props) {
|
|
1054
|
+
const [key] = Object.keys(props);
|
|
1055
|
+
const value = props[key];
|
|
1056
|
+
for (const layer of this.state.layers) {
|
|
1057
|
+
if (layer.props?.[key]) {
|
|
1058
|
+
previous.push({
|
|
1059
|
+
...layer,
|
|
1060
|
+
props: {
|
|
1061
|
+
...layer.props,
|
|
1062
|
+
[key]: value
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
previous.push(layer);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
await this.render({ previous });
|
|
1071
|
+
}
|
|
1072
|
+
async go(url, options = {}) {
|
|
1073
|
+
const result = await this.render({ url });
|
|
1074
|
+
if (result.context.url.pathname !== url) {
|
|
1075
|
+
this.history.replaceState({}, "", result.context.url.pathname);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (options.replace) {
|
|
1079
|
+
this.history.replaceState({}, "", url);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
this.history.pushState({}, "", url);
|
|
1083
|
+
}
|
|
1084
|
+
async render(options = {}) {
|
|
1085
|
+
const previous = options.previous ?? this.state.layers;
|
|
1086
|
+
const url = options.url ?? this.url;
|
|
1087
|
+
this.transitioning = { to: url };
|
|
1088
|
+
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
1089
|
+
previous,
|
|
1090
|
+
state: this.state
|
|
1091
|
+
});
|
|
1092
|
+
if (result.redirect) return await this.render({ url: result.redirect });
|
|
1093
|
+
this.transitioning = void 0;
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Get embedded layers from the server.
|
|
1098
|
+
*/
|
|
1099
|
+
getHydrationState() {
|
|
1100
|
+
try {
|
|
1101
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
console.error(error);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
ready = (0, __alepha_core.$hook)({
|
|
1107
|
+
name: "ready",
|
|
1108
|
+
handler: async () => {
|
|
1109
|
+
const hydration = this.getHydrationState();
|
|
1110
|
+
const previous = hydration?.layers ?? [];
|
|
1111
|
+
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
|
|
1112
|
+
const { context } = await this.render({ previous });
|
|
1113
|
+
if (context.head) this.headProvider.renderHead(this.document, context.head);
|
|
1114
|
+
await this.alepha.emit("react:browser:render", {
|
|
1115
|
+
state: this.state,
|
|
1116
|
+
context,
|
|
1117
|
+
hydration
|
|
1118
|
+
});
|
|
1119
|
+
window.addEventListener("popstate", () => {
|
|
1120
|
+
this.render();
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
onTransitionEnd = (0, __alepha_core.$hook)({
|
|
1125
|
+
name: "react:transition:end",
|
|
1126
|
+
handler: async ({ context }) => {
|
|
1127
|
+
this.headProvider.renderHead(this.document, context.head);
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
448
1131
|
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region src/hooks/RouterHookApi.ts
|
|
1134
|
+
var RouterHookApi = class {
|
|
1135
|
+
constructor(pages, state, layer, browser) {
|
|
1136
|
+
this.pages = pages;
|
|
1137
|
+
this.state = state;
|
|
1138
|
+
this.layer = layer;
|
|
1139
|
+
this.browser = browser;
|
|
1140
|
+
}
|
|
1141
|
+
get current() {
|
|
1142
|
+
return this.state;
|
|
1143
|
+
}
|
|
1144
|
+
get pathname() {
|
|
1145
|
+
return this.state.pathname;
|
|
1146
|
+
}
|
|
1147
|
+
get query() {
|
|
1148
|
+
const query = {};
|
|
1149
|
+
for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
|
|
1150
|
+
return query;
|
|
1151
|
+
}
|
|
1152
|
+
async back() {
|
|
1153
|
+
this.browser?.history.back();
|
|
1154
|
+
}
|
|
1155
|
+
async forward() {
|
|
1156
|
+
this.browser?.history.forward();
|
|
1157
|
+
}
|
|
1158
|
+
async invalidate(props) {
|
|
1159
|
+
await this.browser?.invalidate(props);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Create a valid href for the given pathname.
|
|
1163
|
+
*
|
|
1164
|
+
* @param pathname
|
|
1165
|
+
* @param layer
|
|
1166
|
+
*/
|
|
1167
|
+
createHref(pathname, layer = this.layer, options = {}) {
|
|
1168
|
+
if (typeof pathname === "object") pathname = pathname.options.path ?? "";
|
|
1169
|
+
if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
|
|
1170
|
+
return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
1171
|
+
}
|
|
1172
|
+
async go(path, options) {
|
|
1173
|
+
for (const page of this.pages) if (page.name === path) {
|
|
1174
|
+
path = page.path ?? "";
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
await this.browser?.go(this.createHref(path, this.layer, options), options);
|
|
1178
|
+
}
|
|
1179
|
+
anchor(path, options = {}) {
|
|
1180
|
+
for (const page of this.pages) if (page.name === path) {
|
|
1181
|
+
path = page.path ?? "";
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
const href = this.createHref(path, this.layer, options);
|
|
1185
|
+
return {
|
|
1186
|
+
href,
|
|
1187
|
+
onClick: (ev) => {
|
|
1188
|
+
ev.stopPropagation();
|
|
1189
|
+
ev.preventDefault();
|
|
1190
|
+
this.go(path, options).catch(console.error);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Set query params.
|
|
1196
|
+
*
|
|
1197
|
+
* @param record
|
|
1198
|
+
* @param options
|
|
1199
|
+
*/
|
|
1200
|
+
setQueryParams(record, options = {}) {
|
|
1201
|
+
const func = typeof record === "function" ? record : () => record;
|
|
1202
|
+
const search = new URLSearchParams(func(this.query)).toString();
|
|
1203
|
+
const state = search ? `${this.pathname}?${search}` : this.pathname;
|
|
1204
|
+
if (options.push) window.history.pushState({}, "", state);
|
|
1205
|
+
else window.history.replaceState({}, "", state);
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region src/hooks/useRouter.ts
|
|
449
1211
|
const useRouter = () => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}, []);
|
|
458
|
-
return React.useMemo(
|
|
459
|
-
() => new RouterHookApi(
|
|
460
|
-
pages,
|
|
461
|
-
ctx.state,
|
|
462
|
-
layer,
|
|
463
|
-
ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider.ReactBrowserProvider) : void 0
|
|
464
|
-
),
|
|
465
|
-
[layer]
|
|
466
|
-
);
|
|
1212
|
+
const ctx = (0, react.useContext)(RouterContext);
|
|
1213
|
+
const layer = (0, react.useContext)(RouterLayerContext);
|
|
1214
|
+
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1215
|
+
const pages = (0, react.useMemo)(() => {
|
|
1216
|
+
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
1217
|
+
}, []);
|
|
1218
|
+
return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
467
1219
|
};
|
|
468
1220
|
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region src/components/Link.tsx
|
|
469
1223
|
const Link = (props) => {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1224
|
+
react.default.useContext(RouterContext);
|
|
1225
|
+
const router = useRouter();
|
|
1226
|
+
const to = typeof props.to === "string" ? props.to : props.to[__alepha_core.OPTIONS].path;
|
|
1227
|
+
if (!to) return null;
|
|
1228
|
+
const can = typeof props.to === "string" ? void 0 : props.to[__alepha_core.OPTIONS].can;
|
|
1229
|
+
if (can && !can()) return null;
|
|
1230
|
+
const name = typeof props.to === "string" ? void 0 : props.to[__alepha_core.OPTIONS].name;
|
|
1231
|
+
const anchorProps = {
|
|
1232
|
+
...props,
|
|
1233
|
+
to: void 0
|
|
1234
|
+
};
|
|
1235
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1236
|
+
...router.anchor(to),
|
|
1237
|
+
...anchorProps,
|
|
1238
|
+
children: props.children ?? name
|
|
1239
|
+
});
|
|
486
1240
|
};
|
|
1241
|
+
var Link_default = Link;
|
|
487
1242
|
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region src/hooks/useActive.ts
|
|
488
1245
|
const useActive = (path) => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
router.go(href).then(() => {
|
|
519
|
-
setPending(false);
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
};
|
|
1246
|
+
const router = useRouter();
|
|
1247
|
+
const ctx = (0, react.useContext)(RouterContext);
|
|
1248
|
+
const layer = (0, react.useContext)(RouterLayerContext);
|
|
1249
|
+
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1250
|
+
let name;
|
|
1251
|
+
if (typeof path === "object" && path.options.name) name = path.options.name;
|
|
1252
|
+
const [current, setCurrent] = (0, react.useState)(ctx.state.pathname);
|
|
1253
|
+
const href = (0, react.useMemo)(() => router.createHref(path, layer), [path, layer]);
|
|
1254
|
+
const [isPending, setPending] = (0, react.useState)(false);
|
|
1255
|
+
const isActive = current === href;
|
|
1256
|
+
useRouterEvents({ onEnd: ({ state }) => setCurrent(state.pathname) });
|
|
1257
|
+
return {
|
|
1258
|
+
name,
|
|
1259
|
+
isPending,
|
|
1260
|
+
isActive,
|
|
1261
|
+
anchorProps: {
|
|
1262
|
+
href,
|
|
1263
|
+
onClick: (ev) => {
|
|
1264
|
+
ev.stopPropagation();
|
|
1265
|
+
ev.preventDefault();
|
|
1266
|
+
if (isActive) return;
|
|
1267
|
+
if (isPending) return;
|
|
1268
|
+
setPending(true);
|
|
1269
|
+
router.go(href).then(() => {
|
|
1270
|
+
setPending(false);
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
524
1275
|
};
|
|
525
1276
|
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region src/hooks/useInject.ts
|
|
526
1279
|
const useInject = (clazz) => {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
return React.useMemo(() => ctx.alepha.get(clazz), []);
|
|
1280
|
+
const ctx = (0, react.useContext)(RouterContext);
|
|
1281
|
+
if (!ctx) throw new Error("useRouter must be used within a <RouterProvider>");
|
|
1282
|
+
return (0, react.useMemo)(() => ctx.alepha.get(clazz), []);
|
|
532
1283
|
};
|
|
533
1284
|
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/hooks/useClient.ts
|
|
534
1287
|
const useClient = (_scope) => {
|
|
535
|
-
|
|
1288
|
+
return useInject(__alepha_server.HttpClient).of();
|
|
536
1289
|
};
|
|
537
1290
|
|
|
1291
|
+
//#endregion
|
|
1292
|
+
//#region src/hooks/useQueryParams.ts
|
|
538
1293
|
const useQueryParams = (schema, options = {}) => {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return { ...data, [key]: encode(ctx.alepha, schema, queryParams2) };
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
];
|
|
1294
|
+
const ctx = (0, react.useContext)(RouterContext);
|
|
1295
|
+
if (!ctx) throw new Error("useQueryParams must be used within a RouterProvider");
|
|
1296
|
+
const key = options.key ?? "q";
|
|
1297
|
+
const router = useRouter();
|
|
1298
|
+
const querystring = router.query[key];
|
|
1299
|
+
const [queryParams, setQueryParams] = (0, react.useState)(decode(ctx.alepha, schema, router.query[key]));
|
|
1300
|
+
(0, react.useEffect)(() => {
|
|
1301
|
+
setQueryParams(decode(ctx.alepha, schema, querystring));
|
|
1302
|
+
}, [querystring]);
|
|
1303
|
+
return [queryParams, (queryParams$1) => {
|
|
1304
|
+
setQueryParams(queryParams$1);
|
|
1305
|
+
router.setQueryParams((data) => {
|
|
1306
|
+
return {
|
|
1307
|
+
...data,
|
|
1308
|
+
[key]: encode(ctx.alepha, schema, queryParams$1)
|
|
1309
|
+
};
|
|
1310
|
+
});
|
|
1311
|
+
}];
|
|
561
1312
|
};
|
|
562
1313
|
const encode = (alepha, schema, data) => {
|
|
563
|
-
|
|
1314
|
+
return btoa(JSON.stringify(alepha.parse(schema, data)));
|
|
564
1315
|
};
|
|
565
1316
|
const decode = (alepha, schema, data) => {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1317
|
+
try {
|
|
1318
|
+
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
1319
|
+
} catch (_error) {
|
|
1320
|
+
return {};
|
|
1321
|
+
}
|
|
571
1322
|
};
|
|
572
1323
|
|
|
1324
|
+
//#endregion
|
|
1325
|
+
//#region src/hooks/useRouterState.ts
|
|
573
1326
|
const useRouterState = () => {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
ReactBrowserProvider.useRouterEvents({
|
|
581
|
-
onEnd: ({ state: state2 }) => setState({ ...state2 })
|
|
582
|
-
});
|
|
583
|
-
return state;
|
|
1327
|
+
const ctx = (0, react.useContext)(RouterContext);
|
|
1328
|
+
const layer = (0, react.useContext)(RouterLayerContext);
|
|
1329
|
+
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1330
|
+
const [state, setState] = (0, react.useState)(ctx.state);
|
|
1331
|
+
useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
|
|
1332
|
+
return state;
|
|
584
1333
|
};
|
|
585
1334
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/index.ts
|
|
1337
|
+
/**
|
|
1338
|
+
* Alepha React Module
|
|
1339
|
+
*
|
|
1340
|
+
* Alepha React Module contains a router for client-side navigation and server-side rendering.
|
|
1341
|
+
* Routes can be defined using the `$page` descriptor.
|
|
1342
|
+
*
|
|
1343
|
+
* @see {@link $page}
|
|
1344
|
+
* @module alepha.react
|
|
1345
|
+
*/
|
|
1346
|
+
var AlephaReact = class {
|
|
1347
|
+
name = "alepha.react";
|
|
1348
|
+
$services = (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(ReactServerProvider).with(PageDescriptorProvider);
|
|
1349
|
+
};
|
|
1350
|
+
(0, __alepha_core.__bind)($page, AlephaReact);
|
|
591
1351
|
|
|
592
|
-
|
|
593
|
-
exports
|
|
594
|
-
exports.ErrorBoundary = ReactBrowserProvider.ErrorBoundary;
|
|
595
|
-
exports.NestedView = ReactBrowserProvider.NestedView;
|
|
596
|
-
exports.PageDescriptorProvider = ReactBrowserProvider.PageDescriptorProvider;
|
|
597
|
-
exports.ReactBrowserProvider = ReactBrowserProvider.ReactBrowserProvider;
|
|
598
|
-
exports.RedirectionError = ReactBrowserProvider.RedirectionError;
|
|
599
|
-
exports.RouterContext = ReactBrowserProvider.RouterContext;
|
|
600
|
-
exports.RouterLayerContext = ReactBrowserProvider.RouterLayerContext;
|
|
601
|
-
exports.isPageRoute = ReactBrowserProvider.isPageRoute;
|
|
602
|
-
exports.useAlepha = ReactBrowserProvider.useAlepha;
|
|
603
|
-
exports.useRouterEvents = ReactBrowserProvider.useRouterEvents;
|
|
1352
|
+
//#endregion
|
|
1353
|
+
exports.$page = $page;
|
|
604
1354
|
exports.AlephaReact = AlephaReact;
|
|
605
|
-
exports.
|
|
1355
|
+
exports.ClientOnly = ClientOnly_default;
|
|
1356
|
+
exports.ErrorBoundary = ErrorBoundary_default;
|
|
1357
|
+
exports.Link = Link_default;
|
|
1358
|
+
exports.NestedView = NestedView_default;
|
|
1359
|
+
exports.PageDescriptorProvider = PageDescriptorProvider;
|
|
1360
|
+
exports.ReactBrowserProvider = ReactBrowserProvider;
|
|
606
1361
|
exports.ReactServerProvider = ReactServerProvider;
|
|
1362
|
+
exports.RedirectionError = RedirectionError;
|
|
1363
|
+
exports.RouterContext = RouterContext;
|
|
607
1364
|
exports.RouterHookApi = RouterHookApi;
|
|
1365
|
+
exports.RouterLayerContext = RouterLayerContext;
|
|
1366
|
+
exports.isPageRoute = isPageRoute;
|
|
608
1367
|
exports.useActive = useActive;
|
|
1368
|
+
exports.useAlepha = useAlepha;
|
|
609
1369
|
exports.useClient = useClient;
|
|
610
1370
|
exports.useInject = useInject;
|
|
611
1371
|
exports.useQueryParams = useQueryParams;
|
|
612
1372
|
exports.useRouter = useRouter;
|
|
1373
|
+
exports.useRouterEvents = useRouterEvents;
|
|
613
1374
|
exports.useRouterState = useRouterState;
|
|
1375
|
+
//# sourceMappingURL=index.cjs.map
|