@alepha/react 0.6.1 → 0.6.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/README.md +1 -28
- package/dist/index.browser.cjs +19 -23
- package/dist/index.browser.js +7 -7
- package/dist/index.cjs +235 -512
- package/dist/index.d.ts +240 -678
- package/dist/index.js +220 -492
- package/dist/{useAuth-DOVx2kqa.cjs → useActive-BVqdq757.cjs} +333 -431
- package/dist/{useAuth-i7wbKVrt.js → useActive-dAmCT31a.js} +332 -427
- package/package.json +13 -14
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
import { __descriptor, KIND, NotImplementedError, EventEmitter, $logger, $inject, Alepha, $hook } from '@alepha/core';
|
|
2
1
|
import { jsx } from 'react/jsx-runtime';
|
|
3
2
|
import React, { createContext, useContext, useState, useEffect, createElement, useMemo } from 'react';
|
|
4
3
|
import { HttpClient } from '@alepha/server';
|
|
4
|
+
import { __descriptor, NotImplementedError, KIND, $logger, $inject, Alepha, EventEmitter, $hook, t } from '@alepha/core';
|
|
5
5
|
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
6
|
-
import {
|
|
6
|
+
import { RouterProvider } from '@alepha/router';
|
|
7
7
|
|
|
8
|
-
const KEY = "
|
|
9
|
-
const $
|
|
8
|
+
const KEY = "PAGE";
|
|
9
|
+
const $page = (options) => {
|
|
10
10
|
__descriptor(KEY);
|
|
11
|
+
if (options.children) {
|
|
12
|
+
for (const child of options.children) {
|
|
13
|
+
child.options.parent = {
|
|
14
|
+
options
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (options.parent) {
|
|
19
|
+
options.parent.options.children ??= [];
|
|
20
|
+
options.parent.options.children.push({
|
|
21
|
+
options
|
|
22
|
+
});
|
|
23
|
+
}
|
|
11
24
|
return {
|
|
12
25
|
[KIND]: KEY,
|
|
13
|
-
options
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
$auth[KIND] = KEY;
|
|
17
|
-
|
|
18
|
-
const pageDescriptorKey = "PAGE";
|
|
19
|
-
const $page = (options) => {
|
|
20
|
-
__descriptor(pageDescriptorKey);
|
|
21
|
-
return {
|
|
22
|
-
[KIND]: pageDescriptorKey,
|
|
23
26
|
options,
|
|
24
27
|
render: () => {
|
|
25
|
-
throw new NotImplementedError(
|
|
28
|
+
throw new NotImplementedError(KEY);
|
|
26
29
|
},
|
|
27
30
|
go: () => {
|
|
28
|
-
throw new NotImplementedError(
|
|
31
|
+
throw new NotImplementedError(KEY);
|
|
29
32
|
},
|
|
30
33
|
createAnchorProps: () => {
|
|
31
|
-
throw new NotImplementedError(
|
|
34
|
+
throw new NotImplementedError(KEY);
|
|
35
|
+
},
|
|
36
|
+
can: () => {
|
|
37
|
+
if (options.can) {
|
|
38
|
+
return options.can();
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
32
41
|
}
|
|
33
42
|
};
|
|
34
43
|
};
|
|
35
|
-
$page[KIND] =
|
|
44
|
+
$page[KIND] = KEY;
|
|
36
45
|
|
|
37
46
|
const RouterContext = createContext(
|
|
38
47
|
void 0
|
|
@@ -49,7 +58,7 @@ const NestedView = (props) => {
|
|
|
49
58
|
);
|
|
50
59
|
useEffect(() => {
|
|
51
60
|
if (app?.alepha.isBrowser()) {
|
|
52
|
-
return app?.
|
|
61
|
+
return app?.events.on("end", (state) => {
|
|
53
62
|
setView(state.layers[index]?.element);
|
|
54
63
|
});
|
|
55
64
|
}
|
|
@@ -64,162 +73,37 @@ class RedirectionError extends Error {
|
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
class
|
|
76
|
+
class PageDescriptorProvider {
|
|
68
77
|
log = $logger();
|
|
69
78
|
alepha = $inject(Alepha);
|
|
70
79
|
pages = [];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
*
|
|
75
|
-
* @param name - Page name
|
|
76
|
-
* @return PageRoute
|
|
77
|
-
*/
|
|
80
|
+
getPages() {
|
|
81
|
+
return this.pages;
|
|
82
|
+
}
|
|
78
83
|
page(name) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
for (const page of this.pages) {
|
|
85
|
+
if (page.name === name) {
|
|
86
|
+
return page;
|
|
87
|
+
}
|
|
82
88
|
}
|
|
83
|
-
|
|
89
|
+
throw new Error(`Page ${name} not found`);
|
|
84
90
|
}
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
*/
|
|
88
|
-
root(state, context = {}) {
|
|
91
|
+
root(state, context = {}, events) {
|
|
89
92
|
return createElement(
|
|
90
93
|
RouterContext.Provider,
|
|
91
94
|
{
|
|
92
95
|
value: {
|
|
93
|
-
state,
|
|
94
|
-
router: this,
|
|
95
96
|
alepha: this.alepha,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
97
|
+
state,
|
|
98
|
+
context,
|
|
99
|
+
events: events ?? new EventEmitter()
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
|
-
state.layers[0]?.element
|
|
102
|
+
createElement(NestedView, {}, state.layers[0]?.element)
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* @param url
|
|
108
|
-
* @param options
|
|
109
|
-
*/
|
|
110
|
-
async render(url, options = {}) {
|
|
111
|
-
const [pathname, search = ""] = url.split("?");
|
|
112
|
-
const state = {
|
|
113
|
-
pathname,
|
|
114
|
-
search,
|
|
115
|
-
layers: [],
|
|
116
|
-
context: {}
|
|
117
|
-
};
|
|
118
|
-
await this.emit("begin", void 0);
|
|
119
|
-
try {
|
|
120
|
-
let layers = await this.match(url, options, state.context);
|
|
121
|
-
if (layers.length === 0) {
|
|
122
|
-
if (this.notFoundPageRoute) {
|
|
123
|
-
layers = await this.createLayers(url, this.notFoundPageRoute);
|
|
124
|
-
} else {
|
|
125
|
-
layers.push({
|
|
126
|
-
name: "not-found",
|
|
127
|
-
element: "Not Found",
|
|
128
|
-
index: 0,
|
|
129
|
-
path: "/"
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
state.layers = layers;
|
|
134
|
-
await this.emit("success", void 0);
|
|
135
|
-
} catch (e) {
|
|
136
|
-
if (e instanceof RedirectionError) {
|
|
137
|
-
return {
|
|
138
|
-
element: null,
|
|
139
|
-
layers: [],
|
|
140
|
-
redirect: typeof e.page === "string" ? e.page : this.href(e.page),
|
|
141
|
-
context: state.context
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
this.log.error(e);
|
|
145
|
-
state.layers = [
|
|
146
|
-
{
|
|
147
|
-
name: "error",
|
|
148
|
-
element: this.renderError(e),
|
|
149
|
-
index: 0,
|
|
150
|
-
path: "/"
|
|
151
|
-
}
|
|
152
|
-
];
|
|
153
|
-
await this.emit("error", e);
|
|
154
|
-
}
|
|
155
|
-
if (options.state) {
|
|
156
|
-
options.state.layers = state.layers;
|
|
157
|
-
options.state.pathname = state.pathname;
|
|
158
|
-
options.state.search = state.search;
|
|
159
|
-
options.state.context = state.context;
|
|
160
|
-
await this.emit("end", options.state);
|
|
161
|
-
return {
|
|
162
|
-
element: this.root(options.state, options.args),
|
|
163
|
-
layers: options.state.layers,
|
|
164
|
-
context: state.context
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
await this.emit("end", state);
|
|
168
|
-
return {
|
|
169
|
-
element: this.root(state, options.args),
|
|
170
|
-
layers: state.layers,
|
|
171
|
-
context: state.context
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
* @param url
|
|
177
|
-
* @param options
|
|
178
|
-
* @param context
|
|
179
|
-
* @protected
|
|
180
|
-
*/
|
|
181
|
-
async match(url, options = {}, context = {}) {
|
|
182
|
-
const pages = this.pages;
|
|
183
|
-
const previous = options.previous;
|
|
184
|
-
const [pathname, search] = url.split("?");
|
|
185
|
-
for (const route of pages) {
|
|
186
|
-
if (route.children?.find((it) => !it.path || it.path === "/")) continue;
|
|
187
|
-
if (!route.match) continue;
|
|
188
|
-
const match2 = route.match.exec(pathname);
|
|
189
|
-
if (match2) {
|
|
190
|
-
const params = match2.params ?? {};
|
|
191
|
-
const query = {};
|
|
192
|
-
if (search) {
|
|
193
|
-
for (const [key, value] of new URLSearchParams(search).entries()) {
|
|
194
|
-
query[key] = String(value);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return await this.createLayers(
|
|
198
|
-
url,
|
|
199
|
-
route,
|
|
200
|
-
params,
|
|
201
|
-
query,
|
|
202
|
-
previous,
|
|
203
|
-
options.args,
|
|
204
|
-
context
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return [];
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Create layers for the given route.
|
|
212
|
-
*
|
|
213
|
-
* @param url
|
|
214
|
-
* @param route
|
|
215
|
-
* @param params
|
|
216
|
-
* @param query
|
|
217
|
-
* @param previous
|
|
218
|
-
* @param args
|
|
219
|
-
* @param renderContext
|
|
220
|
-
* @protected
|
|
221
|
-
*/
|
|
222
|
-
async createLayers(url, route, params = {}, query = {}, previous = [], args, renderContext) {
|
|
105
|
+
async createLayers(route, request) {
|
|
106
|
+
const { pathname, search } = request.url;
|
|
223
107
|
const layers = [];
|
|
224
108
|
let context = {};
|
|
225
109
|
const stack = [{ route }];
|
|
@@ -234,13 +118,13 @@ class Router extends EventEmitter {
|
|
|
234
118
|
const route2 = it.route;
|
|
235
119
|
const config = {};
|
|
236
120
|
try {
|
|
237
|
-
config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, query) : query;
|
|
121
|
+
config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, request.query) : request.query;
|
|
238
122
|
} catch (e) {
|
|
239
123
|
it.error = e;
|
|
240
124
|
break;
|
|
241
125
|
}
|
|
242
126
|
try {
|
|
243
|
-
config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, params) : params;
|
|
127
|
+
config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, request.params) : request.params;
|
|
244
128
|
} catch (e) {
|
|
245
129
|
it.error = e;
|
|
246
130
|
break;
|
|
@@ -251,18 +135,20 @@ class Router extends EventEmitter {
|
|
|
251
135
|
if (!route2.resolve) {
|
|
252
136
|
continue;
|
|
253
137
|
}
|
|
138
|
+
const previous = request.previous;
|
|
254
139
|
if (previous?.[i] && !forceRefresh && previous[i].name === route2.name) {
|
|
255
|
-
const
|
|
140
|
+
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
256
141
|
const prev = JSON.stringify({
|
|
257
|
-
part:
|
|
142
|
+
part: url(previous[i].part),
|
|
258
143
|
params: previous[i].config?.params ?? {}
|
|
259
144
|
});
|
|
260
145
|
const curr = JSON.stringify({
|
|
261
|
-
part:
|
|
146
|
+
part: url(route2.path),
|
|
262
147
|
params: config.params ?? {}
|
|
263
148
|
});
|
|
264
149
|
if (prev === curr) {
|
|
265
150
|
it.props = previous[i].props;
|
|
151
|
+
it.error = previous[i].error;
|
|
266
152
|
context = {
|
|
267
153
|
...context,
|
|
268
154
|
...it.props
|
|
@@ -272,15 +158,14 @@ class Router extends EventEmitter {
|
|
|
272
158
|
forceRefresh = true;
|
|
273
159
|
}
|
|
274
160
|
try {
|
|
275
|
-
const props = await route2.resolve?.(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
) ?? {};
|
|
161
|
+
const props = await route2.resolve?.({
|
|
162
|
+
...request,
|
|
163
|
+
// request
|
|
164
|
+
...config,
|
|
165
|
+
// params, query
|
|
166
|
+
...context
|
|
167
|
+
// previous props
|
|
168
|
+
}) ?? {};
|
|
284
169
|
it.props = {
|
|
285
170
|
...props
|
|
286
171
|
};
|
|
@@ -290,7 +175,13 @@ class Router extends EventEmitter {
|
|
|
290
175
|
};
|
|
291
176
|
} catch (e) {
|
|
292
177
|
if (e instanceof RedirectionError) {
|
|
293
|
-
|
|
178
|
+
return {
|
|
179
|
+
layers: [],
|
|
180
|
+
redirect: typeof e.page === "string" ? e.page : this.href(e.page),
|
|
181
|
+
head: request.head,
|
|
182
|
+
pathname,
|
|
183
|
+
search
|
|
184
|
+
};
|
|
294
185
|
}
|
|
295
186
|
this.log.error(e);
|
|
296
187
|
it.error = e;
|
|
@@ -301,28 +192,29 @@ class Router extends EventEmitter {
|
|
|
301
192
|
for (let i = 0; i < stack.length; i++) {
|
|
302
193
|
const it = stack[i];
|
|
303
194
|
const props = it.props ?? {};
|
|
304
|
-
const
|
|
305
|
-
for (const key of Object.keys(
|
|
306
|
-
|
|
195
|
+
const params = { ...it.config?.params };
|
|
196
|
+
for (const key of Object.keys(params)) {
|
|
197
|
+
params[key] = String(params[key]);
|
|
307
198
|
}
|
|
308
|
-
if (it.route.
|
|
309
|
-
this.
|
|
199
|
+
if (it.route.head && !it.error) {
|
|
200
|
+
this.fillHead(it.route, request, {
|
|
310
201
|
...props,
|
|
311
202
|
...context
|
|
312
203
|
});
|
|
313
204
|
}
|
|
314
205
|
acc += "/";
|
|
315
|
-
acc += it.route.path ? compile(it.route.path)
|
|
206
|
+
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
316
207
|
const path = acc.replace(/\/+/, "/");
|
|
317
208
|
if (it.error) {
|
|
318
209
|
const errorHandler = this.getErrorHandler(it.route);
|
|
319
|
-
const element = errorHandler ? errorHandler({
|
|
210
|
+
const element = await (errorHandler ? errorHandler({
|
|
320
211
|
...it.config,
|
|
321
212
|
error: it.error,
|
|
322
|
-
url
|
|
323
|
-
}) : this.renderError(it.error);
|
|
213
|
+
url: ""
|
|
214
|
+
}) : this.renderError(it.error));
|
|
324
215
|
layers.push({
|
|
325
216
|
props,
|
|
217
|
+
error: it.error,
|
|
326
218
|
name: it.route.name,
|
|
327
219
|
part: it.route.path,
|
|
328
220
|
config: it.config,
|
|
@@ -346,13 +238,8 @@ class Router extends EventEmitter {
|
|
|
346
238
|
path
|
|
347
239
|
});
|
|
348
240
|
}
|
|
349
|
-
return layers;
|
|
241
|
+
return { layers, head: request.head, pathname, search };
|
|
350
242
|
}
|
|
351
|
-
/**
|
|
352
|
-
*
|
|
353
|
-
* @param route
|
|
354
|
-
* @protected
|
|
355
|
-
*/
|
|
356
243
|
getErrorHandler(route) {
|
|
357
244
|
if (route.errorHandler) return route.errorHandler;
|
|
358
245
|
let parent = route.parent;
|
|
@@ -361,12 +248,6 @@ class Router extends EventEmitter {
|
|
|
361
248
|
parent = parent.parent;
|
|
362
249
|
}
|
|
363
250
|
}
|
|
364
|
-
/**
|
|
365
|
-
*
|
|
366
|
-
* @param page
|
|
367
|
-
* @param props
|
|
368
|
-
* @protected
|
|
369
|
-
*/
|
|
370
251
|
async createElement(page, props) {
|
|
371
252
|
if (page.lazy) {
|
|
372
253
|
const component = await page.lazy();
|
|
@@ -377,48 +258,43 @@ class Router extends EventEmitter {
|
|
|
377
258
|
}
|
|
378
259
|
return void 0;
|
|
379
260
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
ctx.helmet ??= {};
|
|
393
|
-
if (ctx.helmet?.title) {
|
|
394
|
-
ctx.helmet.title = `${helmet.title} - ${ctx.helmet.title}`;
|
|
395
|
-
} else {
|
|
396
|
-
ctx.helmet.title = helmet.title;
|
|
397
|
-
}
|
|
261
|
+
fillHead(page, ctx, props) {
|
|
262
|
+
if (!page.head) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
ctx.head ??= {};
|
|
266
|
+
const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
|
|
267
|
+
if (head.title) {
|
|
268
|
+
ctx.head ??= {};
|
|
269
|
+
if (ctx.head.titleSeparator) {
|
|
270
|
+
ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
|
|
271
|
+
} else {
|
|
272
|
+
ctx.head.title = head.title;
|
|
398
273
|
}
|
|
274
|
+
ctx.head.titleSeparator = head.titleSeparator;
|
|
275
|
+
}
|
|
276
|
+
if (head.htmlAttributes) {
|
|
277
|
+
ctx.head.htmlAttributes = {
|
|
278
|
+
...ctx.head.htmlAttributes,
|
|
279
|
+
...head.htmlAttributes
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (head.bodyAttributes) {
|
|
283
|
+
ctx.head.bodyAttributes = {
|
|
284
|
+
...ctx.head.bodyAttributes,
|
|
285
|
+
...head.bodyAttributes
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (head.meta) {
|
|
289
|
+
ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
|
|
399
290
|
}
|
|
400
291
|
}
|
|
401
|
-
/**
|
|
402
|
-
*
|
|
403
|
-
* @param e
|
|
404
|
-
* @protected
|
|
405
|
-
*/
|
|
406
292
|
renderError(e) {
|
|
407
293
|
return createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
|
|
408
294
|
}
|
|
409
|
-
/**
|
|
410
|
-
* Render an empty view.
|
|
411
|
-
*
|
|
412
|
-
* @protected
|
|
413
|
-
*/
|
|
414
295
|
renderEmptyView() {
|
|
415
296
|
return createElement(NestedView, {});
|
|
416
297
|
}
|
|
417
|
-
/**
|
|
418
|
-
* Create a valid href for the given page.
|
|
419
|
-
* @param page
|
|
420
|
-
* @param params
|
|
421
|
-
*/
|
|
422
298
|
href(page, params = {}) {
|
|
423
299
|
const found = this.pages.find((it) => it.name === page.options.name);
|
|
424
300
|
if (!found) {
|
|
@@ -430,16 +306,15 @@ class Router extends EventEmitter {
|
|
|
430
306
|
url = `${parent.path ?? ""}/${url}`;
|
|
431
307
|
parent = parent.parent;
|
|
432
308
|
}
|
|
433
|
-
url = compile(url
|
|
309
|
+
url = this.compile(url, params);
|
|
434
310
|
return url.replace(/\/\/+/g, "/") || "/";
|
|
435
311
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
*/
|
|
312
|
+
compile(path, params = {}) {
|
|
313
|
+
for (const [key, value] of Object.entries(params)) {
|
|
314
|
+
path = path.replace(`:${key}`, value);
|
|
315
|
+
}
|
|
316
|
+
return path;
|
|
317
|
+
}
|
|
443
318
|
renderView(index, path, view = this.renderEmptyView()) {
|
|
444
319
|
return createElement(
|
|
445
320
|
RouterLayerContext.Provider,
|
|
@@ -452,23 +327,39 @@ class Router extends EventEmitter {
|
|
|
452
327
|
view
|
|
453
328
|
);
|
|
454
329
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
330
|
+
configure = $hook({
|
|
331
|
+
name: "configure",
|
|
332
|
+
handler: () => {
|
|
333
|
+
const pages = this.alepha.getDescriptorValues($page);
|
|
334
|
+
for (const { value, key } of pages) {
|
|
335
|
+
value.options.name ??= key;
|
|
336
|
+
if (value.options.parent) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
this.add(this.map(pages, value));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
map(pages, target) {
|
|
344
|
+
const children = target.options.children ?? [];
|
|
345
|
+
for (const it of pages) {
|
|
346
|
+
if (it.value.options.parent === target) {
|
|
347
|
+
children.push(it.value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
...target.options,
|
|
352
|
+
parent: void 0,
|
|
353
|
+
children: children.map((it) => this.map(pages, it))
|
|
354
|
+
};
|
|
355
|
+
}
|
|
459
356
|
add(entry) {
|
|
460
357
|
if (this.alepha.isReady()) {
|
|
461
358
|
throw new Error("Router is already initialized");
|
|
462
359
|
}
|
|
463
|
-
if (entry.notFoundHandler) {
|
|
464
|
-
this.notFoundPageRoute = {
|
|
465
|
-
name: "not-found",
|
|
466
|
-
component: entry.notFoundHandler
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
360
|
entry.name ??= this.nextId();
|
|
470
361
|
const page = entry;
|
|
471
|
-
page.match = this.
|
|
362
|
+
page.match = this.createMatch(page);
|
|
472
363
|
this.pages.push(page);
|
|
473
364
|
if (page.children) {
|
|
474
365
|
for (const child of page.children) {
|
|
@@ -477,13 +368,7 @@ class Router extends EventEmitter {
|
|
|
477
368
|
}
|
|
478
369
|
}
|
|
479
370
|
}
|
|
480
|
-
|
|
481
|
-
* Create a match function for the given page.
|
|
482
|
-
*
|
|
483
|
-
* @param page
|
|
484
|
-
* @protected
|
|
485
|
-
*/
|
|
486
|
-
createMatchFunction(page) {
|
|
371
|
+
createMatch(page) {
|
|
487
372
|
let url = page.path ?? "/";
|
|
488
373
|
let target = page.parent;
|
|
489
374
|
while (target) {
|
|
@@ -491,112 +376,195 @@ class Router extends EventEmitter {
|
|
|
491
376
|
target = target.parent;
|
|
492
377
|
}
|
|
493
378
|
let path = url.replace(/\/\/+/g, "/");
|
|
494
|
-
if (path.endsWith("/")) {
|
|
379
|
+
if (path.endsWith("/") && path !== "/") {
|
|
495
380
|
path = path.slice(0, -1);
|
|
496
381
|
}
|
|
497
|
-
|
|
498
|
-
return {
|
|
499
|
-
exec: match(path.split("?")[0]),
|
|
500
|
-
path
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
return {
|
|
504
|
-
exec: match(path),
|
|
505
|
-
path
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
*
|
|
510
|
-
*/
|
|
511
|
-
empty() {
|
|
512
|
-
return this.pages.length === 0;
|
|
382
|
+
return path;
|
|
513
383
|
}
|
|
514
|
-
/**
|
|
515
|
-
*
|
|
516
|
-
* @protected
|
|
517
|
-
*/
|
|
518
384
|
_next = 0;
|
|
519
|
-
/**
|
|
520
|
-
*
|
|
521
|
-
* @protected
|
|
522
|
-
*/
|
|
523
385
|
nextId() {
|
|
524
386
|
this._next += 1;
|
|
525
387
|
return `P${this._next}`;
|
|
526
388
|
}
|
|
527
389
|
}
|
|
390
|
+
const isPageRoute = (it) => {
|
|
391
|
+
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
392
|
+
};
|
|
528
393
|
|
|
529
|
-
class
|
|
394
|
+
class BrowserHeadProvider {
|
|
395
|
+
renderHead(document, head) {
|
|
396
|
+
if (head.title) {
|
|
397
|
+
document.title = head.title;
|
|
398
|
+
}
|
|
399
|
+
if (head.bodyAttributes) {
|
|
400
|
+
for (const [key, value] of Object.entries(head.bodyAttributes)) {
|
|
401
|
+
if (value) {
|
|
402
|
+
document.body.setAttribute(key, value);
|
|
403
|
+
} else {
|
|
404
|
+
document.body.removeAttribute(key);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (head.htmlAttributes) {
|
|
409
|
+
for (const [key, value] of Object.entries(head.htmlAttributes)) {
|
|
410
|
+
if (value) {
|
|
411
|
+
document.documentElement.setAttribute(key, value);
|
|
412
|
+
} else {
|
|
413
|
+
document.documentElement.removeAttribute(key);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (head.meta) {
|
|
418
|
+
for (const [key, value] of Object.entries(head.meta)) {
|
|
419
|
+
const meta = document.querySelector(`meta[name="${key}"]`);
|
|
420
|
+
if (meta) {
|
|
421
|
+
meta.setAttribute("content", value.content);
|
|
422
|
+
} else {
|
|
423
|
+
const newMeta = document.createElement("meta");
|
|
424
|
+
newMeta.setAttribute("name", key);
|
|
425
|
+
newMeta.setAttribute("content", value.content);
|
|
426
|
+
document.head.appendChild(newMeta);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
class BrowserRouterProvider extends RouterProvider {
|
|
434
|
+
log = $logger();
|
|
530
435
|
alepha = $inject(Alepha);
|
|
531
|
-
|
|
436
|
+
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
437
|
+
events = new EventEmitter();
|
|
438
|
+
add(entry) {
|
|
439
|
+
this.pageDescriptorProvider.add(entry);
|
|
440
|
+
}
|
|
532
441
|
configure = $hook({
|
|
533
442
|
name: "configure",
|
|
534
|
-
handler: () => {
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
443
|
+
handler: async () => {
|
|
444
|
+
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
445
|
+
if (page.component || page.lazy) {
|
|
446
|
+
this.push({
|
|
447
|
+
path: page.match,
|
|
448
|
+
page
|
|
449
|
+
});
|
|
540
450
|
}
|
|
541
|
-
this.router.add(this.map(pages, value));
|
|
542
451
|
}
|
|
543
452
|
}
|
|
544
453
|
});
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
454
|
+
async transition(url, options = {}) {
|
|
455
|
+
const { pathname, search } = url;
|
|
456
|
+
const state = {
|
|
457
|
+
pathname,
|
|
458
|
+
search,
|
|
459
|
+
layers: [],
|
|
460
|
+
head: {}
|
|
461
|
+
};
|
|
462
|
+
await this.events.emit("begin", void 0);
|
|
463
|
+
try {
|
|
464
|
+
const previous = options.previous;
|
|
465
|
+
const { route, params } = this.match(pathname);
|
|
466
|
+
const query = {};
|
|
467
|
+
if (search) {
|
|
468
|
+
for (const [key, value] of new URLSearchParams(search).entries()) {
|
|
469
|
+
query[key] = String(value);
|
|
470
|
+
}
|
|
556
471
|
}
|
|
472
|
+
if (isPageRoute(route)) {
|
|
473
|
+
const result = await this.pageDescriptorProvider.createLayers(
|
|
474
|
+
route.page,
|
|
475
|
+
{
|
|
476
|
+
url,
|
|
477
|
+
params: params ?? {},
|
|
478
|
+
query,
|
|
479
|
+
previous,
|
|
480
|
+
...state,
|
|
481
|
+
head: state.head,
|
|
482
|
+
...options.context ?? {}
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
if (result.redirect) {
|
|
486
|
+
return {
|
|
487
|
+
element: null,
|
|
488
|
+
layers: [],
|
|
489
|
+
redirect: result.redirect,
|
|
490
|
+
head: state.head
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
state.layers = result.layers;
|
|
494
|
+
state.head = result.head;
|
|
495
|
+
}
|
|
496
|
+
if (state.layers.length === 0) {
|
|
497
|
+
state.layers.push({
|
|
498
|
+
name: "not-found",
|
|
499
|
+
element: "Not Found",
|
|
500
|
+
index: 0,
|
|
501
|
+
path: "/"
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
await this.events.emit("success", void 0);
|
|
505
|
+
} catch (e) {
|
|
506
|
+
this.log.error(e);
|
|
507
|
+
state.layers = [
|
|
508
|
+
{
|
|
509
|
+
name: "error",
|
|
510
|
+
element: this.pageDescriptorProvider.renderError(e),
|
|
511
|
+
index: 0,
|
|
512
|
+
path: "/"
|
|
513
|
+
}
|
|
514
|
+
];
|
|
515
|
+
await this.events.emit("error", e);
|
|
557
516
|
}
|
|
517
|
+
if (!options.state) {
|
|
518
|
+
await this.events.emit("end", state);
|
|
519
|
+
return {
|
|
520
|
+
element: this.root(state, options.context),
|
|
521
|
+
layers: state.layers,
|
|
522
|
+
head: state.head
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
options.state.layers = state.layers;
|
|
526
|
+
options.state.pathname = state.pathname;
|
|
527
|
+
options.state.search = state.search;
|
|
528
|
+
options.state.head = state.head;
|
|
529
|
+
await this.events.emit("end", options.state);
|
|
558
530
|
return {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
531
|
+
element: this.root(state, options.context),
|
|
532
|
+
layers: options.state.layers,
|
|
533
|
+
head: state.head
|
|
562
534
|
};
|
|
563
535
|
}
|
|
536
|
+
root(state, context = {}) {
|
|
537
|
+
return this.pageDescriptorProvider.root(state, context, this.events);
|
|
538
|
+
}
|
|
564
539
|
}
|
|
565
540
|
|
|
541
|
+
const envSchema = t.object({
|
|
542
|
+
REACT_ROOT_ID: t.string({ default: "root" })
|
|
543
|
+
});
|
|
566
544
|
class ReactBrowserProvider {
|
|
567
545
|
log = $logger();
|
|
568
546
|
client = $inject(HttpClient);
|
|
569
|
-
|
|
547
|
+
alepha = $inject(Alepha);
|
|
548
|
+
router = $inject(BrowserRouterProvider);
|
|
549
|
+
headProvider = $inject(BrowserHeadProvider);
|
|
550
|
+
env = $inject(envSchema);
|
|
570
551
|
root;
|
|
571
552
|
transitioning;
|
|
572
553
|
state = {
|
|
573
554
|
layers: [],
|
|
574
555
|
pathname: "",
|
|
575
556
|
search: "",
|
|
576
|
-
|
|
557
|
+
head: {}
|
|
577
558
|
};
|
|
578
|
-
/**
|
|
579
|
-
*
|
|
580
|
-
*/
|
|
581
559
|
get document() {
|
|
582
560
|
return window.document;
|
|
583
561
|
}
|
|
584
|
-
/**
|
|
585
|
-
*
|
|
586
|
-
*/
|
|
587
562
|
get history() {
|
|
588
563
|
return window.history;
|
|
589
564
|
}
|
|
590
|
-
/**
|
|
591
|
-
*
|
|
592
|
-
*/
|
|
593
565
|
get url() {
|
|
594
566
|
return window.location.pathname + window.location.search;
|
|
595
567
|
}
|
|
596
|
-
/**
|
|
597
|
-
*
|
|
598
|
-
* @param props
|
|
599
|
-
*/
|
|
600
568
|
async invalidate(props) {
|
|
601
569
|
const previous = [];
|
|
602
570
|
if (props) {
|
|
@@ -637,29 +605,22 @@ class ReactBrowserProvider {
|
|
|
637
605
|
}
|
|
638
606
|
this.history.pushState({}, "", url);
|
|
639
607
|
}
|
|
640
|
-
/**
|
|
641
|
-
*
|
|
642
|
-
* @param options
|
|
643
|
-
* @protected
|
|
644
|
-
*/
|
|
645
608
|
async render(options = {}) {
|
|
646
609
|
const previous = options.previous ?? this.state.layers;
|
|
647
610
|
const url = options.url ?? this.url;
|
|
648
611
|
this.transitioning = { to: url };
|
|
649
|
-
const result = await this.router.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
612
|
+
const result = await this.router.transition(
|
|
613
|
+
new URL(`http://localhost${url}`),
|
|
614
|
+
{
|
|
615
|
+
previous,
|
|
616
|
+
state: this.state
|
|
617
|
+
}
|
|
618
|
+
);
|
|
653
619
|
if (result.redirect) {
|
|
654
620
|
return await this.render({ url: result.redirect });
|
|
655
621
|
}
|
|
656
622
|
this.transitioning = void 0;
|
|
657
|
-
return { url };
|
|
658
|
-
}
|
|
659
|
-
renderHelmetContext(ctx) {
|
|
660
|
-
if (ctx.title) {
|
|
661
|
-
this.document.title = ctx.title;
|
|
662
|
-
}
|
|
623
|
+
return { url, head: result.head };
|
|
663
624
|
}
|
|
664
625
|
/**
|
|
665
626
|
* Get embedded layers from the server.
|
|
@@ -680,27 +641,15 @@ class ReactBrowserProvider {
|
|
|
680
641
|
* @protected
|
|
681
642
|
*/
|
|
682
643
|
getRootElement() {
|
|
683
|
-
const root = this.document.getElementById(
|
|
644
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
684
645
|
if (root) {
|
|
685
646
|
return root;
|
|
686
647
|
}
|
|
687
648
|
const div = this.document.createElement("div");
|
|
688
|
-
div.id =
|
|
689
|
-
this.document.body.
|
|
649
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
650
|
+
this.document.body.prepend(div);
|
|
690
651
|
return div;
|
|
691
652
|
}
|
|
692
|
-
getUserFromCookies() {
|
|
693
|
-
const cookies = this.document.cookie.split("; ");
|
|
694
|
-
const userCookie = cookies.find((cookie) => cookie.startsWith("user="));
|
|
695
|
-
try {
|
|
696
|
-
if (userCookie) {
|
|
697
|
-
return JSON.parse(decodeURIComponent(userCookie.split("=")[1]));
|
|
698
|
-
}
|
|
699
|
-
} catch (error) {
|
|
700
|
-
this.log.warn(error, "Failed to parse user cookie");
|
|
701
|
-
}
|
|
702
|
-
return void 0;
|
|
703
|
-
}
|
|
704
653
|
// -------------------------------------------------------------------------------------------------------------------
|
|
705
654
|
/**
|
|
706
655
|
*
|
|
@@ -711,73 +660,35 @@ class ReactBrowserProvider {
|
|
|
711
660
|
handler: async () => {
|
|
712
661
|
const cache = this.getHydrationState();
|
|
713
662
|
const previous = cache?.layers ?? [];
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
663
|
+
if (cache?.links) {
|
|
664
|
+
this.client.links = cache.links;
|
|
665
|
+
}
|
|
666
|
+
const { head } = await this.render({ previous });
|
|
667
|
+
if (head) {
|
|
668
|
+
this.headProvider.renderHead(this.document, head);
|
|
669
|
+
}
|
|
670
|
+
const context = {};
|
|
671
|
+
await this.alepha.run("react:browser:render", {
|
|
672
|
+
context,
|
|
673
|
+
cache
|
|
717
674
|
});
|
|
675
|
+
const element = this.router.root(this.state, context);
|
|
718
676
|
if (previous.length > 0) {
|
|
719
677
|
this.root = hydrateRoot(this.getRootElement(), element);
|
|
720
678
|
this.log.info("Hydrated root element");
|
|
721
679
|
} else {
|
|
722
|
-
this.root
|
|
680
|
+
this.root ??= createRoot(this.getRootElement());
|
|
723
681
|
this.root.render(element);
|
|
724
682
|
this.log.info("Created root element");
|
|
725
683
|
}
|
|
726
684
|
window.addEventListener("popstate", () => {
|
|
727
685
|
this.render();
|
|
728
686
|
});
|
|
729
|
-
this.router.on("end", ({
|
|
730
|
-
|
|
731
|
-
this.renderHelmetContext(context.helmet);
|
|
732
|
-
}
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
/**
|
|
737
|
-
*
|
|
738
|
-
* @protected
|
|
739
|
-
*/
|
|
740
|
-
stop = $hook({
|
|
741
|
-
name: "stop",
|
|
742
|
-
handler: async () => {
|
|
743
|
-
if (this.root) {
|
|
744
|
-
this.root.unmount();
|
|
745
|
-
this.log.info("Unmounted root element");
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
class Auth {
|
|
752
|
-
alepha = $inject(Alepha);
|
|
753
|
-
log = $logger();
|
|
754
|
-
client = $inject(HttpClient);
|
|
755
|
-
api = "/api/_oauth/login";
|
|
756
|
-
start = $hook({
|
|
757
|
-
name: "start",
|
|
758
|
-
handler: async () => {
|
|
759
|
-
this.client.on("onError", (err) => {
|
|
760
|
-
if (err.statusCode === 401) {
|
|
761
|
-
this.login();
|
|
762
|
-
}
|
|
687
|
+
this.router.events.on("end", ({ head: head2 }) => {
|
|
688
|
+
this.headProvider.renderHead(this.document, head2);
|
|
763
689
|
});
|
|
764
690
|
}
|
|
765
691
|
});
|
|
766
|
-
login = (provider) => {
|
|
767
|
-
if (this.alepha.isBrowser()) {
|
|
768
|
-
const browser = this.alepha.get(ReactBrowserProvider);
|
|
769
|
-
const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
|
|
770
|
-
window.location.href = `${this.api}?redirect=${redirect}`;
|
|
771
|
-
if (browser.transitioning) {
|
|
772
|
-
throw new RedirectionError(browser.state.pathname);
|
|
773
|
-
}
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
throw new RedirectionError(this.api);
|
|
777
|
-
};
|
|
778
|
-
logout = () => {
|
|
779
|
-
window.location.href = `/api/_oauth/logout?redirect=${encodeURIComponent(window.location.origin)}`;
|
|
780
|
-
};
|
|
781
692
|
}
|
|
782
693
|
|
|
783
694
|
class RouterHookApi {
|
|
@@ -900,14 +811,23 @@ const useRouter = () => {
|
|
|
900
811
|
layer,
|
|
901
812
|
ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
|
|
902
813
|
),
|
|
903
|
-
[
|
|
814
|
+
[layer]
|
|
904
815
|
);
|
|
905
816
|
};
|
|
906
817
|
|
|
907
818
|
const Link = (props) => {
|
|
908
819
|
React.useContext(RouterContext);
|
|
820
|
+
const to = typeof props.to === "string" ? props.to : props.to.options.path;
|
|
821
|
+
if (!to) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const can = typeof props.to === "string" ? void 0 : props.to.options.can;
|
|
825
|
+
if (can && !can()) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
const name = typeof props.to === "string" ? void 0 : props.to.options.name;
|
|
909
829
|
const router = useRouter();
|
|
910
|
-
return /* @__PURE__ */ jsx("a", { ...router.createAnchorProps(
|
|
830
|
+
return /* @__PURE__ */ jsx("a", { ...router.createAnchorProps(to), ...props, children: props.children ?? name });
|
|
911
831
|
};
|
|
912
832
|
|
|
913
833
|
const useInject = (clazz) => {
|
|
@@ -915,7 +835,9 @@ const useInject = (clazz) => {
|
|
|
915
835
|
if (!ctx) {
|
|
916
836
|
throw new Error("useRouter must be used within a <RouterProvider>");
|
|
917
837
|
}
|
|
918
|
-
return ctx.alepha.get(clazz
|
|
838
|
+
return ctx.alepha.get(clazz, {
|
|
839
|
+
skipRegistration: true
|
|
840
|
+
});
|
|
919
841
|
};
|
|
920
842
|
|
|
921
843
|
const useClient = () => {
|
|
@@ -972,13 +894,13 @@ const useRouterEvents = (opts = {}) => {
|
|
|
972
894
|
const onEnd = opts.onEnd;
|
|
973
895
|
const onError = opts.onError;
|
|
974
896
|
if (onBegin) {
|
|
975
|
-
subs.push(ctx.
|
|
897
|
+
subs.push(ctx.events.on("begin", onBegin));
|
|
976
898
|
}
|
|
977
899
|
if (onEnd) {
|
|
978
|
-
subs.push(ctx.
|
|
900
|
+
subs.push(ctx.events.on("end", onEnd));
|
|
979
901
|
}
|
|
980
902
|
if (onError) {
|
|
981
|
-
subs.push(ctx.
|
|
903
|
+
subs.push(ctx.events.on("error", onError));
|
|
982
904
|
}
|
|
983
905
|
return () => {
|
|
984
906
|
for (const sub of subs) {
|
|
@@ -996,7 +918,7 @@ const useRouterState = () => {
|
|
|
996
918
|
}
|
|
997
919
|
const [state, setState] = useState(ctx.state);
|
|
998
920
|
useEffect(
|
|
999
|
-
() => ctx.
|
|
921
|
+
() => ctx.events.on("end", (it) => {
|
|
1000
922
|
setState({ ...it });
|
|
1001
923
|
}),
|
|
1002
924
|
[]
|
|
@@ -1020,7 +942,7 @@ const useActive = (path) => {
|
|
|
1020
942
|
const [isPending, setPending] = useState(false);
|
|
1021
943
|
const isActive = current === href;
|
|
1022
944
|
useEffect(
|
|
1023
|
-
() => ctx.
|
|
945
|
+
() => ctx.events.on("end", ({ pathname }) => setCurrent(pathname)),
|
|
1024
946
|
[]
|
|
1025
947
|
);
|
|
1026
948
|
return {
|
|
@@ -1043,21 +965,4 @@ const useActive = (path) => {
|
|
|
1043
965
|
};
|
|
1044
966
|
};
|
|
1045
967
|
|
|
1046
|
-
|
|
1047
|
-
const ctx = useContext(RouterContext);
|
|
1048
|
-
if (!ctx) {
|
|
1049
|
-
throw new Error("useAuth must be used within a RouterContext");
|
|
1050
|
-
}
|
|
1051
|
-
const args = ctx.args ?? {};
|
|
1052
|
-
return {
|
|
1053
|
-
user: args.user,
|
|
1054
|
-
logout: () => {
|
|
1055
|
-
ctx.alepha.get(Auth).logout();
|
|
1056
|
-
},
|
|
1057
|
-
login: (provider) => {
|
|
1058
|
-
ctx.alepha.get(Auth).login();
|
|
1059
|
-
}
|
|
1060
|
-
};
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
export { $auth as $, Auth as A, Link as L, NestedView as N, PageDescriptorProvider as P, Router as R, $page as a, RouterContext as b, RouterLayerContext as c, RouterHookApi as d, useClient as e, useQueryParams as f, useRouter as g, useRouterEvents as h, useRouterState as i, useActive as j, useAuth as k, ReactBrowserProvider as l, RedirectionError as m, pageDescriptorKey as p, useInject as u };
|
|
968
|
+
export { $page as $, BrowserRouterProvider as B, Link as L, NestedView as N, PageDescriptorProvider as P, RouterContext as R, RouterLayerContext as a, RouterHookApi as b, useClient as c, useQueryParams as d, useRouter as e, useRouterEvents as f, useRouterState as g, useActive as h, RedirectionError as i, isPageRoute as j, ReactBrowserProvider as k, useInject as u };
|