@alepha/react 0.9.3 → 0.9.4
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 +46 -0
- package/dist/index.browser.js +315 -320
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +496 -457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +276 -258
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +274 -256
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +494 -460
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/components/NestedView.tsx +15 -13
- package/src/components/NotFound.tsx +1 -1
- package/src/descriptors/$page.ts +16 -4
- package/src/errors/Redirection.ts +8 -5
- package/src/hooks/useActive.ts +25 -34
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -4
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -31
- package/src/hooks/useRouterEvents.ts +7 -7
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +0 -7
- package/src/index.browser.ts +11 -11
- package/src/index.shared.ts +2 -3
- package/src/index.ts +21 -30
- package/src/providers/ReactBrowserProvider.ts +149 -65
- package/src/providers/ReactBrowserRouterProvider.ts +132 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +84 -112
- package/src/providers/ReactServerProvider.ts +69 -74
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +44 -54
- package/src/contexts/RouterContext.ts +0 -14
- package/src/providers/BrowserRouterProvider.ts +0 -155
- package/src/providers/ReactBrowserRenderer.ts +0 -93
package/dist/index.cjs
CHANGED
|
@@ -25,13 +25,16 @@ const __alepha_core = __toESM(require("@alepha/core"));
|
|
|
25
25
|
const __alepha_server = __toESM(require("@alepha/server"));
|
|
26
26
|
const __alepha_server_cache = __toESM(require("@alepha/server-cache"));
|
|
27
27
|
const __alepha_server_links = __toESM(require("@alepha/server-links"));
|
|
28
|
+
const __alepha_logger = __toESM(require("@alepha/logger"));
|
|
28
29
|
const react = __toESM(require("react"));
|
|
29
30
|
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
|
|
30
|
-
const __alepha_router = __toESM(require("@alepha/router"));
|
|
31
31
|
const node_fs = __toESM(require("node:fs"));
|
|
32
32
|
const node_path = __toESM(require("node:path"));
|
|
33
33
|
const __alepha_server_static = __toESM(require("@alepha/server-static"));
|
|
34
34
|
const react_dom_server = __toESM(require("react-dom/server"));
|
|
35
|
+
const __alepha_datetime = __toESM(require("@alepha/datetime"));
|
|
36
|
+
const react_dom_client = __toESM(require("react-dom/client"));
|
|
37
|
+
const __alepha_router = __toESM(require("@alepha/router"));
|
|
35
38
|
|
|
36
39
|
//#region src/descriptors/$page.ts
|
|
37
40
|
/**
|
|
@@ -57,6 +60,12 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
|
|
|
57
60
|
async render(options) {
|
|
58
61
|
throw new Error("render method is not implemented in this environment");
|
|
59
62
|
}
|
|
63
|
+
match(url) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
pathname(config) {
|
|
67
|
+
return this.options.path || "";
|
|
68
|
+
}
|
|
60
69
|
};
|
|
61
70
|
$page[__alepha_core.KIND] = PageDescriptor;
|
|
62
71
|
|
|
@@ -228,28 +237,54 @@ const ErrorViewerProduction = () => {
|
|
|
228
237
|
});
|
|
229
238
|
};
|
|
230
239
|
|
|
231
|
-
//#endregion
|
|
232
|
-
//#region src/contexts/RouterContext.ts
|
|
233
|
-
const RouterContext = (0, react.createContext)(void 0);
|
|
234
|
-
|
|
235
240
|
//#endregion
|
|
236
241
|
//#region src/contexts/RouterLayerContext.ts
|
|
237
242
|
const RouterLayerContext = (0, react.createContext)(void 0);
|
|
238
243
|
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/errors/Redirection.ts
|
|
246
|
+
/**
|
|
247
|
+
* Used for Redirection during the page loading.
|
|
248
|
+
*
|
|
249
|
+
* Depends on the context, it can be thrown or just returned.
|
|
250
|
+
*/
|
|
251
|
+
var Redirection = class extends Error {
|
|
252
|
+
redirect;
|
|
253
|
+
constructor(redirect) {
|
|
254
|
+
super("Redirection");
|
|
255
|
+
this.redirect = redirect;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
239
259
|
//#endregion
|
|
240
260
|
//#region src/contexts/AlephaContext.ts
|
|
241
261
|
const AlephaContext = (0, react.createContext)(void 0);
|
|
242
262
|
|
|
243
263
|
//#endregion
|
|
244
264
|
//#region src/hooks/useAlepha.ts
|
|
265
|
+
/**
|
|
266
|
+
* Main Alepha hook.
|
|
267
|
+
*
|
|
268
|
+
* It provides access to the Alepha instance within a React component.
|
|
269
|
+
*
|
|
270
|
+
* With Alepha, you can access the core functionalities of the framework:
|
|
271
|
+
*
|
|
272
|
+
* - alepha.state() for state management
|
|
273
|
+
* - alepha.inject() for dependency injection
|
|
274
|
+
* - alepha.emit() for event handling
|
|
275
|
+
* etc...
|
|
276
|
+
*/
|
|
245
277
|
const useAlepha = () => {
|
|
246
278
|
const alepha = (0, react.useContext)(AlephaContext);
|
|
247
|
-
if (!alepha) throw new
|
|
279
|
+
if (!alepha) throw new __alepha_core.AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
|
|
248
280
|
return alepha;
|
|
249
281
|
};
|
|
250
282
|
|
|
251
283
|
//#endregion
|
|
252
284
|
//#region src/hooks/useRouterEvents.ts
|
|
285
|
+
/**
|
|
286
|
+
* Subscribe to various router events.
|
|
287
|
+
*/
|
|
253
288
|
const useRouterEvents = (opts = {}, deps = []) => {
|
|
254
289
|
const alepha = useAlepha();
|
|
255
290
|
(0, react.useEffect)(() => {
|
|
@@ -322,19 +357,21 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
322
357
|
* ```
|
|
323
358
|
*/
|
|
324
359
|
const NestedView = (props) => {
|
|
325
|
-
const app = (0, react.useContext)(RouterContext);
|
|
326
360
|
const layer = (0, react.useContext)(RouterLayerContext);
|
|
327
361
|
const index = layer?.index ?? 0;
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
362
|
+
const alepha = useAlepha();
|
|
363
|
+
const state = alepha.state("react.router.state");
|
|
364
|
+
if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
|
|
365
|
+
const [view, setView] = (0, react.useState)(state.layers[index]?.element);
|
|
366
|
+
useRouterEvents({ onEnd: ({ state: state$1 }) => {
|
|
367
|
+
if (!state$1.layers[index]?.cache) setView(state$1.layers[index]?.element);
|
|
368
|
+
} }, []);
|
|
334
369
|
const element = view ?? props.children ?? null;
|
|
335
370
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
|
|
336
371
|
fallback: (error) => {
|
|
337
|
-
|
|
372
|
+
const result = state.onError(error, state);
|
|
373
|
+
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
374
|
+
return result;
|
|
338
375
|
},
|
|
339
376
|
children: element
|
|
340
377
|
});
|
|
@@ -361,27 +398,17 @@ function NotFoundPage(props) {
|
|
|
361
398
|
fontSize: "1rem",
|
|
362
399
|
marginBottom: "0.5rem"
|
|
363
400
|
},
|
|
364
|
-
children: "This page does not exist"
|
|
401
|
+
children: "404 - This page does not exist"
|
|
365
402
|
})
|
|
366
403
|
});
|
|
367
404
|
}
|
|
368
405
|
|
|
369
406
|
//#endregion
|
|
370
|
-
//#region src/
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.page = page;
|
|
376
|
-
}
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
//#endregion
|
|
380
|
-
//#region src/providers/PageDescriptorProvider.ts
|
|
381
|
-
const envSchema$1 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
|
|
382
|
-
var PageDescriptorProvider = class {
|
|
383
|
-
log = (0, __alepha_core.$logger)();
|
|
384
|
-
env = (0, __alepha_core.$env)(envSchema$1);
|
|
407
|
+
//#region src/providers/ReactPageProvider.ts
|
|
408
|
+
const envSchema$2 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
|
|
409
|
+
var ReactPageProvider = class {
|
|
410
|
+
log = (0, __alepha_logger.$logger)();
|
|
411
|
+
env = (0, __alepha_core.$env)(envSchema$2);
|
|
385
412
|
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
386
413
|
pages = [];
|
|
387
414
|
getPages() {
|
|
@@ -408,22 +435,21 @@ var PageDescriptorProvider = class {
|
|
|
408
435
|
return url.replace(/\/\/+/g, "/") || "/";
|
|
409
436
|
}
|
|
410
437
|
url(name, options = {}) {
|
|
411
|
-
return new URL(this.pathname(name, options), options.
|
|
438
|
+
return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
|
|
412
439
|
}
|
|
413
|
-
root(state
|
|
414
|
-
const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(
|
|
415
|
-
state,
|
|
416
|
-
context
|
|
417
|
-
} }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element)));
|
|
440
|
+
root(state) {
|
|
441
|
+
const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element));
|
|
418
442
|
if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
|
|
419
443
|
return root;
|
|
420
444
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Create a new RouterState based on a given route and request.
|
|
447
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
448
|
+
* It also handles errors and redirects.
|
|
449
|
+
*/
|
|
450
|
+
async createLayers(route, state, previous = []) {
|
|
424
451
|
let context = {};
|
|
425
452
|
const stack = [{ route }];
|
|
426
|
-
request.onError = (error) => this.renderError(error);
|
|
427
453
|
let parent = route.parent;
|
|
428
454
|
while (parent) {
|
|
429
455
|
stack.unshift({ route: parent });
|
|
@@ -435,19 +461,18 @@ var PageDescriptorProvider = class {
|
|
|
435
461
|
const route$1 = it.route;
|
|
436
462
|
const config = {};
|
|
437
463
|
try {
|
|
438
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query,
|
|
464
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
|
|
439
465
|
} catch (e) {
|
|
440
466
|
it.error = e;
|
|
441
467
|
break;
|
|
442
468
|
}
|
|
443
469
|
try {
|
|
444
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params,
|
|
470
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, state.params) : {};
|
|
445
471
|
} catch (e) {
|
|
446
472
|
it.error = e;
|
|
447
473
|
break;
|
|
448
474
|
}
|
|
449
475
|
it.config = { ...config };
|
|
450
|
-
const previous = request.previous;
|
|
451
476
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
452
477
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
453
478
|
const prev = JSON.stringify({
|
|
@@ -473,7 +498,7 @@ var PageDescriptorProvider = class {
|
|
|
473
498
|
if (!route$1.resolve) continue;
|
|
474
499
|
try {
|
|
475
500
|
const props = await route$1.resolve?.({
|
|
476
|
-
...
|
|
501
|
+
...state,
|
|
477
502
|
...config,
|
|
478
503
|
...context
|
|
479
504
|
}) ?? {};
|
|
@@ -483,11 +508,8 @@ var PageDescriptorProvider = class {
|
|
|
483
508
|
...props
|
|
484
509
|
};
|
|
485
510
|
} catch (e) {
|
|
486
|
-
if (e instanceof Redirection) return
|
|
487
|
-
|
|
488
|
-
search
|
|
489
|
-
});
|
|
490
|
-
this.log.error(e);
|
|
511
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
512
|
+
this.log.error("Page resolver has failed", e);
|
|
491
513
|
it.error = e;
|
|
492
514
|
break;
|
|
493
515
|
}
|
|
@@ -503,69 +525,58 @@ var PageDescriptorProvider = class {
|
|
|
503
525
|
const path = acc.replace(/\/+/, "/");
|
|
504
526
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
505
527
|
if (localErrorHandler) {
|
|
506
|
-
const onErrorParent =
|
|
507
|
-
|
|
528
|
+
const onErrorParent = state.onError;
|
|
529
|
+
state.onError = (error, context$1) => {
|
|
508
530
|
const result = localErrorHandler(error, context$1);
|
|
509
531
|
if (result === void 0) return onErrorParent(error, context$1);
|
|
510
532
|
return result;
|
|
511
533
|
};
|
|
512
534
|
}
|
|
513
|
-
if (it.error) try {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
pathname,
|
|
518
|
-
search
|
|
535
|
+
if (!it.error) try {
|
|
536
|
+
const element = await this.createElement(it.route, {
|
|
537
|
+
...props,
|
|
538
|
+
...context
|
|
519
539
|
});
|
|
520
|
-
|
|
521
|
-
|
|
540
|
+
state.layers.push({
|
|
541
|
+
name: it.route.name,
|
|
542
|
+
props,
|
|
543
|
+
part: it.route.path,
|
|
544
|
+
config: it.config,
|
|
545
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
546
|
+
index: i + 1,
|
|
547
|
+
path,
|
|
548
|
+
route: it.route,
|
|
549
|
+
cache: it.cache
|
|
550
|
+
});
|
|
551
|
+
} catch (e) {
|
|
552
|
+
it.error = e;
|
|
553
|
+
}
|
|
554
|
+
if (it.error) try {
|
|
555
|
+
let element = await state.onError(it.error, state);
|
|
556
|
+
if (element === void 0) throw it.error;
|
|
557
|
+
if (element instanceof Redirection) return { redirect: element.redirect };
|
|
558
|
+
if (element === null) element = this.renderError(it.error);
|
|
559
|
+
state.layers.push({
|
|
522
560
|
props,
|
|
523
561
|
error: it.error,
|
|
524
562
|
name: it.route.name,
|
|
525
563
|
part: it.route.path,
|
|
526
564
|
config: it.config,
|
|
527
|
-
element: this.renderView(i + 1, path, element
|
|
565
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
528
566
|
index: i + 1,
|
|
529
567
|
path,
|
|
530
568
|
route: it.route
|
|
531
569
|
});
|
|
532
570
|
break;
|
|
533
571
|
} catch (e) {
|
|
534
|
-
if (e instanceof Redirection) return
|
|
535
|
-
pathname,
|
|
536
|
-
search
|
|
537
|
-
});
|
|
572
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
538
573
|
throw e;
|
|
539
574
|
}
|
|
540
|
-
const element = await this.createElement(it.route, {
|
|
541
|
-
...props,
|
|
542
|
-
...context
|
|
543
|
-
});
|
|
544
|
-
layers.push({
|
|
545
|
-
name: it.route.name,
|
|
546
|
-
props,
|
|
547
|
-
part: it.route.path,
|
|
548
|
-
config: it.config,
|
|
549
|
-
element: this.renderView(i + 1, path, element, it.route),
|
|
550
|
-
index: i + 1,
|
|
551
|
-
path,
|
|
552
|
-
route: it.route,
|
|
553
|
-
cache: it.cache
|
|
554
|
-
});
|
|
555
575
|
}
|
|
556
|
-
return {
|
|
557
|
-
layers,
|
|
558
|
-
pathname,
|
|
559
|
-
search
|
|
560
|
-
};
|
|
576
|
+
return { state };
|
|
561
577
|
}
|
|
562
|
-
createRedirectionLayer(
|
|
563
|
-
return {
|
|
564
|
-
layers: [],
|
|
565
|
-
redirect: typeof href === "string" ? href : this.href(href),
|
|
566
|
-
pathname: context.pathname,
|
|
567
|
-
search: context.search
|
|
568
|
-
};
|
|
578
|
+
createRedirectionLayer(redirect) {
|
|
579
|
+
return { redirect };
|
|
569
580
|
}
|
|
570
581
|
getErrorHandler(route) {
|
|
571
582
|
if (route.errorHandler) return route.errorHandler;
|
|
@@ -691,218 +702,9 @@ const isPageRoute = (it) => {
|
|
|
691
702
|
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
692
703
|
};
|
|
693
704
|
|
|
694
|
-
//#endregion
|
|
695
|
-
//#region src/providers/BrowserRouterProvider.ts
|
|
696
|
-
var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
697
|
-
log = (0, __alepha_core.$logger)();
|
|
698
|
-
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
699
|
-
pageDescriptorProvider = (0, __alepha_core.$inject)(PageDescriptorProvider);
|
|
700
|
-
add(entry) {
|
|
701
|
-
this.pageDescriptorProvider.add(entry);
|
|
702
|
-
}
|
|
703
|
-
configure = (0, __alepha_core.$hook)({
|
|
704
|
-
on: "configure",
|
|
705
|
-
handler: async () => {
|
|
706
|
-
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
707
|
-
path: page.match,
|
|
708
|
-
page
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
async transition(url, options = {}) {
|
|
713
|
-
const { pathname, search } = url;
|
|
714
|
-
const state = {
|
|
715
|
-
pathname,
|
|
716
|
-
search,
|
|
717
|
-
layers: []
|
|
718
|
-
};
|
|
719
|
-
const context = {
|
|
720
|
-
url,
|
|
721
|
-
query: {},
|
|
722
|
-
params: {},
|
|
723
|
-
onError: () => null,
|
|
724
|
-
...options.context ?? {}
|
|
725
|
-
};
|
|
726
|
-
await this.alepha.emit("react:transition:begin", {
|
|
727
|
-
state,
|
|
728
|
-
context
|
|
729
|
-
});
|
|
730
|
-
try {
|
|
731
|
-
const previous = options.previous;
|
|
732
|
-
const { route, params } = this.match(pathname);
|
|
733
|
-
const query = {};
|
|
734
|
-
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
735
|
-
context.query = query;
|
|
736
|
-
context.params = params ?? {};
|
|
737
|
-
context.previous = previous;
|
|
738
|
-
if (isPageRoute(route)) {
|
|
739
|
-
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
740
|
-
if (result.redirect) return {
|
|
741
|
-
redirect: result.redirect,
|
|
742
|
-
state,
|
|
743
|
-
context
|
|
744
|
-
};
|
|
745
|
-
state.layers = result.layers;
|
|
746
|
-
}
|
|
747
|
-
if (state.layers.length === 0) state.layers.push({
|
|
748
|
-
name: "not-found",
|
|
749
|
-
element: (0, react.createElement)(NotFoundPage),
|
|
750
|
-
index: 0,
|
|
751
|
-
path: "/"
|
|
752
|
-
});
|
|
753
|
-
await this.alepha.emit("react:transition:success", {
|
|
754
|
-
state,
|
|
755
|
-
context
|
|
756
|
-
});
|
|
757
|
-
} catch (e) {
|
|
758
|
-
this.log.error(e);
|
|
759
|
-
state.layers = [{
|
|
760
|
-
name: "error",
|
|
761
|
-
element: this.pageDescriptorProvider.renderError(e),
|
|
762
|
-
index: 0,
|
|
763
|
-
path: "/"
|
|
764
|
-
}];
|
|
765
|
-
await this.alepha.emit("react:transition:error", {
|
|
766
|
-
error: e,
|
|
767
|
-
state,
|
|
768
|
-
context
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
if (options.state) {
|
|
772
|
-
options.state.layers = state.layers;
|
|
773
|
-
options.state.pathname = state.pathname;
|
|
774
|
-
options.state.search = state.search;
|
|
775
|
-
}
|
|
776
|
-
if (options.previous) for (let i = 0; i < options.previous.length; i++) {
|
|
777
|
-
const layer = options.previous[i];
|
|
778
|
-
if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
|
|
779
|
-
}
|
|
780
|
-
await this.alepha.emit("react:transition:end", {
|
|
781
|
-
state: options.state,
|
|
782
|
-
context
|
|
783
|
-
});
|
|
784
|
-
return {
|
|
785
|
-
context,
|
|
786
|
-
state
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
root(state, context) {
|
|
790
|
-
return this.pageDescriptorProvider.root(state, context);
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
//#endregion
|
|
795
|
-
//#region src/providers/ReactBrowserProvider.ts
|
|
796
|
-
var ReactBrowserProvider = class {
|
|
797
|
-
log = (0, __alepha_core.$logger)();
|
|
798
|
-
client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
|
|
799
|
-
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
800
|
-
router = (0, __alepha_core.$inject)(BrowserRouterProvider);
|
|
801
|
-
root;
|
|
802
|
-
transitioning;
|
|
803
|
-
state = {
|
|
804
|
-
layers: [],
|
|
805
|
-
pathname: "",
|
|
806
|
-
search: ""
|
|
807
|
-
};
|
|
808
|
-
get document() {
|
|
809
|
-
return window.document;
|
|
810
|
-
}
|
|
811
|
-
get history() {
|
|
812
|
-
return window.history;
|
|
813
|
-
}
|
|
814
|
-
get location() {
|
|
815
|
-
return window.location;
|
|
816
|
-
}
|
|
817
|
-
get url() {
|
|
818
|
-
let url = this.location.pathname + this.location.search;
|
|
819
|
-
return url;
|
|
820
|
-
}
|
|
821
|
-
pushState(url, replace) {
|
|
822
|
-
let path = url;
|
|
823
|
-
if (replace) this.history.replaceState({}, "", path);
|
|
824
|
-
else this.history.pushState({}, "", path);
|
|
825
|
-
}
|
|
826
|
-
async invalidate(props) {
|
|
827
|
-
const previous = [];
|
|
828
|
-
if (props) {
|
|
829
|
-
const [key] = Object.keys(props);
|
|
830
|
-
const value = props[key];
|
|
831
|
-
for (const layer of this.state.layers) {
|
|
832
|
-
if (layer.props?.[key]) {
|
|
833
|
-
previous.push({
|
|
834
|
-
...layer,
|
|
835
|
-
props: {
|
|
836
|
-
...layer.props,
|
|
837
|
-
[key]: value
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
break;
|
|
841
|
-
}
|
|
842
|
-
previous.push(layer);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
await this.render({ previous });
|
|
846
|
-
}
|
|
847
|
-
async go(url, options = {}) {
|
|
848
|
-
const result = await this.render({ url });
|
|
849
|
-
if (result.context.url.pathname + result.context.url.search !== url) {
|
|
850
|
-
this.pushState(result.context.url.pathname + result.context.url.search);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
this.pushState(url, options.replace);
|
|
854
|
-
}
|
|
855
|
-
async render(options = {}) {
|
|
856
|
-
const previous = options.previous ?? this.state.layers;
|
|
857
|
-
const url = options.url ?? this.url;
|
|
858
|
-
this.transitioning = { to: url };
|
|
859
|
-
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
860
|
-
previous,
|
|
861
|
-
state: this.state
|
|
862
|
-
});
|
|
863
|
-
if (result.redirect) return await this.render({ url: result.redirect });
|
|
864
|
-
this.transitioning = void 0;
|
|
865
|
-
return result;
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Get embedded layers from the server.
|
|
869
|
-
*/
|
|
870
|
-
getHydrationState() {
|
|
871
|
-
try {
|
|
872
|
-
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
873
|
-
} catch (error) {
|
|
874
|
-
console.error(error);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
ready = (0, __alepha_core.$hook)({
|
|
878
|
-
on: "ready",
|
|
879
|
-
handler: async () => {
|
|
880
|
-
const hydration = this.getHydrationState();
|
|
881
|
-
const previous = hydration?.layers ?? [];
|
|
882
|
-
if (hydration) {
|
|
883
|
-
for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
|
|
884
|
-
}
|
|
885
|
-
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
|
|
886
|
-
...link,
|
|
887
|
-
prefix: hydration.links.prefix
|
|
888
|
-
});
|
|
889
|
-
const { context } = await this.render({ previous });
|
|
890
|
-
await this.alepha.emit("react:browser:render", {
|
|
891
|
-
state: this.state,
|
|
892
|
-
context,
|
|
893
|
-
hydration
|
|
894
|
-
});
|
|
895
|
-
window.addEventListener("popstate", () => {
|
|
896
|
-
if (this.state.pathname === this.url) return;
|
|
897
|
-
this.render();
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
});
|
|
901
|
-
};
|
|
902
|
-
|
|
903
705
|
//#endregion
|
|
904
706
|
//#region src/providers/ReactServerProvider.ts
|
|
905
|
-
const envSchema = __alepha_core.t.object({
|
|
707
|
+
const envSchema$1 = __alepha_core.t.object({
|
|
906
708
|
REACT_SERVER_DIST: __alepha_core.t.string({ default: "public" }),
|
|
907
709
|
REACT_SERVER_PREFIX: __alepha_core.t.string({ default: "" }),
|
|
908
710
|
REACT_SSR_ENABLED: __alepha_core.t.optional(__alepha_core.t.boolean()),
|
|
@@ -910,13 +712,13 @@ const envSchema = __alepha_core.t.object({
|
|
|
910
712
|
REACT_SERVER_TEMPLATE: __alepha_core.t.optional(__alepha_core.t.string({ size: "rich" }))
|
|
911
713
|
});
|
|
912
714
|
var ReactServerProvider = class {
|
|
913
|
-
log = (0,
|
|
715
|
+
log = (0, __alepha_logger.$logger)();
|
|
914
716
|
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
915
|
-
|
|
717
|
+
pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
|
|
916
718
|
serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
|
|
917
719
|
serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
|
|
918
720
|
serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
|
|
919
|
-
env = (0, __alepha_core.$env)(envSchema);
|
|
721
|
+
env = (0, __alepha_core.$env)(envSchema$1);
|
|
920
722
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
921
723
|
onConfigure = (0, __alepha_core.$hook)({
|
|
922
724
|
on: "configure",
|
|
@@ -963,7 +765,7 @@ var ReactServerProvider = class {
|
|
|
963
765
|
return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
964
766
|
}
|
|
965
767
|
async registerPages(templateLoader) {
|
|
966
|
-
for (const page of this.
|
|
768
|
+
for (const page of this.pageApi.getPages()) {
|
|
967
769
|
if (page.children?.length) continue;
|
|
968
770
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
969
771
|
this.serverRouterProvider.createRoute({
|
|
@@ -997,25 +799,30 @@ var ReactServerProvider = class {
|
|
|
997
799
|
*/
|
|
998
800
|
createRenderFunction(name, withIndex = false) {
|
|
999
801
|
return async (options = {}) => {
|
|
1000
|
-
const page = this.
|
|
1001
|
-
const url = new URL(this.
|
|
1002
|
-
const
|
|
802
|
+
const page = this.pageApi.page(name);
|
|
803
|
+
const url = new URL(this.pageApi.url(name, options));
|
|
804
|
+
const entry = {
|
|
1003
805
|
url,
|
|
1004
806
|
params: options.params ?? {},
|
|
1005
807
|
query: options.query ?? {},
|
|
1006
|
-
|
|
1007
|
-
|
|
808
|
+
onError: () => null,
|
|
809
|
+
layers: []
|
|
1008
810
|
};
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
811
|
+
const state = entry;
|
|
812
|
+
this.log.trace("Rendering", { url });
|
|
813
|
+
await this.alepha.emit("react:server:render:begin", { state });
|
|
814
|
+
const { redirect } = await this.pageApi.createLayers(page, state);
|
|
815
|
+
if (redirect) throw new __alepha_core.AlephaError("Redirection is not supported in this context");
|
|
816
|
+
if (!withIndex && !options.html) {
|
|
817
|
+
this.alepha.state("react.router.state", state);
|
|
818
|
+
return {
|
|
819
|
+
state,
|
|
820
|
+
html: (0, react_dom_server.renderToString)(this.pageApi.root(state))
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const html = this.renderToHtml(this.template ?? "", state, options.hydration);
|
|
1016
824
|
if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
|
|
1017
825
|
const result = {
|
|
1018
|
-
context,
|
|
1019
826
|
state,
|
|
1020
827
|
html
|
|
1021
828
|
};
|
|
@@ -1023,89 +830,82 @@ var ReactServerProvider = class {
|
|
|
1023
830
|
return result;
|
|
1024
831
|
};
|
|
1025
832
|
}
|
|
1026
|
-
createHandler(
|
|
833
|
+
createHandler(route, templateLoader) {
|
|
1027
834
|
return async (serverRequest) => {
|
|
1028
835
|
const { url, reply, query, params } = serverRequest;
|
|
1029
836
|
const template = await templateLoader();
|
|
1030
837
|
if (!template) throw new Error("Template not found");
|
|
1031
|
-
this.log.trace("Rendering page", { name:
|
|
1032
|
-
const
|
|
838
|
+
this.log.trace("Rendering page", { name: route.name });
|
|
839
|
+
const entry = {
|
|
1033
840
|
url,
|
|
1034
841
|
params,
|
|
1035
842
|
query,
|
|
1036
|
-
|
|
1037
|
-
|
|
843
|
+
onError: () => null,
|
|
844
|
+
layers: []
|
|
1038
845
|
};
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}));
|
|
1046
|
-
this.alepha.context.set("links", context.links);
|
|
1047
|
-
}
|
|
1048
|
-
let target = page;
|
|
846
|
+
const state = entry;
|
|
847
|
+
if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(__alepha_server_links.ServerLinksProvider).getUserApiLinks({
|
|
848
|
+
user: serverRequest.user,
|
|
849
|
+
authorization: serverRequest.headers.authorization
|
|
850
|
+
}));
|
|
851
|
+
let target = route;
|
|
1049
852
|
while (target) {
|
|
1050
|
-
if (
|
|
853
|
+
if (route.can && !route.can()) {
|
|
1051
854
|
reply.status = 403;
|
|
1052
855
|
reply.headers["content-type"] = "text/plain";
|
|
1053
856
|
return "Forbidden";
|
|
1054
857
|
}
|
|
1055
858
|
target = target.parent;
|
|
1056
859
|
}
|
|
1057
|
-
await this.alepha.emit("react:transition:begin", {
|
|
1058
|
-
request: serverRequest,
|
|
1059
|
-
context
|
|
1060
|
-
});
|
|
1061
860
|
await this.alepha.emit("react:server:render:begin", {
|
|
1062
861
|
request: serverRequest,
|
|
1063
|
-
|
|
862
|
+
state
|
|
1064
863
|
});
|
|
1065
864
|
this.serverTimingProvider.beginTiming("createLayers");
|
|
1066
|
-
const
|
|
865
|
+
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
1067
866
|
this.serverTimingProvider.endTiming("createLayers");
|
|
1068
|
-
if (
|
|
867
|
+
if (redirect) return reply.redirect(redirect);
|
|
1069
868
|
reply.headers["content-type"] = "text/html";
|
|
1070
869
|
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
1071
870
|
reply.headers.pragma = "no-cache";
|
|
1072
871
|
reply.headers.expires = "0";
|
|
1073
|
-
|
|
1074
|
-
const html = this.renderToHtml(template, state, context);
|
|
872
|
+
const html = this.renderToHtml(template, state);
|
|
1075
873
|
if (html instanceof Redirection) {
|
|
1076
|
-
reply.redirect(typeof html.
|
|
874
|
+
reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
|
|
1077
875
|
return;
|
|
1078
876
|
}
|
|
1079
877
|
const event = {
|
|
1080
878
|
request: serverRequest,
|
|
1081
|
-
context,
|
|
1082
879
|
state,
|
|
1083
880
|
html
|
|
1084
881
|
};
|
|
1085
882
|
await this.alepha.emit("react:server:render:end", event);
|
|
1086
|
-
|
|
1087
|
-
this.log.trace("Page rendered", { name:
|
|
883
|
+
route.onServerResponse?.(serverRequest);
|
|
884
|
+
this.log.trace("Page rendered", { name: route.name });
|
|
1088
885
|
return event.html;
|
|
1089
886
|
};
|
|
1090
887
|
}
|
|
1091
|
-
renderToHtml(template, state,
|
|
1092
|
-
const element = this.
|
|
888
|
+
renderToHtml(template, state, hydration = true) {
|
|
889
|
+
const element = this.pageApi.root(state);
|
|
890
|
+
this.alepha.state("react.router.state", state);
|
|
1093
891
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
1094
892
|
let app = "";
|
|
1095
893
|
try {
|
|
1096
894
|
app = (0, react_dom_server.renderToString)(element);
|
|
1097
895
|
} catch (error) {
|
|
1098
|
-
this.log.error("
|
|
1099
|
-
const element$1 =
|
|
896
|
+
this.log.error("renderToString has failed, fallback to error handler", error);
|
|
897
|
+
const element$1 = state.onError(error, state);
|
|
1100
898
|
if (element$1 instanceof Redirection) return element$1;
|
|
1101
899
|
app = (0, react_dom_server.renderToString)(element$1);
|
|
900
|
+
this.log.debug("Error handled successfully with fallback");
|
|
1102
901
|
}
|
|
1103
902
|
this.serverTimingProvider.endTiming("renderToString");
|
|
1104
903
|
const response = { html: template };
|
|
1105
904
|
if (hydration) {
|
|
1106
|
-
const { request, context
|
|
905
|
+
const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
|
|
1107
906
|
const hydrationData = {
|
|
1108
|
-
...
|
|
907
|
+
...store,
|
|
908
|
+
"react.router.state": void 0,
|
|
1109
909
|
layers: state.layers.map((it) => ({
|
|
1110
910
|
...it,
|
|
1111
911
|
error: it.error ? {
|
|
@@ -1120,7 +920,7 @@ var ReactServerProvider = class {
|
|
|
1120
920
|
route: void 0
|
|
1121
921
|
}))
|
|
1122
922
|
};
|
|
1123
|
-
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}
|
|
923
|
+
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
|
|
1124
924
|
this.fillTemplate(response, app, script);
|
|
1125
925
|
}
|
|
1126
926
|
return response.html;
|
|
@@ -1141,27 +941,260 @@ var ReactServerProvider = class {
|
|
|
1141
941
|
};
|
|
1142
942
|
|
|
1143
943
|
//#endregion
|
|
1144
|
-
//#region src/
|
|
1145
|
-
var
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
this.
|
|
1151
|
-
|
|
1152
|
-
|
|
944
|
+
//#region src/providers/ReactBrowserRouterProvider.ts
|
|
945
|
+
var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
946
|
+
log = (0, __alepha_logger.$logger)();
|
|
947
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
948
|
+
pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
|
|
949
|
+
add(entry) {
|
|
950
|
+
this.pageApi.add(entry);
|
|
951
|
+
}
|
|
952
|
+
configure = (0, __alepha_core.$hook)({
|
|
953
|
+
on: "configure",
|
|
954
|
+
handler: async () => {
|
|
955
|
+
for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
|
|
956
|
+
path: page.match,
|
|
957
|
+
page
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
async transition(url, previous = []) {
|
|
962
|
+
const { pathname, search } = url;
|
|
963
|
+
const entry = {
|
|
964
|
+
url,
|
|
965
|
+
query: {},
|
|
966
|
+
params: {},
|
|
967
|
+
layers: [],
|
|
968
|
+
onError: () => null
|
|
969
|
+
};
|
|
970
|
+
const state = entry;
|
|
971
|
+
await this.alepha.emit("react:transition:begin", { state });
|
|
972
|
+
try {
|
|
973
|
+
const { route, params } = this.match(pathname);
|
|
974
|
+
const query = {};
|
|
975
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
976
|
+
state.query = query;
|
|
977
|
+
state.params = params ?? {};
|
|
978
|
+
if (isPageRoute(route)) {
|
|
979
|
+
const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
|
|
980
|
+
if (redirect) return redirect;
|
|
981
|
+
}
|
|
982
|
+
if (state.layers.length === 0) state.layers.push({
|
|
983
|
+
name: "not-found",
|
|
984
|
+
element: (0, react.createElement)(NotFoundPage),
|
|
985
|
+
index: 0,
|
|
986
|
+
path: "/"
|
|
987
|
+
});
|
|
988
|
+
await this.alepha.emit("react:transition:success", { state });
|
|
989
|
+
} catch (e) {
|
|
990
|
+
this.log.error("Transition has failed", e);
|
|
991
|
+
state.layers = [{
|
|
992
|
+
name: "error",
|
|
993
|
+
element: this.pageApi.renderError(e),
|
|
994
|
+
index: 0,
|
|
995
|
+
path: "/"
|
|
996
|
+
}];
|
|
997
|
+
await this.alepha.emit("react:transition:error", {
|
|
998
|
+
error: e,
|
|
999
|
+
state
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
if (previous) for (let i = 0; i < previous.length; i++) {
|
|
1003
|
+
const layer = previous[i];
|
|
1004
|
+
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
1005
|
+
}
|
|
1006
|
+
await this.alepha.emit("react:transition:end", { state });
|
|
1007
|
+
this.alepha.state("react.router.state", state);
|
|
1008
|
+
}
|
|
1009
|
+
root(state) {
|
|
1010
|
+
return this.pageApi.root(state);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/providers/ReactBrowserProvider.ts
|
|
1016
|
+
const envSchema = __alepha_core.t.object({ REACT_ROOT_ID: __alepha_core.t.string({ default: "root" }) });
|
|
1017
|
+
var ReactBrowserProvider = class {
|
|
1018
|
+
env = (0, __alepha_core.$env)(envSchema);
|
|
1019
|
+
log = (0, __alepha_logger.$logger)();
|
|
1020
|
+
client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
|
|
1021
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
1022
|
+
router = (0, __alepha_core.$inject)(ReactBrowserRouterProvider);
|
|
1023
|
+
dateTimeProvider = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider);
|
|
1024
|
+
root;
|
|
1025
|
+
options = { scrollRestoration: "top" };
|
|
1026
|
+
getRootElement() {
|
|
1027
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
1028
|
+
if (root) return root;
|
|
1029
|
+
const div = this.document.createElement("div");
|
|
1030
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
1031
|
+
this.document.body.prepend(div);
|
|
1032
|
+
return div;
|
|
1033
|
+
}
|
|
1034
|
+
transitioning;
|
|
1035
|
+
get state() {
|
|
1036
|
+
return this.alepha.state("react.router.state");
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Accessor for Document DOM API.
|
|
1040
|
+
*/
|
|
1041
|
+
get document() {
|
|
1042
|
+
return window.document;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Accessor for History DOM API.
|
|
1046
|
+
*/
|
|
1047
|
+
get history() {
|
|
1048
|
+
return window.history;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Accessor for Location DOM API.
|
|
1052
|
+
*/
|
|
1053
|
+
get location() {
|
|
1054
|
+
return window.location;
|
|
1055
|
+
}
|
|
1056
|
+
get base() {
|
|
1057
|
+
const base = {}.env?.BASE_URL;
|
|
1058
|
+
if (!base || base === "/") return "";
|
|
1059
|
+
return base;
|
|
1060
|
+
}
|
|
1061
|
+
get url() {
|
|
1062
|
+
const url = this.location.pathname + this.location.search;
|
|
1063
|
+
if (this.base) return url.replace(this.base, "");
|
|
1064
|
+
return url;
|
|
1065
|
+
}
|
|
1066
|
+
pushState(path, replace) {
|
|
1067
|
+
const url = this.base + path;
|
|
1068
|
+
if (replace) this.history.replaceState({}, "", url);
|
|
1069
|
+
else this.history.pushState({}, "", url);
|
|
1070
|
+
}
|
|
1071
|
+
async invalidate(props) {
|
|
1072
|
+
const previous = [];
|
|
1073
|
+
this.log.trace("Invalidating layers");
|
|
1074
|
+
if (props) {
|
|
1075
|
+
const [key] = Object.keys(props);
|
|
1076
|
+
const value = props[key];
|
|
1077
|
+
for (const layer of this.state.layers) {
|
|
1078
|
+
if (layer.props?.[key]) {
|
|
1079
|
+
previous.push({
|
|
1080
|
+
...layer,
|
|
1081
|
+
props: {
|
|
1082
|
+
...layer.props,
|
|
1083
|
+
[key]: value
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
previous.push(layer);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
await this.render({ previous });
|
|
1092
|
+
}
|
|
1093
|
+
async go(url, options = {}) {
|
|
1094
|
+
this.log.trace(`Going to ${url}`, {
|
|
1095
|
+
url,
|
|
1096
|
+
options
|
|
1097
|
+
});
|
|
1098
|
+
await this.render({
|
|
1099
|
+
url,
|
|
1100
|
+
previous: options.force ? [] : this.state.layers
|
|
1101
|
+
});
|
|
1102
|
+
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
1103
|
+
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
this.pushState(url, options.replace);
|
|
1107
|
+
}
|
|
1108
|
+
async render(options = {}) {
|
|
1109
|
+
const previous = options.previous ?? this.state.layers;
|
|
1110
|
+
const url = options.url ?? this.url;
|
|
1111
|
+
const start = this.dateTimeProvider.now();
|
|
1112
|
+
this.transitioning = {
|
|
1113
|
+
to: url,
|
|
1114
|
+
from: this.state?.url.pathname
|
|
1115
|
+
};
|
|
1116
|
+
this.log.debug("Transitioning...", { to: url });
|
|
1117
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
|
|
1118
|
+
if (redirect) {
|
|
1119
|
+
this.log.info("Redirecting to", { redirect });
|
|
1120
|
+
return await this.render({ url: redirect });
|
|
1121
|
+
}
|
|
1122
|
+
const ms = this.dateTimeProvider.now().diff(start);
|
|
1123
|
+
this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
|
|
1124
|
+
this.transitioning = void 0;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Get embedded layers from the server.
|
|
1128
|
+
*/
|
|
1129
|
+
getHydrationState() {
|
|
1130
|
+
try {
|
|
1131
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
console.error(error);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
onTransitionEnd = (0, __alepha_core.$hook)({
|
|
1137
|
+
on: "react:transition:end",
|
|
1138
|
+
handler: () => {
|
|
1139
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
|
|
1140
|
+
this.log.trace("Restoring scroll position to top");
|
|
1141
|
+
window.scrollTo(0, 0);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
ready = (0, __alepha_core.$hook)({
|
|
1146
|
+
on: "ready",
|
|
1147
|
+
handler: async () => {
|
|
1148
|
+
const hydration = this.getHydrationState();
|
|
1149
|
+
const previous = hydration?.layers ?? [];
|
|
1150
|
+
if (hydration) {
|
|
1151
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
|
|
1152
|
+
}
|
|
1153
|
+
await this.render({ previous });
|
|
1154
|
+
const element = this.router.root(this.state);
|
|
1155
|
+
if (hydration?.layers) {
|
|
1156
|
+
this.root = (0, react_dom_client.hydrateRoot)(this.getRootElement(), element);
|
|
1157
|
+
this.log.info("Hydrated root element");
|
|
1158
|
+
} else {
|
|
1159
|
+
this.root ??= (0, react_dom_client.createRoot)(this.getRootElement());
|
|
1160
|
+
this.root.render(element);
|
|
1161
|
+
this.log.info("Created root element");
|
|
1162
|
+
}
|
|
1163
|
+
window.addEventListener("popstate", () => {
|
|
1164
|
+
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1165
|
+
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
1166
|
+
this.render();
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
//#endregion
|
|
1173
|
+
//#region src/services/ReactRouter.ts
|
|
1174
|
+
var ReactRouter = class {
|
|
1175
|
+
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
1176
|
+
pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
|
|
1177
|
+
get state() {
|
|
1178
|
+
return this.alepha.state("react.router.state");
|
|
1179
|
+
}
|
|
1180
|
+
get pages() {
|
|
1181
|
+
return this.pageApi.getPages();
|
|
1182
|
+
}
|
|
1183
|
+
get browser() {
|
|
1184
|
+
if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
|
|
1185
|
+
return void 0;
|
|
1153
1186
|
}
|
|
1154
1187
|
path(name, config = {}) {
|
|
1155
1188
|
return this.pageApi.pathname(name, {
|
|
1156
1189
|
params: {
|
|
1157
|
-
...this.
|
|
1190
|
+
...this.state.params,
|
|
1158
1191
|
...config.params
|
|
1159
1192
|
},
|
|
1160
1193
|
query: config.query
|
|
1161
1194
|
});
|
|
1162
1195
|
}
|
|
1163
1196
|
getURL() {
|
|
1164
|
-
if (!this.browser) return this.
|
|
1197
|
+
if (!this.browser) return this.state.url;
|
|
1165
1198
|
return new URL(this.location.href);
|
|
1166
1199
|
}
|
|
1167
1200
|
get location() {
|
|
@@ -1172,11 +1205,11 @@ var RouterHookApi = class {
|
|
|
1172
1205
|
return this.state;
|
|
1173
1206
|
}
|
|
1174
1207
|
get pathname() {
|
|
1175
|
-
return this.state.pathname;
|
|
1208
|
+
return this.state.url.pathname;
|
|
1176
1209
|
}
|
|
1177
1210
|
get query() {
|
|
1178
1211
|
const query = {};
|
|
1179
|
-
for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
|
|
1212
|
+
for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
|
|
1180
1213
|
return query;
|
|
1181
1214
|
}
|
|
1182
1215
|
async back() {
|
|
@@ -1188,17 +1221,6 @@ var RouterHookApi = class {
|
|
|
1188
1221
|
async invalidate(props) {
|
|
1189
1222
|
await this.browser?.invalidate(props);
|
|
1190
1223
|
}
|
|
1191
|
-
/**
|
|
1192
|
-
* Create a valid href for the given pathname.
|
|
1193
|
-
*
|
|
1194
|
-
* @param pathname
|
|
1195
|
-
* @param layer
|
|
1196
|
-
*/
|
|
1197
|
-
createHref(pathname, layer = this.layer, options = {}) {
|
|
1198
|
-
if (typeof pathname === "object") pathname = pathname.options.path ?? "";
|
|
1199
|
-
if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
|
|
1200
|
-
return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
1201
|
-
}
|
|
1202
1224
|
async go(path, options) {
|
|
1203
1225
|
for (const page of this.pages) if (page.name === path) {
|
|
1204
1226
|
await this.browser?.go(this.path(path, options), options);
|
|
@@ -1213,7 +1235,7 @@ var RouterHookApi = class {
|
|
|
1213
1235
|
break;
|
|
1214
1236
|
}
|
|
1215
1237
|
return {
|
|
1216
|
-
href,
|
|
1238
|
+
href: this.base(href),
|
|
1217
1239
|
onClick: (ev) => {
|
|
1218
1240
|
ev.stopPropagation();
|
|
1219
1241
|
ev.preventDefault();
|
|
@@ -1221,6 +1243,11 @@ var RouterHookApi = class {
|
|
|
1221
1243
|
}
|
|
1222
1244
|
};
|
|
1223
1245
|
}
|
|
1246
|
+
base(path) {
|
|
1247
|
+
const base = {}.env?.BASE_URL;
|
|
1248
|
+
if (!base || base === "/") return path;
|
|
1249
|
+
return base + path;
|
|
1250
|
+
}
|
|
1224
1251
|
/**
|
|
1225
1252
|
* Set query params.
|
|
1226
1253
|
*
|
|
@@ -1236,17 +1263,35 @@ var RouterHookApi = class {
|
|
|
1236
1263
|
}
|
|
1237
1264
|
};
|
|
1238
1265
|
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/hooks/useInject.ts
|
|
1268
|
+
/**
|
|
1269
|
+
* Hook to inject a service instance.
|
|
1270
|
+
* It's a wrapper of `useAlepha().inject(service)` with a memoization.
|
|
1271
|
+
*/
|
|
1272
|
+
const useInject = (service) => {
|
|
1273
|
+
const alepha = useAlepha();
|
|
1274
|
+
return (0, react.useMemo)(() => alepha.inject(service), []);
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1239
1277
|
//#endregion
|
|
1240
1278
|
//#region src/hooks/useRouter.ts
|
|
1279
|
+
/**
|
|
1280
|
+
* Use this hook to access the React Router instance.
|
|
1281
|
+
*
|
|
1282
|
+
* You can add a type parameter to specify the type of your application.
|
|
1283
|
+
* This will allow you to use the router in a typesafe way.
|
|
1284
|
+
*
|
|
1285
|
+
* @example
|
|
1286
|
+
* class App {
|
|
1287
|
+
* home = $page();
|
|
1288
|
+
* }
|
|
1289
|
+
*
|
|
1290
|
+
* const router = useRouter<App>();
|
|
1291
|
+
* router.go("home"); // typesafe
|
|
1292
|
+
*/
|
|
1241
1293
|
const useRouter = () => {
|
|
1242
|
-
|
|
1243
|
-
const ctx = (0, react.useContext)(RouterContext);
|
|
1244
|
-
const layer = (0, react.useContext)(RouterLayerContext);
|
|
1245
|
-
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1246
|
-
const pages = (0, react.useMemo)(() => {
|
|
1247
|
-
return alepha.inject(PageDescriptorProvider).getPages();
|
|
1248
|
-
}, []);
|
|
1249
|
-
return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
|
|
1294
|
+
return useInject(ReactRouter);
|
|
1250
1295
|
};
|
|
1251
1296
|
|
|
1252
1297
|
//#endregion
|
|
@@ -1262,46 +1307,6 @@ const Link = (props) => {
|
|
|
1262
1307
|
};
|
|
1263
1308
|
var Link_default = Link;
|
|
1264
1309
|
|
|
1265
|
-
//#endregion
|
|
1266
|
-
//#region src/hooks/useActive.ts
|
|
1267
|
-
const useActive = (path) => {
|
|
1268
|
-
const router = useRouter();
|
|
1269
|
-
const ctx = (0, react.useContext)(RouterContext);
|
|
1270
|
-
const layer = (0, react.useContext)(RouterLayerContext);
|
|
1271
|
-
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1272
|
-
const [current, setCurrent] = (0, react.useState)(ctx.state.pathname);
|
|
1273
|
-
const href = (0, react.useMemo)(() => router.createHref(path ?? "", layer), [path, layer]);
|
|
1274
|
-
const [isPending, setPending] = (0, react.useState)(false);
|
|
1275
|
-
const isActive = current === href || current === `${href}/` || `${current}/` === href;
|
|
1276
|
-
useRouterEvents({ onEnd: ({ state }) => {
|
|
1277
|
-
path && setCurrent(state.pathname);
|
|
1278
|
-
} }, [path]);
|
|
1279
|
-
return {
|
|
1280
|
-
isPending,
|
|
1281
|
-
isActive,
|
|
1282
|
-
anchorProps: {
|
|
1283
|
-
href,
|
|
1284
|
-
onClick: (ev) => {
|
|
1285
|
-
ev?.stopPropagation();
|
|
1286
|
-
ev?.preventDefault();
|
|
1287
|
-
if (isActive) return;
|
|
1288
|
-
if (isPending) return;
|
|
1289
|
-
setPending(true);
|
|
1290
|
-
router.go(href).then(() => {
|
|
1291
|
-
setPending(false);
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
};
|
|
1296
|
-
};
|
|
1297
|
-
|
|
1298
|
-
//#endregion
|
|
1299
|
-
//#region src/hooks/useInject.ts
|
|
1300
|
-
const useInject = (service) => {
|
|
1301
|
-
const alepha = useAlepha();
|
|
1302
|
-
return (0, react.useMemo)(() => alepha.inject(service), []);
|
|
1303
|
-
};
|
|
1304
|
-
|
|
1305
1310
|
//#endregion
|
|
1306
1311
|
//#region src/hooks/useStore.ts
|
|
1307
1312
|
/**
|
|
@@ -1319,24 +1324,70 @@ const useStore = (key, defaultValue) => {
|
|
|
1319
1324
|
if (ev.key === key) setState(ev.value);
|
|
1320
1325
|
});
|
|
1321
1326
|
}, []);
|
|
1322
|
-
if (!alepha.isBrowser()) {
|
|
1323
|
-
const value = alepha.context.get(key);
|
|
1324
|
-
if (value !== null) return [value, (_) => {}];
|
|
1325
|
-
}
|
|
1326
1327
|
return [state, (value) => {
|
|
1327
1328
|
alepha.state(key, value);
|
|
1328
1329
|
}];
|
|
1329
1330
|
};
|
|
1330
1331
|
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/hooks/useRouterState.ts
|
|
1334
|
+
const useRouterState = () => {
|
|
1335
|
+
const [state] = useStore("react.router.state");
|
|
1336
|
+
if (!state) throw new __alepha_core.AlephaError("Missing react router state");
|
|
1337
|
+
return state;
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/hooks/useActive.ts
|
|
1342
|
+
const useActive = (args) => {
|
|
1343
|
+
const router = useRouter();
|
|
1344
|
+
const [isPending, setPending] = (0, react.useState)(false);
|
|
1345
|
+
const state = useRouterState();
|
|
1346
|
+
const current = state.url.pathname;
|
|
1347
|
+
const options = typeof args === "string" ? { href: args } : {
|
|
1348
|
+
...args,
|
|
1349
|
+
href: args.href
|
|
1350
|
+
};
|
|
1351
|
+
const href = options.href;
|
|
1352
|
+
let isActive = current === href || current === `${href}/` || `${current}/` === href;
|
|
1353
|
+
if (options.startWith && !isActive) isActive = current.startsWith(href);
|
|
1354
|
+
return {
|
|
1355
|
+
isPending,
|
|
1356
|
+
isActive,
|
|
1357
|
+
anchorProps: {
|
|
1358
|
+
href: router.base(href),
|
|
1359
|
+
onClick: async (ev) => {
|
|
1360
|
+
ev?.stopPropagation();
|
|
1361
|
+
ev?.preventDefault();
|
|
1362
|
+
if (isActive) return;
|
|
1363
|
+
if (isPending) return;
|
|
1364
|
+
setPending(true);
|
|
1365
|
+
try {
|
|
1366
|
+
await router.go(href);
|
|
1367
|
+
} finally {
|
|
1368
|
+
setPending(false);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1331
1375
|
//#endregion
|
|
1332
1376
|
//#region src/hooks/useClient.ts
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1377
|
+
/**
|
|
1378
|
+
* Hook to get a virtual client for the specified scope.
|
|
1379
|
+
*
|
|
1380
|
+
* It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
|
|
1381
|
+
*/
|
|
1382
|
+
const useClient = (scope) => {
|
|
1383
|
+
return useInject(__alepha_server_links.LinkProvider).client(scope);
|
|
1336
1384
|
};
|
|
1337
1385
|
|
|
1338
1386
|
//#endregion
|
|
1339
1387
|
//#region src/hooks/useQueryParams.ts
|
|
1388
|
+
/**
|
|
1389
|
+
* Not well tested. Use with caution.
|
|
1390
|
+
*/
|
|
1340
1391
|
const useQueryParams = (schema, options = {}) => {
|
|
1341
1392
|
const alepha = useAlepha();
|
|
1342
1393
|
const key = options.key ?? "q";
|
|
@@ -1367,29 +1418,17 @@ const decode = (alepha, schema, data) => {
|
|
|
1367
1418
|
}
|
|
1368
1419
|
};
|
|
1369
1420
|
|
|
1370
|
-
//#endregion
|
|
1371
|
-
//#region src/hooks/useRouterState.ts
|
|
1372
|
-
const useRouterState = () => {
|
|
1373
|
-
const router = (0, react.useContext)(RouterContext);
|
|
1374
|
-
const layer = (0, react.useContext)(RouterLayerContext);
|
|
1375
|
-
if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
|
|
1376
|
-
const [state, setState] = (0, react.useState)(router.state);
|
|
1377
|
-
useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
|
|
1378
|
-
return state;
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
1421
|
//#endregion
|
|
1382
1422
|
//#region src/hooks/useSchema.ts
|
|
1383
1423
|
const useSchema = (action) => {
|
|
1384
1424
|
const name = action.name;
|
|
1385
1425
|
const alepha = useAlepha();
|
|
1386
1426
|
const httpClient = useInject(__alepha_server.HttpClient);
|
|
1387
|
-
const linkProvider = useInject(__alepha_server_links.LinkProvider);
|
|
1388
1427
|
const [schema, setSchema] = (0, react.useState)(ssrSchemaLoading(alepha, name));
|
|
1389
1428
|
(0, react.useEffect)(() => {
|
|
1390
1429
|
if (!schema.loading) return;
|
|
1391
1430
|
const opts = { cache: true };
|
|
1392
|
-
httpClient.fetch(`${
|
|
1431
|
+
httpClient.fetch(`${__alepha_server_links.LinkProvider.path.apiLinks}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
|
|
1393
1432
|
}, [name]);
|
|
1394
1433
|
return schema;
|
|
1395
1434
|
};
|
|
@@ -1398,10 +1437,10 @@ const useSchema = (action) => {
|
|
|
1398
1437
|
*/
|
|
1399
1438
|
const ssrSchemaLoading = (alepha, name) => {
|
|
1400
1439
|
if (!alepha.isBrowser()) {
|
|
1401
|
-
const
|
|
1402
|
-
const can =
|
|
1440
|
+
const linkProvider = alepha.inject(__alepha_server_links.LinkProvider);
|
|
1441
|
+
const can = linkProvider.getServerLinks().find((link) => link.name === name);
|
|
1403
1442
|
if (can) {
|
|
1404
|
-
const schema$1 =
|
|
1443
|
+
const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
|
|
1405
1444
|
if (schema$1) {
|
|
1406
1445
|
can.schema = schema$1;
|
|
1407
1446
|
return schema$1;
|
|
@@ -1409,7 +1448,7 @@ const ssrSchemaLoading = (alepha, name) => {
|
|
|
1409
1448
|
}
|
|
1410
1449
|
return { loading: true };
|
|
1411
1450
|
}
|
|
1412
|
-
const schema = alepha.inject(__alepha_server_links.LinkProvider).links
|
|
1451
|
+
const schema = alepha.inject(__alepha_server_links.LinkProvider).links.find((it) => it.name === name)?.schema;
|
|
1413
1452
|
if (schema) return schema;
|
|
1414
1453
|
return { loading: true };
|
|
1415
1454
|
};
|
|
@@ -1431,10 +1470,10 @@ const AlephaReact = (0, __alepha_core.$module)({
|
|
|
1431
1470
|
descriptors: [$page],
|
|
1432
1471
|
services: [
|
|
1433
1472
|
ReactServerProvider,
|
|
1434
|
-
|
|
1435
|
-
|
|
1473
|
+
ReactPageProvider,
|
|
1474
|
+
ReactRouter
|
|
1436
1475
|
],
|
|
1437
|
-
register: (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(
|
|
1476
|
+
register: (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
|
|
1438
1477
|
});
|
|
1439
1478
|
|
|
1440
1479
|
//#endregion
|
|
@@ -1443,16 +1482,16 @@ exports.AlephaContext = AlephaContext;
|
|
|
1443
1482
|
exports.AlephaReact = AlephaReact;
|
|
1444
1483
|
exports.ClientOnly = ClientOnly_default;
|
|
1445
1484
|
exports.ErrorBoundary = ErrorBoundary_default;
|
|
1485
|
+
exports.ErrorViewer = ErrorViewer_default;
|
|
1446
1486
|
exports.Link = Link_default;
|
|
1447
1487
|
exports.NestedView = NestedView_default;
|
|
1448
1488
|
exports.NotFound = NotFoundPage;
|
|
1449
1489
|
exports.PageDescriptor = PageDescriptor;
|
|
1450
|
-
exports.PageDescriptorProvider = PageDescriptorProvider;
|
|
1451
1490
|
exports.ReactBrowserProvider = ReactBrowserProvider;
|
|
1491
|
+
exports.ReactPageProvider = ReactPageProvider;
|
|
1492
|
+
exports.ReactRouter = ReactRouter;
|
|
1452
1493
|
exports.ReactServerProvider = ReactServerProvider;
|
|
1453
1494
|
exports.Redirection = Redirection;
|
|
1454
|
-
exports.RouterContext = RouterContext;
|
|
1455
|
-
exports.RouterHookApi = RouterHookApi;
|
|
1456
1495
|
exports.RouterLayerContext = RouterLayerContext;
|
|
1457
1496
|
exports.isPageRoute = isPageRoute;
|
|
1458
1497
|
exports.ssrSchemaLoading = ssrSchemaLoading;
|