@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.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { $env, $hook, $inject, $
|
|
2
|
-
import { AlephaServer, HttpClient, ServerRouterProvider, ServerTimingProvider
|
|
1
|
+
import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
|
|
2
|
+
import { AlephaServer, HttpClient, ServerRouterProvider, ServerTimingProvider } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
5
|
+
import { $logger } from "@alepha/logger";
|
|
5
6
|
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { RouterProvider } from "@alepha/router";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
11
11
|
import { renderToString } from "react-dom/server";
|
|
12
|
+
import { DateTimeProvider } from "@alepha/datetime";
|
|
13
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
14
|
+
import { RouterProvider } from "@alepha/router";
|
|
12
15
|
|
|
13
16
|
//#region src/descriptors/$page.ts
|
|
14
17
|
/**
|
|
@@ -34,6 +37,12 @@ var PageDescriptor = class extends Descriptor {
|
|
|
34
37
|
async render(options) {
|
|
35
38
|
throw new Error("render method is not implemented in this environment");
|
|
36
39
|
}
|
|
40
|
+
match(url) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
pathname(config) {
|
|
44
|
+
return this.options.path || "";
|
|
45
|
+
}
|
|
37
46
|
};
|
|
38
47
|
$page[KIND] = PageDescriptor;
|
|
39
48
|
|
|
@@ -205,28 +214,54 @@ const ErrorViewerProduction = () => {
|
|
|
205
214
|
});
|
|
206
215
|
};
|
|
207
216
|
|
|
208
|
-
//#endregion
|
|
209
|
-
//#region src/contexts/RouterContext.ts
|
|
210
|
-
const RouterContext = createContext(void 0);
|
|
211
|
-
|
|
212
217
|
//#endregion
|
|
213
218
|
//#region src/contexts/RouterLayerContext.ts
|
|
214
219
|
const RouterLayerContext = createContext(void 0);
|
|
215
220
|
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/errors/Redirection.ts
|
|
223
|
+
/**
|
|
224
|
+
* Used for Redirection during the page loading.
|
|
225
|
+
*
|
|
226
|
+
* Depends on the context, it can be thrown or just returned.
|
|
227
|
+
*/
|
|
228
|
+
var Redirection = class extends Error {
|
|
229
|
+
redirect;
|
|
230
|
+
constructor(redirect) {
|
|
231
|
+
super("Redirection");
|
|
232
|
+
this.redirect = redirect;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
216
236
|
//#endregion
|
|
217
237
|
//#region src/contexts/AlephaContext.ts
|
|
218
238
|
const AlephaContext = createContext(void 0);
|
|
219
239
|
|
|
220
240
|
//#endregion
|
|
221
241
|
//#region src/hooks/useAlepha.ts
|
|
242
|
+
/**
|
|
243
|
+
* Main Alepha hook.
|
|
244
|
+
*
|
|
245
|
+
* It provides access to the Alepha instance within a React component.
|
|
246
|
+
*
|
|
247
|
+
* With Alepha, you can access the core functionalities of the framework:
|
|
248
|
+
*
|
|
249
|
+
* - alepha.state() for state management
|
|
250
|
+
* - alepha.inject() for dependency injection
|
|
251
|
+
* - alepha.emit() for event handling
|
|
252
|
+
* etc...
|
|
253
|
+
*/
|
|
222
254
|
const useAlepha = () => {
|
|
223
255
|
const alepha = useContext(AlephaContext);
|
|
224
|
-
if (!alepha) throw new
|
|
256
|
+
if (!alepha) throw new AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
|
|
225
257
|
return alepha;
|
|
226
258
|
};
|
|
227
259
|
|
|
228
260
|
//#endregion
|
|
229
261
|
//#region src/hooks/useRouterEvents.ts
|
|
262
|
+
/**
|
|
263
|
+
* Subscribe to various router events.
|
|
264
|
+
*/
|
|
230
265
|
const useRouterEvents = (opts = {}, deps = []) => {
|
|
231
266
|
const alepha = useAlepha();
|
|
232
267
|
useEffect(() => {
|
|
@@ -299,19 +334,21 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
299
334
|
* ```
|
|
300
335
|
*/
|
|
301
336
|
const NestedView = (props) => {
|
|
302
|
-
const app = useContext(RouterContext);
|
|
303
337
|
const layer = useContext(RouterLayerContext);
|
|
304
338
|
const index = layer?.index ?? 0;
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
const alepha = useAlepha();
|
|
340
|
+
const state = alepha.state("react.router.state");
|
|
341
|
+
if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
|
|
342
|
+
const [view, setView] = useState(state.layers[index]?.element);
|
|
343
|
+
useRouterEvents({ onEnd: ({ state: state$1 }) => {
|
|
344
|
+
if (!state$1.layers[index]?.cache) setView(state$1.layers[index]?.element);
|
|
345
|
+
} }, []);
|
|
311
346
|
const element = view ?? props.children ?? null;
|
|
312
347
|
return /* @__PURE__ */ jsx(ErrorBoundary_default, {
|
|
313
348
|
fallback: (error) => {
|
|
314
|
-
|
|
349
|
+
const result = state.onError(error, state);
|
|
350
|
+
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
351
|
+
return result;
|
|
315
352
|
},
|
|
316
353
|
children: element
|
|
317
354
|
});
|
|
@@ -338,27 +375,17 @@ function NotFoundPage(props) {
|
|
|
338
375
|
fontSize: "1rem",
|
|
339
376
|
marginBottom: "0.5rem"
|
|
340
377
|
},
|
|
341
|
-
children: "This page does not exist"
|
|
378
|
+
children: "404 - This page does not exist"
|
|
342
379
|
})
|
|
343
380
|
});
|
|
344
381
|
}
|
|
345
382
|
|
|
346
383
|
//#endregion
|
|
347
|
-
//#region src/
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
constructor(page) {
|
|
351
|
-
super("Redirection");
|
|
352
|
-
this.page = page;
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
//#endregion
|
|
357
|
-
//#region src/providers/PageDescriptorProvider.ts
|
|
358
|
-
const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
359
|
-
var PageDescriptorProvider = class {
|
|
384
|
+
//#region src/providers/ReactPageProvider.ts
|
|
385
|
+
const envSchema$2 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
386
|
+
var ReactPageProvider = class {
|
|
360
387
|
log = $logger();
|
|
361
|
-
env = $env(envSchema$
|
|
388
|
+
env = $env(envSchema$2);
|
|
362
389
|
alepha = $inject(Alepha);
|
|
363
390
|
pages = [];
|
|
364
391
|
getPages() {
|
|
@@ -385,22 +412,21 @@ var PageDescriptorProvider = class {
|
|
|
385
412
|
return url.replace(/\/\/+/g, "/") || "/";
|
|
386
413
|
}
|
|
387
414
|
url(name, options = {}) {
|
|
388
|
-
return new URL(this.pathname(name, options), options.
|
|
415
|
+
return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
|
|
389
416
|
}
|
|
390
|
-
root(state
|
|
391
|
-
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(
|
|
392
|
-
state,
|
|
393
|
-
context
|
|
394
|
-
} }, createElement(NestedView_default, {}, state.layers[0]?.element)));
|
|
417
|
+
root(state) {
|
|
418
|
+
const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
|
|
395
419
|
if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
|
|
396
420
|
return root;
|
|
397
421
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Create a new RouterState based on a given route and request.
|
|
424
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
425
|
+
* It also handles errors and redirects.
|
|
426
|
+
*/
|
|
427
|
+
async createLayers(route, state, previous = []) {
|
|
401
428
|
let context = {};
|
|
402
429
|
const stack = [{ route }];
|
|
403
|
-
request.onError = (error) => this.renderError(error);
|
|
404
430
|
let parent = route.parent;
|
|
405
431
|
while (parent) {
|
|
406
432
|
stack.unshift({ route: parent });
|
|
@@ -412,19 +438,18 @@ var PageDescriptorProvider = class {
|
|
|
412
438
|
const route$1 = it.route;
|
|
413
439
|
const config = {};
|
|
414
440
|
try {
|
|
415
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query,
|
|
441
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
|
|
416
442
|
} catch (e) {
|
|
417
443
|
it.error = e;
|
|
418
444
|
break;
|
|
419
445
|
}
|
|
420
446
|
try {
|
|
421
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params,
|
|
447
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, state.params) : {};
|
|
422
448
|
} catch (e) {
|
|
423
449
|
it.error = e;
|
|
424
450
|
break;
|
|
425
451
|
}
|
|
426
452
|
it.config = { ...config };
|
|
427
|
-
const previous = request.previous;
|
|
428
453
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
429
454
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
430
455
|
const prev = JSON.stringify({
|
|
@@ -450,7 +475,7 @@ var PageDescriptorProvider = class {
|
|
|
450
475
|
if (!route$1.resolve) continue;
|
|
451
476
|
try {
|
|
452
477
|
const props = await route$1.resolve?.({
|
|
453
|
-
...
|
|
478
|
+
...state,
|
|
454
479
|
...config,
|
|
455
480
|
...context
|
|
456
481
|
}) ?? {};
|
|
@@ -460,11 +485,8 @@ var PageDescriptorProvider = class {
|
|
|
460
485
|
...props
|
|
461
486
|
};
|
|
462
487
|
} catch (e) {
|
|
463
|
-
if (e instanceof Redirection) return
|
|
464
|
-
|
|
465
|
-
search
|
|
466
|
-
});
|
|
467
|
-
this.log.error(e);
|
|
488
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
489
|
+
this.log.error("Page resolver has failed", e);
|
|
468
490
|
it.error = e;
|
|
469
491
|
break;
|
|
470
492
|
}
|
|
@@ -480,69 +502,58 @@ var PageDescriptorProvider = class {
|
|
|
480
502
|
const path = acc.replace(/\/+/, "/");
|
|
481
503
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
482
504
|
if (localErrorHandler) {
|
|
483
|
-
const onErrorParent =
|
|
484
|
-
|
|
505
|
+
const onErrorParent = state.onError;
|
|
506
|
+
state.onError = (error, context$1) => {
|
|
485
507
|
const result = localErrorHandler(error, context$1);
|
|
486
508
|
if (result === void 0) return onErrorParent(error, context$1);
|
|
487
509
|
return result;
|
|
488
510
|
};
|
|
489
511
|
}
|
|
490
|
-
if (it.error) try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
512
|
+
if (!it.error) try {
|
|
513
|
+
const element = await this.createElement(it.route, {
|
|
514
|
+
...props,
|
|
515
|
+
...context
|
|
516
|
+
});
|
|
517
|
+
state.layers.push({
|
|
518
|
+
name: it.route.name,
|
|
519
|
+
props,
|
|
520
|
+
part: it.route.path,
|
|
521
|
+
config: it.config,
|
|
522
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
523
|
+
index: i + 1,
|
|
524
|
+
path,
|
|
525
|
+
route: it.route,
|
|
526
|
+
cache: it.cache
|
|
496
527
|
});
|
|
497
|
-
|
|
498
|
-
|
|
528
|
+
} catch (e) {
|
|
529
|
+
it.error = e;
|
|
530
|
+
}
|
|
531
|
+
if (it.error) try {
|
|
532
|
+
let element = await state.onError(it.error, state);
|
|
533
|
+
if (element === void 0) throw it.error;
|
|
534
|
+
if (element instanceof Redirection) return { redirect: element.redirect };
|
|
535
|
+
if (element === null) element = this.renderError(it.error);
|
|
536
|
+
state.layers.push({
|
|
499
537
|
props,
|
|
500
538
|
error: it.error,
|
|
501
539
|
name: it.route.name,
|
|
502
540
|
part: it.route.path,
|
|
503
541
|
config: it.config,
|
|
504
|
-
element: this.renderView(i + 1, path, element
|
|
542
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
505
543
|
index: i + 1,
|
|
506
544
|
path,
|
|
507
545
|
route: it.route
|
|
508
546
|
});
|
|
509
547
|
break;
|
|
510
548
|
} catch (e) {
|
|
511
|
-
if (e instanceof Redirection) return
|
|
512
|
-
pathname,
|
|
513
|
-
search
|
|
514
|
-
});
|
|
549
|
+
if (e instanceof Redirection) return { redirect: e.redirect };
|
|
515
550
|
throw e;
|
|
516
551
|
}
|
|
517
|
-
const element = await this.createElement(it.route, {
|
|
518
|
-
...props,
|
|
519
|
-
...context
|
|
520
|
-
});
|
|
521
|
-
layers.push({
|
|
522
|
-
name: it.route.name,
|
|
523
|
-
props,
|
|
524
|
-
part: it.route.path,
|
|
525
|
-
config: it.config,
|
|
526
|
-
element: this.renderView(i + 1, path, element, it.route),
|
|
527
|
-
index: i + 1,
|
|
528
|
-
path,
|
|
529
|
-
route: it.route,
|
|
530
|
-
cache: it.cache
|
|
531
|
-
});
|
|
532
552
|
}
|
|
533
|
-
return {
|
|
534
|
-
layers,
|
|
535
|
-
pathname,
|
|
536
|
-
search
|
|
537
|
-
};
|
|
553
|
+
return { state };
|
|
538
554
|
}
|
|
539
|
-
createRedirectionLayer(
|
|
540
|
-
return {
|
|
541
|
-
layers: [],
|
|
542
|
-
redirect: typeof href === "string" ? href : this.href(href),
|
|
543
|
-
pathname: context.pathname,
|
|
544
|
-
search: context.search
|
|
545
|
-
};
|
|
555
|
+
createRedirectionLayer(redirect) {
|
|
556
|
+
return { redirect };
|
|
546
557
|
}
|
|
547
558
|
getErrorHandler(route) {
|
|
548
559
|
if (route.errorHandler) return route.errorHandler;
|
|
@@ -668,223 +679,9 @@ const isPageRoute = (it) => {
|
|
|
668
679
|
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
669
680
|
};
|
|
670
681
|
|
|
671
|
-
//#endregion
|
|
672
|
-
//#region src/providers/BrowserRouterProvider.ts
|
|
673
|
-
var BrowserRouterProvider = class extends RouterProvider {
|
|
674
|
-
log = $logger();
|
|
675
|
-
alepha = $inject(Alepha);
|
|
676
|
-
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
677
|
-
add(entry) {
|
|
678
|
-
this.pageDescriptorProvider.add(entry);
|
|
679
|
-
}
|
|
680
|
-
configure = $hook({
|
|
681
|
-
on: "configure",
|
|
682
|
-
handler: async () => {
|
|
683
|
-
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
684
|
-
path: page.match,
|
|
685
|
-
page
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
async transition(url, options = {}) {
|
|
690
|
-
const { pathname, search } = url;
|
|
691
|
-
const state = {
|
|
692
|
-
pathname,
|
|
693
|
-
search,
|
|
694
|
-
layers: []
|
|
695
|
-
};
|
|
696
|
-
const context = {
|
|
697
|
-
url,
|
|
698
|
-
query: {},
|
|
699
|
-
params: {},
|
|
700
|
-
onError: () => null,
|
|
701
|
-
...options.context ?? {}
|
|
702
|
-
};
|
|
703
|
-
await this.alepha.emit("react:transition:begin", {
|
|
704
|
-
state,
|
|
705
|
-
context
|
|
706
|
-
});
|
|
707
|
-
try {
|
|
708
|
-
const previous = options.previous;
|
|
709
|
-
const { route, params } = this.match(pathname);
|
|
710
|
-
const query = {};
|
|
711
|
-
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
712
|
-
context.query = query;
|
|
713
|
-
context.params = params ?? {};
|
|
714
|
-
context.previous = previous;
|
|
715
|
-
if (isPageRoute(route)) {
|
|
716
|
-
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
717
|
-
if (result.redirect) return {
|
|
718
|
-
redirect: result.redirect,
|
|
719
|
-
state,
|
|
720
|
-
context
|
|
721
|
-
};
|
|
722
|
-
state.layers = result.layers;
|
|
723
|
-
}
|
|
724
|
-
if (state.layers.length === 0) state.layers.push({
|
|
725
|
-
name: "not-found",
|
|
726
|
-
element: createElement(NotFoundPage),
|
|
727
|
-
index: 0,
|
|
728
|
-
path: "/"
|
|
729
|
-
});
|
|
730
|
-
await this.alepha.emit("react:transition:success", {
|
|
731
|
-
state,
|
|
732
|
-
context
|
|
733
|
-
});
|
|
734
|
-
} catch (e) {
|
|
735
|
-
this.log.error(e);
|
|
736
|
-
state.layers = [{
|
|
737
|
-
name: "error",
|
|
738
|
-
element: this.pageDescriptorProvider.renderError(e),
|
|
739
|
-
index: 0,
|
|
740
|
-
path: "/"
|
|
741
|
-
}];
|
|
742
|
-
await this.alepha.emit("react:transition:error", {
|
|
743
|
-
error: e,
|
|
744
|
-
state,
|
|
745
|
-
context
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
if (options.state) {
|
|
749
|
-
options.state.layers = state.layers;
|
|
750
|
-
options.state.pathname = state.pathname;
|
|
751
|
-
options.state.search = state.search;
|
|
752
|
-
}
|
|
753
|
-
if (options.previous) for (let i = 0; i < options.previous.length; i++) {
|
|
754
|
-
const layer = options.previous[i];
|
|
755
|
-
if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
|
|
756
|
-
}
|
|
757
|
-
await this.alepha.emit("react:transition:end", {
|
|
758
|
-
state: options.state,
|
|
759
|
-
context
|
|
760
|
-
});
|
|
761
|
-
return {
|
|
762
|
-
context,
|
|
763
|
-
state
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
root(state, context) {
|
|
767
|
-
return this.pageDescriptorProvider.root(state, context);
|
|
768
|
-
}
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
//#endregion
|
|
772
|
-
//#region src/providers/ReactBrowserProvider.ts
|
|
773
|
-
var ReactBrowserProvider = class {
|
|
774
|
-
log = $logger();
|
|
775
|
-
client = $inject(LinkProvider);
|
|
776
|
-
alepha = $inject(Alepha);
|
|
777
|
-
router = $inject(BrowserRouterProvider);
|
|
778
|
-
root;
|
|
779
|
-
transitioning;
|
|
780
|
-
state = {
|
|
781
|
-
layers: [],
|
|
782
|
-
pathname: "",
|
|
783
|
-
search: ""
|
|
784
|
-
};
|
|
785
|
-
get document() {
|
|
786
|
-
return window.document;
|
|
787
|
-
}
|
|
788
|
-
get history() {
|
|
789
|
-
return window.history;
|
|
790
|
-
}
|
|
791
|
-
get location() {
|
|
792
|
-
return window.location;
|
|
793
|
-
}
|
|
794
|
-
get url() {
|
|
795
|
-
let url = this.location.pathname + this.location.search;
|
|
796
|
-
if (import.meta?.env?.BASE_URL) {
|
|
797
|
-
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
798
|
-
if (!url.startsWith("/")) url = `/${url}`;
|
|
799
|
-
}
|
|
800
|
-
return url;
|
|
801
|
-
}
|
|
802
|
-
pushState(url, replace) {
|
|
803
|
-
let path = url;
|
|
804
|
-
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
805
|
-
if (replace) this.history.replaceState({}, "", path);
|
|
806
|
-
else this.history.pushState({}, "", path);
|
|
807
|
-
}
|
|
808
|
-
async invalidate(props) {
|
|
809
|
-
const previous = [];
|
|
810
|
-
if (props) {
|
|
811
|
-
const [key] = Object.keys(props);
|
|
812
|
-
const value = props[key];
|
|
813
|
-
for (const layer of this.state.layers) {
|
|
814
|
-
if (layer.props?.[key]) {
|
|
815
|
-
previous.push({
|
|
816
|
-
...layer,
|
|
817
|
-
props: {
|
|
818
|
-
...layer.props,
|
|
819
|
-
[key]: value
|
|
820
|
-
}
|
|
821
|
-
});
|
|
822
|
-
break;
|
|
823
|
-
}
|
|
824
|
-
previous.push(layer);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
await this.render({ previous });
|
|
828
|
-
}
|
|
829
|
-
async go(url, options = {}) {
|
|
830
|
-
const result = await this.render({ url });
|
|
831
|
-
if (result.context.url.pathname + result.context.url.search !== url) {
|
|
832
|
-
this.pushState(result.context.url.pathname + result.context.url.search);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
this.pushState(url, options.replace);
|
|
836
|
-
}
|
|
837
|
-
async render(options = {}) {
|
|
838
|
-
const previous = options.previous ?? this.state.layers;
|
|
839
|
-
const url = options.url ?? this.url;
|
|
840
|
-
this.transitioning = { to: url };
|
|
841
|
-
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
842
|
-
previous,
|
|
843
|
-
state: this.state
|
|
844
|
-
});
|
|
845
|
-
if (result.redirect) return await this.render({ url: result.redirect });
|
|
846
|
-
this.transitioning = void 0;
|
|
847
|
-
return result;
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* Get embedded layers from the server.
|
|
851
|
-
*/
|
|
852
|
-
getHydrationState() {
|
|
853
|
-
try {
|
|
854
|
-
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
855
|
-
} catch (error) {
|
|
856
|
-
console.error(error);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
ready = $hook({
|
|
860
|
-
on: "ready",
|
|
861
|
-
handler: async () => {
|
|
862
|
-
const hydration = this.getHydrationState();
|
|
863
|
-
const previous = hydration?.layers ?? [];
|
|
864
|
-
if (hydration) {
|
|
865
|
-
for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
|
|
866
|
-
}
|
|
867
|
-
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
|
|
868
|
-
...link,
|
|
869
|
-
prefix: hydration.links.prefix
|
|
870
|
-
});
|
|
871
|
-
const { context } = await this.render({ previous });
|
|
872
|
-
await this.alepha.emit("react:browser:render", {
|
|
873
|
-
state: this.state,
|
|
874
|
-
context,
|
|
875
|
-
hydration
|
|
876
|
-
});
|
|
877
|
-
window.addEventListener("popstate", () => {
|
|
878
|
-
if (this.state.pathname === this.url) return;
|
|
879
|
-
this.render();
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
};
|
|
884
|
-
|
|
885
682
|
//#endregion
|
|
886
683
|
//#region src/providers/ReactServerProvider.ts
|
|
887
|
-
const envSchema = t.object({
|
|
684
|
+
const envSchema$1 = t.object({
|
|
888
685
|
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
889
686
|
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
890
687
|
REACT_SSR_ENABLED: t.optional(t.boolean()),
|
|
@@ -894,11 +691,11 @@ const envSchema = t.object({
|
|
|
894
691
|
var ReactServerProvider = class {
|
|
895
692
|
log = $logger();
|
|
896
693
|
alepha = $inject(Alepha);
|
|
897
|
-
|
|
694
|
+
pageApi = $inject(ReactPageProvider);
|
|
898
695
|
serverStaticProvider = $inject(ServerStaticProvider);
|
|
899
696
|
serverRouterProvider = $inject(ServerRouterProvider);
|
|
900
697
|
serverTimingProvider = $inject(ServerTimingProvider);
|
|
901
|
-
env = $env(envSchema);
|
|
698
|
+
env = $env(envSchema$1);
|
|
902
699
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
903
700
|
onConfigure = $hook({
|
|
904
701
|
on: "configure",
|
|
@@ -945,7 +742,7 @@ var ReactServerProvider = class {
|
|
|
945
742
|
return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
946
743
|
}
|
|
947
744
|
async registerPages(templateLoader) {
|
|
948
|
-
for (const page of this.
|
|
745
|
+
for (const page of this.pageApi.getPages()) {
|
|
949
746
|
if (page.children?.length) continue;
|
|
950
747
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
951
748
|
this.serverRouterProvider.createRoute({
|
|
@@ -979,25 +776,30 @@ var ReactServerProvider = class {
|
|
|
979
776
|
*/
|
|
980
777
|
createRenderFunction(name, withIndex = false) {
|
|
981
778
|
return async (options = {}) => {
|
|
982
|
-
const page = this.
|
|
983
|
-
const url = new URL(this.
|
|
984
|
-
const
|
|
779
|
+
const page = this.pageApi.page(name);
|
|
780
|
+
const url = new URL(this.pageApi.url(name, options));
|
|
781
|
+
const entry = {
|
|
985
782
|
url,
|
|
986
783
|
params: options.params ?? {},
|
|
987
784
|
query: options.query ?? {},
|
|
988
|
-
|
|
989
|
-
|
|
785
|
+
onError: () => null,
|
|
786
|
+
layers: []
|
|
990
787
|
};
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
788
|
+
const state = entry;
|
|
789
|
+
this.log.trace("Rendering", { url });
|
|
790
|
+
await this.alepha.emit("react:server:render:begin", { state });
|
|
791
|
+
const { redirect } = await this.pageApi.createLayers(page, state);
|
|
792
|
+
if (redirect) throw new AlephaError("Redirection is not supported in this context");
|
|
793
|
+
if (!withIndex && !options.html) {
|
|
794
|
+
this.alepha.state("react.router.state", state);
|
|
795
|
+
return {
|
|
796
|
+
state,
|
|
797
|
+
html: renderToString(this.pageApi.root(state))
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const html = this.renderToHtml(this.template ?? "", state, options.hydration);
|
|
998
801
|
if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
|
|
999
802
|
const result = {
|
|
1000
|
-
context,
|
|
1001
803
|
state,
|
|
1002
804
|
html
|
|
1003
805
|
};
|
|
@@ -1005,89 +807,82 @@ var ReactServerProvider = class {
|
|
|
1005
807
|
return result;
|
|
1006
808
|
};
|
|
1007
809
|
}
|
|
1008
|
-
createHandler(
|
|
810
|
+
createHandler(route, templateLoader) {
|
|
1009
811
|
return async (serverRequest) => {
|
|
1010
812
|
const { url, reply, query, params } = serverRequest;
|
|
1011
813
|
const template = await templateLoader();
|
|
1012
814
|
if (!template) throw new Error("Template not found");
|
|
1013
|
-
this.log.trace("Rendering page", { name:
|
|
1014
|
-
const
|
|
815
|
+
this.log.trace("Rendering page", { name: route.name });
|
|
816
|
+
const entry = {
|
|
1015
817
|
url,
|
|
1016
818
|
params,
|
|
1017
819
|
query,
|
|
1018
|
-
|
|
1019
|
-
|
|
820
|
+
onError: () => null,
|
|
821
|
+
layers: []
|
|
1020
822
|
};
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}));
|
|
1028
|
-
this.alepha.context.set("links", context.links);
|
|
1029
|
-
}
|
|
1030
|
-
let target = page;
|
|
823
|
+
const state = entry;
|
|
824
|
+
if (this.alepha.has(ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
825
|
+
user: serverRequest.user,
|
|
826
|
+
authorization: serverRequest.headers.authorization
|
|
827
|
+
}));
|
|
828
|
+
let target = route;
|
|
1031
829
|
while (target) {
|
|
1032
|
-
if (
|
|
830
|
+
if (route.can && !route.can()) {
|
|
1033
831
|
reply.status = 403;
|
|
1034
832
|
reply.headers["content-type"] = "text/plain";
|
|
1035
833
|
return "Forbidden";
|
|
1036
834
|
}
|
|
1037
835
|
target = target.parent;
|
|
1038
836
|
}
|
|
1039
|
-
await this.alepha.emit("react:transition:begin", {
|
|
1040
|
-
request: serverRequest,
|
|
1041
|
-
context
|
|
1042
|
-
});
|
|
1043
837
|
await this.alepha.emit("react:server:render:begin", {
|
|
1044
838
|
request: serverRequest,
|
|
1045
|
-
|
|
839
|
+
state
|
|
1046
840
|
});
|
|
1047
841
|
this.serverTimingProvider.beginTiming("createLayers");
|
|
1048
|
-
const
|
|
842
|
+
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
1049
843
|
this.serverTimingProvider.endTiming("createLayers");
|
|
1050
|
-
if (
|
|
844
|
+
if (redirect) return reply.redirect(redirect);
|
|
1051
845
|
reply.headers["content-type"] = "text/html";
|
|
1052
846
|
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
1053
847
|
reply.headers.pragma = "no-cache";
|
|
1054
848
|
reply.headers.expires = "0";
|
|
1055
|
-
|
|
1056
|
-
const html = this.renderToHtml(template, state, context);
|
|
849
|
+
const html = this.renderToHtml(template, state);
|
|
1057
850
|
if (html instanceof Redirection) {
|
|
1058
|
-
reply.redirect(typeof html.
|
|
851
|
+
reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
|
|
1059
852
|
return;
|
|
1060
853
|
}
|
|
1061
854
|
const event = {
|
|
1062
855
|
request: serverRequest,
|
|
1063
|
-
context,
|
|
1064
856
|
state,
|
|
1065
857
|
html
|
|
1066
858
|
};
|
|
1067
859
|
await this.alepha.emit("react:server:render:end", event);
|
|
1068
|
-
|
|
1069
|
-
this.log.trace("Page rendered", { name:
|
|
860
|
+
route.onServerResponse?.(serverRequest);
|
|
861
|
+
this.log.trace("Page rendered", { name: route.name });
|
|
1070
862
|
return event.html;
|
|
1071
863
|
};
|
|
1072
864
|
}
|
|
1073
|
-
renderToHtml(template, state,
|
|
1074
|
-
const element = this.
|
|
865
|
+
renderToHtml(template, state, hydration = true) {
|
|
866
|
+
const element = this.pageApi.root(state);
|
|
867
|
+
this.alepha.state("react.router.state", state);
|
|
1075
868
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
1076
869
|
let app = "";
|
|
1077
870
|
try {
|
|
1078
871
|
app = renderToString(element);
|
|
1079
872
|
} catch (error) {
|
|
1080
|
-
this.log.error("
|
|
1081
|
-
const element$1 =
|
|
873
|
+
this.log.error("renderToString has failed, fallback to error handler", error);
|
|
874
|
+
const element$1 = state.onError(error, state);
|
|
1082
875
|
if (element$1 instanceof Redirection) return element$1;
|
|
1083
876
|
app = renderToString(element$1);
|
|
877
|
+
this.log.debug("Error handled successfully with fallback");
|
|
1084
878
|
}
|
|
1085
879
|
this.serverTimingProvider.endTiming("renderToString");
|
|
1086
880
|
const response = { html: template };
|
|
1087
881
|
if (hydration) {
|
|
1088
|
-
const { request, context
|
|
882
|
+
const { request, context,...store } = this.alepha.context.als?.getStore() ?? {};
|
|
1089
883
|
const hydrationData = {
|
|
1090
|
-
...
|
|
884
|
+
...store,
|
|
885
|
+
"react.router.state": void 0,
|
|
1091
886
|
layers: state.layers.map((it) => ({
|
|
1092
887
|
...it,
|
|
1093
888
|
error: it.error ? {
|
|
@@ -1102,7 +897,7 @@ var ReactServerProvider = class {
|
|
|
1102
897
|
route: void 0
|
|
1103
898
|
}))
|
|
1104
899
|
};
|
|
1105
|
-
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}
|
|
900
|
+
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
|
|
1106
901
|
this.fillTemplate(response, app, script);
|
|
1107
902
|
}
|
|
1108
903
|
return response.html;
|
|
@@ -1123,27 +918,260 @@ var ReactServerProvider = class {
|
|
|
1123
918
|
};
|
|
1124
919
|
|
|
1125
920
|
//#endregion
|
|
1126
|
-
//#region src/
|
|
1127
|
-
var
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
this.
|
|
1133
|
-
|
|
1134
|
-
|
|
921
|
+
//#region src/providers/ReactBrowserRouterProvider.ts
|
|
922
|
+
var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
923
|
+
log = $logger();
|
|
924
|
+
alepha = $inject(Alepha);
|
|
925
|
+
pageApi = $inject(ReactPageProvider);
|
|
926
|
+
add(entry) {
|
|
927
|
+
this.pageApi.add(entry);
|
|
928
|
+
}
|
|
929
|
+
configure = $hook({
|
|
930
|
+
on: "configure",
|
|
931
|
+
handler: async () => {
|
|
932
|
+
for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
|
|
933
|
+
path: page.match,
|
|
934
|
+
page
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
async transition(url, previous = []) {
|
|
939
|
+
const { pathname, search } = url;
|
|
940
|
+
const entry = {
|
|
941
|
+
url,
|
|
942
|
+
query: {},
|
|
943
|
+
params: {},
|
|
944
|
+
layers: [],
|
|
945
|
+
onError: () => null
|
|
946
|
+
};
|
|
947
|
+
const state = entry;
|
|
948
|
+
await this.alepha.emit("react:transition:begin", { state });
|
|
949
|
+
try {
|
|
950
|
+
const { route, params } = this.match(pathname);
|
|
951
|
+
const query = {};
|
|
952
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
953
|
+
state.query = query;
|
|
954
|
+
state.params = params ?? {};
|
|
955
|
+
if (isPageRoute(route)) {
|
|
956
|
+
const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
|
|
957
|
+
if (redirect) return redirect;
|
|
958
|
+
}
|
|
959
|
+
if (state.layers.length === 0) state.layers.push({
|
|
960
|
+
name: "not-found",
|
|
961
|
+
element: createElement(NotFoundPage),
|
|
962
|
+
index: 0,
|
|
963
|
+
path: "/"
|
|
964
|
+
});
|
|
965
|
+
await this.alepha.emit("react:transition:success", { state });
|
|
966
|
+
} catch (e) {
|
|
967
|
+
this.log.error("Transition has failed", e);
|
|
968
|
+
state.layers = [{
|
|
969
|
+
name: "error",
|
|
970
|
+
element: this.pageApi.renderError(e),
|
|
971
|
+
index: 0,
|
|
972
|
+
path: "/"
|
|
973
|
+
}];
|
|
974
|
+
await this.alepha.emit("react:transition:error", {
|
|
975
|
+
error: e,
|
|
976
|
+
state
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if (previous) for (let i = 0; i < previous.length; i++) {
|
|
980
|
+
const layer = previous[i];
|
|
981
|
+
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
982
|
+
}
|
|
983
|
+
await this.alepha.emit("react:transition:end", { state });
|
|
984
|
+
this.alepha.state("react.router.state", state);
|
|
985
|
+
}
|
|
986
|
+
root(state) {
|
|
987
|
+
return this.pageApi.root(state);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/providers/ReactBrowserProvider.ts
|
|
993
|
+
const envSchema = t.object({ REACT_ROOT_ID: t.string({ default: "root" }) });
|
|
994
|
+
var ReactBrowserProvider = class {
|
|
995
|
+
env = $env(envSchema);
|
|
996
|
+
log = $logger();
|
|
997
|
+
client = $inject(LinkProvider);
|
|
998
|
+
alepha = $inject(Alepha);
|
|
999
|
+
router = $inject(ReactBrowserRouterProvider);
|
|
1000
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
1001
|
+
root;
|
|
1002
|
+
options = { scrollRestoration: "top" };
|
|
1003
|
+
getRootElement() {
|
|
1004
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
1005
|
+
if (root) return root;
|
|
1006
|
+
const div = this.document.createElement("div");
|
|
1007
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
1008
|
+
this.document.body.prepend(div);
|
|
1009
|
+
return div;
|
|
1010
|
+
}
|
|
1011
|
+
transitioning;
|
|
1012
|
+
get state() {
|
|
1013
|
+
return this.alepha.state("react.router.state");
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Accessor for Document DOM API.
|
|
1017
|
+
*/
|
|
1018
|
+
get document() {
|
|
1019
|
+
return window.document;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Accessor for History DOM API.
|
|
1023
|
+
*/
|
|
1024
|
+
get history() {
|
|
1025
|
+
return window.history;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Accessor for Location DOM API.
|
|
1029
|
+
*/
|
|
1030
|
+
get location() {
|
|
1031
|
+
return window.location;
|
|
1032
|
+
}
|
|
1033
|
+
get base() {
|
|
1034
|
+
const base = import.meta.env?.BASE_URL;
|
|
1035
|
+
if (!base || base === "/") return "";
|
|
1036
|
+
return base;
|
|
1037
|
+
}
|
|
1038
|
+
get url() {
|
|
1039
|
+
const url = this.location.pathname + this.location.search;
|
|
1040
|
+
if (this.base) return url.replace(this.base, "");
|
|
1041
|
+
return url;
|
|
1042
|
+
}
|
|
1043
|
+
pushState(path, replace) {
|
|
1044
|
+
const url = this.base + path;
|
|
1045
|
+
if (replace) this.history.replaceState({}, "", url);
|
|
1046
|
+
else this.history.pushState({}, "", url);
|
|
1047
|
+
}
|
|
1048
|
+
async invalidate(props) {
|
|
1049
|
+
const previous = [];
|
|
1050
|
+
this.log.trace("Invalidating layers");
|
|
1051
|
+
if (props) {
|
|
1052
|
+
const [key] = Object.keys(props);
|
|
1053
|
+
const value = props[key];
|
|
1054
|
+
for (const layer of this.state.layers) {
|
|
1055
|
+
if (layer.props?.[key]) {
|
|
1056
|
+
previous.push({
|
|
1057
|
+
...layer,
|
|
1058
|
+
props: {
|
|
1059
|
+
...layer.props,
|
|
1060
|
+
[key]: value
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
previous.push(layer);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
await this.render({ previous });
|
|
1069
|
+
}
|
|
1070
|
+
async go(url, options = {}) {
|
|
1071
|
+
this.log.trace(`Going to ${url}`, {
|
|
1072
|
+
url,
|
|
1073
|
+
options
|
|
1074
|
+
});
|
|
1075
|
+
await this.render({
|
|
1076
|
+
url,
|
|
1077
|
+
previous: options.force ? [] : this.state.layers
|
|
1078
|
+
});
|
|
1079
|
+
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
1080
|
+
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
this.pushState(url, options.replace);
|
|
1084
|
+
}
|
|
1085
|
+
async render(options = {}) {
|
|
1086
|
+
const previous = options.previous ?? this.state.layers;
|
|
1087
|
+
const url = options.url ?? this.url;
|
|
1088
|
+
const start = this.dateTimeProvider.now();
|
|
1089
|
+
this.transitioning = {
|
|
1090
|
+
to: url,
|
|
1091
|
+
from: this.state?.url.pathname
|
|
1092
|
+
};
|
|
1093
|
+
this.log.debug("Transitioning...", { to: url });
|
|
1094
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
|
|
1095
|
+
if (redirect) {
|
|
1096
|
+
this.log.info("Redirecting to", { redirect });
|
|
1097
|
+
return await this.render({ url: redirect });
|
|
1098
|
+
}
|
|
1099
|
+
const ms = this.dateTimeProvider.now().diff(start);
|
|
1100
|
+
this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
|
|
1101
|
+
this.transitioning = void 0;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Get embedded layers from the server.
|
|
1105
|
+
*/
|
|
1106
|
+
getHydrationState() {
|
|
1107
|
+
try {
|
|
1108
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
console.error(error);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
onTransitionEnd = $hook({
|
|
1114
|
+
on: "react:transition:end",
|
|
1115
|
+
handler: () => {
|
|
1116
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
|
|
1117
|
+
this.log.trace("Restoring scroll position to top");
|
|
1118
|
+
window.scrollTo(0, 0);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
ready = $hook({
|
|
1123
|
+
on: "ready",
|
|
1124
|
+
handler: async () => {
|
|
1125
|
+
const hydration = this.getHydrationState();
|
|
1126
|
+
const previous = hydration?.layers ?? [];
|
|
1127
|
+
if (hydration) {
|
|
1128
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
|
|
1129
|
+
}
|
|
1130
|
+
await this.render({ previous });
|
|
1131
|
+
const element = this.router.root(this.state);
|
|
1132
|
+
if (hydration?.layers) {
|
|
1133
|
+
this.root = hydrateRoot(this.getRootElement(), element);
|
|
1134
|
+
this.log.info("Hydrated root element");
|
|
1135
|
+
} else {
|
|
1136
|
+
this.root ??= createRoot(this.getRootElement());
|
|
1137
|
+
this.root.render(element);
|
|
1138
|
+
this.log.info("Created root element");
|
|
1139
|
+
}
|
|
1140
|
+
window.addEventListener("popstate", () => {
|
|
1141
|
+
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1142
|
+
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
1143
|
+
this.render();
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region src/services/ReactRouter.ts
|
|
1151
|
+
var ReactRouter = class {
|
|
1152
|
+
alepha = $inject(Alepha);
|
|
1153
|
+
pageApi = $inject(ReactPageProvider);
|
|
1154
|
+
get state() {
|
|
1155
|
+
return this.alepha.state("react.router.state");
|
|
1156
|
+
}
|
|
1157
|
+
get pages() {
|
|
1158
|
+
return this.pageApi.getPages();
|
|
1159
|
+
}
|
|
1160
|
+
get browser() {
|
|
1161
|
+
if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
|
|
1162
|
+
return void 0;
|
|
1135
1163
|
}
|
|
1136
1164
|
path(name, config = {}) {
|
|
1137
1165
|
return this.pageApi.pathname(name, {
|
|
1138
1166
|
params: {
|
|
1139
|
-
...this.
|
|
1167
|
+
...this.state.params,
|
|
1140
1168
|
...config.params
|
|
1141
1169
|
},
|
|
1142
1170
|
query: config.query
|
|
1143
1171
|
});
|
|
1144
1172
|
}
|
|
1145
1173
|
getURL() {
|
|
1146
|
-
if (!this.browser) return this.
|
|
1174
|
+
if (!this.browser) return this.state.url;
|
|
1147
1175
|
return new URL(this.location.href);
|
|
1148
1176
|
}
|
|
1149
1177
|
get location() {
|
|
@@ -1154,11 +1182,11 @@ var RouterHookApi = class {
|
|
|
1154
1182
|
return this.state;
|
|
1155
1183
|
}
|
|
1156
1184
|
get pathname() {
|
|
1157
|
-
return this.state.pathname;
|
|
1185
|
+
return this.state.url.pathname;
|
|
1158
1186
|
}
|
|
1159
1187
|
get query() {
|
|
1160
1188
|
const query = {};
|
|
1161
|
-
for (const [key, value] of new URLSearchParams(this.state.search).entries()) query[key] = String(value);
|
|
1189
|
+
for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
|
|
1162
1190
|
return query;
|
|
1163
1191
|
}
|
|
1164
1192
|
async back() {
|
|
@@ -1170,17 +1198,6 @@ var RouterHookApi = class {
|
|
|
1170
1198
|
async invalidate(props) {
|
|
1171
1199
|
await this.browser?.invalidate(props);
|
|
1172
1200
|
}
|
|
1173
|
-
/**
|
|
1174
|
-
* Create a valid href for the given pathname.
|
|
1175
|
-
*
|
|
1176
|
-
* @param pathname
|
|
1177
|
-
* @param layer
|
|
1178
|
-
*/
|
|
1179
|
-
createHref(pathname, layer = this.layer, options = {}) {
|
|
1180
|
-
if (typeof pathname === "object") pathname = pathname.options.path ?? "";
|
|
1181
|
-
if (options.params) for (const [key, value] of Object.entries(options.params)) pathname = pathname.replace(`:${key}`, String(value));
|
|
1182
|
-
return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
1183
|
-
}
|
|
1184
1201
|
async go(path, options) {
|
|
1185
1202
|
for (const page of this.pages) if (page.name === path) {
|
|
1186
1203
|
await this.browser?.go(this.path(path, options), options);
|
|
@@ -1195,7 +1212,7 @@ var RouterHookApi = class {
|
|
|
1195
1212
|
break;
|
|
1196
1213
|
}
|
|
1197
1214
|
return {
|
|
1198
|
-
href,
|
|
1215
|
+
href: this.base(href),
|
|
1199
1216
|
onClick: (ev) => {
|
|
1200
1217
|
ev.stopPropagation();
|
|
1201
1218
|
ev.preventDefault();
|
|
@@ -1203,6 +1220,11 @@ var RouterHookApi = class {
|
|
|
1203
1220
|
}
|
|
1204
1221
|
};
|
|
1205
1222
|
}
|
|
1223
|
+
base(path) {
|
|
1224
|
+
const base = import.meta.env?.BASE_URL;
|
|
1225
|
+
if (!base || base === "/") return path;
|
|
1226
|
+
return base + path;
|
|
1227
|
+
}
|
|
1206
1228
|
/**
|
|
1207
1229
|
* Set query params.
|
|
1208
1230
|
*
|
|
@@ -1218,17 +1240,35 @@ var RouterHookApi = class {
|
|
|
1218
1240
|
}
|
|
1219
1241
|
};
|
|
1220
1242
|
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region src/hooks/useInject.ts
|
|
1245
|
+
/**
|
|
1246
|
+
* Hook to inject a service instance.
|
|
1247
|
+
* It's a wrapper of `useAlepha().inject(service)` with a memoization.
|
|
1248
|
+
*/
|
|
1249
|
+
const useInject = (service) => {
|
|
1250
|
+
const alepha = useAlepha();
|
|
1251
|
+
return useMemo(() => alepha.inject(service), []);
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1221
1254
|
//#endregion
|
|
1222
1255
|
//#region src/hooks/useRouter.ts
|
|
1256
|
+
/**
|
|
1257
|
+
* Use this hook to access the React Router instance.
|
|
1258
|
+
*
|
|
1259
|
+
* You can add a type parameter to specify the type of your application.
|
|
1260
|
+
* This will allow you to use the router in a typesafe way.
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* class App {
|
|
1264
|
+
* home = $page();
|
|
1265
|
+
* }
|
|
1266
|
+
*
|
|
1267
|
+
* const router = useRouter<App>();
|
|
1268
|
+
* router.go("home"); // typesafe
|
|
1269
|
+
*/
|
|
1223
1270
|
const useRouter = () => {
|
|
1224
|
-
|
|
1225
|
-
const ctx = useContext(RouterContext);
|
|
1226
|
-
const layer = useContext(RouterLayerContext);
|
|
1227
|
-
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1228
|
-
const pages = useMemo(() => {
|
|
1229
|
-
return alepha.inject(PageDescriptorProvider).getPages();
|
|
1230
|
-
}, []);
|
|
1231
|
-
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
|
|
1271
|
+
return useInject(ReactRouter);
|
|
1232
1272
|
};
|
|
1233
1273
|
|
|
1234
1274
|
//#endregion
|
|
@@ -1244,46 +1284,6 @@ const Link = (props) => {
|
|
|
1244
1284
|
};
|
|
1245
1285
|
var Link_default = Link;
|
|
1246
1286
|
|
|
1247
|
-
//#endregion
|
|
1248
|
-
//#region src/hooks/useActive.ts
|
|
1249
|
-
const useActive = (path) => {
|
|
1250
|
-
const router = useRouter();
|
|
1251
|
-
const ctx = useContext(RouterContext);
|
|
1252
|
-
const layer = useContext(RouterLayerContext);
|
|
1253
|
-
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1254
|
-
const [current, setCurrent] = useState(ctx.state.pathname);
|
|
1255
|
-
const href = useMemo(() => router.createHref(path ?? "", layer), [path, layer]);
|
|
1256
|
-
const [isPending, setPending] = useState(false);
|
|
1257
|
-
const isActive = current === href || current === `${href}/` || `${current}/` === href;
|
|
1258
|
-
useRouterEvents({ onEnd: ({ state }) => {
|
|
1259
|
-
path && setCurrent(state.pathname);
|
|
1260
|
-
} }, [path]);
|
|
1261
|
-
return {
|
|
1262
|
-
isPending,
|
|
1263
|
-
isActive,
|
|
1264
|
-
anchorProps: {
|
|
1265
|
-
href,
|
|
1266
|
-
onClick: (ev) => {
|
|
1267
|
-
ev?.stopPropagation();
|
|
1268
|
-
ev?.preventDefault();
|
|
1269
|
-
if (isActive) return;
|
|
1270
|
-
if (isPending) return;
|
|
1271
|
-
setPending(true);
|
|
1272
|
-
router.go(href).then(() => {
|
|
1273
|
-
setPending(false);
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
};
|
|
1278
|
-
};
|
|
1279
|
-
|
|
1280
|
-
//#endregion
|
|
1281
|
-
//#region src/hooks/useInject.ts
|
|
1282
|
-
const useInject = (service) => {
|
|
1283
|
-
const alepha = useAlepha();
|
|
1284
|
-
return useMemo(() => alepha.inject(service), []);
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
1287
|
//#endregion
|
|
1288
1288
|
//#region src/hooks/useStore.ts
|
|
1289
1289
|
/**
|
|
@@ -1301,24 +1301,70 @@ const useStore = (key, defaultValue) => {
|
|
|
1301
1301
|
if (ev.key === key) setState(ev.value);
|
|
1302
1302
|
});
|
|
1303
1303
|
}, []);
|
|
1304
|
-
if (!alepha.isBrowser()) {
|
|
1305
|
-
const value = alepha.context.get(key);
|
|
1306
|
-
if (value !== null) return [value, (_) => {}];
|
|
1307
|
-
}
|
|
1308
1304
|
return [state, (value) => {
|
|
1309
1305
|
alepha.state(key, value);
|
|
1310
1306
|
}];
|
|
1311
1307
|
};
|
|
1312
1308
|
|
|
1309
|
+
//#endregion
|
|
1310
|
+
//#region src/hooks/useRouterState.ts
|
|
1311
|
+
const useRouterState = () => {
|
|
1312
|
+
const [state] = useStore("react.router.state");
|
|
1313
|
+
if (!state) throw new AlephaError("Missing react router state");
|
|
1314
|
+
return state;
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
//#endregion
|
|
1318
|
+
//#region src/hooks/useActive.ts
|
|
1319
|
+
const useActive = (args) => {
|
|
1320
|
+
const router = useRouter();
|
|
1321
|
+
const [isPending, setPending] = useState(false);
|
|
1322
|
+
const state = useRouterState();
|
|
1323
|
+
const current = state.url.pathname;
|
|
1324
|
+
const options = typeof args === "string" ? { href: args } : {
|
|
1325
|
+
...args,
|
|
1326
|
+
href: args.href
|
|
1327
|
+
};
|
|
1328
|
+
const href = options.href;
|
|
1329
|
+
let isActive = current === href || current === `${href}/` || `${current}/` === href;
|
|
1330
|
+
if (options.startWith && !isActive) isActive = current.startsWith(href);
|
|
1331
|
+
return {
|
|
1332
|
+
isPending,
|
|
1333
|
+
isActive,
|
|
1334
|
+
anchorProps: {
|
|
1335
|
+
href: router.base(href),
|
|
1336
|
+
onClick: async (ev) => {
|
|
1337
|
+
ev?.stopPropagation();
|
|
1338
|
+
ev?.preventDefault();
|
|
1339
|
+
if (isActive) return;
|
|
1340
|
+
if (isPending) return;
|
|
1341
|
+
setPending(true);
|
|
1342
|
+
try {
|
|
1343
|
+
await router.go(href);
|
|
1344
|
+
} finally {
|
|
1345
|
+
setPending(false);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1313
1352
|
//#endregion
|
|
1314
1353
|
//#region src/hooks/useClient.ts
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1354
|
+
/**
|
|
1355
|
+
* Hook to get a virtual client for the specified scope.
|
|
1356
|
+
*
|
|
1357
|
+
* It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
|
|
1358
|
+
*/
|
|
1359
|
+
const useClient = (scope) => {
|
|
1360
|
+
return useInject(LinkProvider).client(scope);
|
|
1318
1361
|
};
|
|
1319
1362
|
|
|
1320
1363
|
//#endregion
|
|
1321
1364
|
//#region src/hooks/useQueryParams.ts
|
|
1365
|
+
/**
|
|
1366
|
+
* Not well tested. Use with caution.
|
|
1367
|
+
*/
|
|
1322
1368
|
const useQueryParams = (schema, options = {}) => {
|
|
1323
1369
|
const alepha = useAlepha();
|
|
1324
1370
|
const key = options.key ?? "q";
|
|
@@ -1349,29 +1395,17 @@ const decode = (alepha, schema, data) => {
|
|
|
1349
1395
|
}
|
|
1350
1396
|
};
|
|
1351
1397
|
|
|
1352
|
-
//#endregion
|
|
1353
|
-
//#region src/hooks/useRouterState.ts
|
|
1354
|
-
const useRouterState = () => {
|
|
1355
|
-
const router = useContext(RouterContext);
|
|
1356
|
-
const layer = useContext(RouterLayerContext);
|
|
1357
|
-
if (!router || !layer) throw new Error("useRouterState must be used within a RouterContext.Provider");
|
|
1358
|
-
const [state, setState] = useState(router.state);
|
|
1359
|
-
useRouterEvents({ onEnd: ({ state: state$1 }) => setState({ ...state$1 }) });
|
|
1360
|
-
return state;
|
|
1361
|
-
};
|
|
1362
|
-
|
|
1363
1398
|
//#endregion
|
|
1364
1399
|
//#region src/hooks/useSchema.ts
|
|
1365
1400
|
const useSchema = (action) => {
|
|
1366
1401
|
const name = action.name;
|
|
1367
1402
|
const alepha = useAlepha();
|
|
1368
1403
|
const httpClient = useInject(HttpClient);
|
|
1369
|
-
const linkProvider = useInject(LinkProvider);
|
|
1370
1404
|
const [schema, setSchema] = useState(ssrSchemaLoading(alepha, name));
|
|
1371
1405
|
useEffect(() => {
|
|
1372
1406
|
if (!schema.loading) return;
|
|
1373
1407
|
const opts = { cache: true };
|
|
1374
|
-
httpClient.fetch(`${
|
|
1408
|
+
httpClient.fetch(`${LinkProvider.path.apiLinks}/${name}/schema`, {}, opts).then((it) => setSchema(it.data));
|
|
1375
1409
|
}, [name]);
|
|
1376
1410
|
return schema;
|
|
1377
1411
|
};
|
|
@@ -1380,10 +1414,10 @@ const useSchema = (action) => {
|
|
|
1380
1414
|
*/
|
|
1381
1415
|
const ssrSchemaLoading = (alepha, name) => {
|
|
1382
1416
|
if (!alepha.isBrowser()) {
|
|
1383
|
-
const
|
|
1384
|
-
const can =
|
|
1417
|
+
const linkProvider = alepha.inject(LinkProvider);
|
|
1418
|
+
const can = linkProvider.getServerLinks().find((link) => link.name === name);
|
|
1385
1419
|
if (can) {
|
|
1386
|
-
const schema$1 =
|
|
1420
|
+
const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
|
|
1387
1421
|
if (schema$1) {
|
|
1388
1422
|
can.schema = schema$1;
|
|
1389
1423
|
return schema$1;
|
|
@@ -1391,7 +1425,7 @@ const ssrSchemaLoading = (alepha, name) => {
|
|
|
1391
1425
|
}
|
|
1392
1426
|
return { loading: true };
|
|
1393
1427
|
}
|
|
1394
|
-
const schema = alepha.inject(LinkProvider).links
|
|
1428
|
+
const schema = alepha.inject(LinkProvider).links.find((it) => it.name === name)?.schema;
|
|
1395
1429
|
if (schema) return schema;
|
|
1396
1430
|
return { loading: true };
|
|
1397
1431
|
};
|
|
@@ -1413,12 +1447,12 @@ const AlephaReact = $module({
|
|
|
1413
1447
|
descriptors: [$page],
|
|
1414
1448
|
services: [
|
|
1415
1449
|
ReactServerProvider,
|
|
1416
|
-
|
|
1417
|
-
|
|
1450
|
+
ReactPageProvider,
|
|
1451
|
+
ReactRouter
|
|
1418
1452
|
],
|
|
1419
|
-
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(
|
|
1453
|
+
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
|
|
1420
1454
|
});
|
|
1421
1455
|
|
|
1422
1456
|
//#endregion
|
|
1423
|
-
export { $page, AlephaContext, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor,
|
|
1457
|
+
export { $page, AlephaContext, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, ErrorViewer_default as ErrorViewer, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactPageProvider, ReactRouter, ReactServerProvider, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
|
|
1424
1458
|
//# sourceMappingURL=index.js.map
|