@alepha/react 0.9.4 → 0.10.0
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 +101 -7
- package/dist/index.browser.js +290 -86
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +352 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -183
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +318 -180
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -110
- package/dist/index.js.map +1 -1
- package/package.json +17 -14
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +159 -16
- package/src/descriptors/$page.ts +169 -1
- package/src/hooks/useActive.ts +0 -1
- package/src/hooks/useAlepha.ts +1 -1
- package/src/hooks/useQueryParams.ts +9 -5
- package/src/hooks/useRouterEvents.ts +27 -19
- package/src/hooks/useStore.ts +5 -5
- package/src/index.browser.ts +3 -0
- package/src/index.ts +6 -1
- package/src/providers/ReactBrowserProvider.ts +21 -16
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +11 -6
- package/src/providers/ReactPageProvider.ts +45 -1
- package/src/providers/ReactServerProvider.ts +105 -38
- package/src/services/ReactRouter.ts +6 -9
package/src/index.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { $module } from "@alepha/core";
|
|
|
2
2
|
import { AlephaServer, type ServerRequest } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
5
|
-
import {
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { $page, type PageAnimation } from "./descriptors/$page.ts";
|
|
6
7
|
import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
|
|
7
8
|
import {
|
|
8
9
|
ReactPageProvider,
|
|
@@ -37,11 +38,15 @@ declare module "@alepha/core" {
|
|
|
37
38
|
};
|
|
38
39
|
// -----------------------------------------------------------------------------------------------------------------
|
|
39
40
|
"react:browser:render": {
|
|
41
|
+
root: HTMLDivElement;
|
|
42
|
+
element: ReactNode;
|
|
40
43
|
state: ReactRouterState;
|
|
41
44
|
hydration?: ReactHydrationState;
|
|
42
45
|
};
|
|
43
46
|
"react:transition:begin": {
|
|
47
|
+
previous: ReactRouterState;
|
|
44
48
|
state: ReactRouterState;
|
|
49
|
+
animation?: PageAnimation;
|
|
45
50
|
};
|
|
46
51
|
"react:transition:success": {
|
|
47
52
|
state: ReactRouterState;
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
import { DateTimeProvider } from "@alepha/datetime";
|
|
11
11
|
import { $logger } from "@alepha/logger";
|
|
12
12
|
import { LinkProvider } from "@alepha/server-links";
|
|
13
|
-
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
14
13
|
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
15
14
|
import type {
|
|
16
15
|
PreviousLayerData,
|
|
@@ -37,7 +36,6 @@ export class ReactBrowserProvider {
|
|
|
37
36
|
protected readonly alepha = $inject(Alepha);
|
|
38
37
|
protected readonly router = $inject(ReactBrowserRouterProvider);
|
|
39
38
|
protected readonly dateTimeProvider = $inject(DateTimeProvider);
|
|
40
|
-
protected root?: Root;
|
|
41
39
|
|
|
42
40
|
public options: ReactBrowserRendererOptions = {
|
|
43
41
|
scrollRestoration: "top",
|
|
@@ -63,7 +61,7 @@ export class ReactBrowserProvider {
|
|
|
63
61
|
};
|
|
64
62
|
|
|
65
63
|
public get state(): ReactRouterState {
|
|
66
|
-
return this.alepha.state("react.router.state")!;
|
|
64
|
+
return this.alepha.state.get("react.router.state")!;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
/**
|
|
@@ -150,6 +148,7 @@ export class ReactBrowserProvider {
|
|
|
150
148
|
await this.render({
|
|
151
149
|
url,
|
|
152
150
|
previous: options.force ? [] : this.state.layers,
|
|
151
|
+
meta: options.meta,
|
|
153
152
|
});
|
|
154
153
|
|
|
155
154
|
// when redirecting in browser
|
|
@@ -161,9 +160,7 @@ export class ReactBrowserProvider {
|
|
|
161
160
|
this.pushState(url, options.replace);
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
protected async render(
|
|
165
|
-
options: { url?: string; previous?: PreviousLayerData[] } = {},
|
|
166
|
-
): Promise<void> {
|
|
163
|
+
protected async render(options: RouterRenderOptions = {}): Promise<void> {
|
|
167
164
|
const previous = options.previous ?? this.state.layers;
|
|
168
165
|
const url = options.url ?? this.url;
|
|
169
166
|
const start = this.dateTimeProvider.now();
|
|
@@ -180,6 +177,7 @@ export class ReactBrowserProvider {
|
|
|
180
177
|
const redirect = await this.router.transition(
|
|
181
178
|
new URL(`http://localhost${url}`),
|
|
182
179
|
previous,
|
|
180
|
+
options.meta,
|
|
183
181
|
);
|
|
184
182
|
|
|
185
183
|
if (redirect) {
|
|
@@ -215,7 +213,8 @@ export class ReactBrowserProvider {
|
|
|
215
213
|
handler: () => {
|
|
216
214
|
if (
|
|
217
215
|
this.options.scrollRestoration === "top" &&
|
|
218
|
-
typeof window !== "undefined"
|
|
216
|
+
typeof window !== "undefined" &&
|
|
217
|
+
!this.alepha.isTest()
|
|
219
218
|
) {
|
|
220
219
|
this.log.trace("Restoring scroll position to top");
|
|
221
220
|
window.scrollTo(0, 0);
|
|
@@ -233,7 +232,7 @@ export class ReactBrowserProvider {
|
|
|
233
232
|
// low budget, but works for now
|
|
234
233
|
for (const [key, value] of Object.entries(hydration)) {
|
|
235
234
|
if (key !== "layers") {
|
|
236
|
-
this.alepha.state(key as keyof State, value);
|
|
235
|
+
this.alepha.state.set(key as keyof State, value);
|
|
237
236
|
}
|
|
238
237
|
}
|
|
239
238
|
}
|
|
@@ -241,14 +240,13 @@ export class ReactBrowserProvider {
|
|
|
241
240
|
await this.render({ previous });
|
|
242
241
|
|
|
243
242
|
const element = this.router.root(this.state);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
}
|
|
243
|
+
|
|
244
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
245
|
+
element,
|
|
246
|
+
root: this.getRootElement(),
|
|
247
|
+
hydration,
|
|
248
|
+
state: this.state,
|
|
249
|
+
});
|
|
252
250
|
|
|
253
251
|
window.addEventListener("popstate", () => {
|
|
254
252
|
// when you update silently queryParams or hash, skip rendering
|
|
@@ -274,6 +272,7 @@ export interface RouterGoOptions {
|
|
|
274
272
|
match?: TransitionOptions;
|
|
275
273
|
params?: Record<string, string>;
|
|
276
274
|
query?: Record<string, string>;
|
|
275
|
+
meta?: Record<string, any>;
|
|
277
276
|
|
|
278
277
|
/**
|
|
279
278
|
* Recreate the whole page, ignoring the current state.
|
|
@@ -286,3 +285,9 @@ export type ReactHydrationState = {
|
|
|
286
285
|
} & {
|
|
287
286
|
[key: string]: any;
|
|
288
287
|
};
|
|
288
|
+
|
|
289
|
+
export interface RouterRenderOptions {
|
|
290
|
+
url?: string;
|
|
291
|
+
previous?: PreviousLayerData[];
|
|
292
|
+
meta?: Record<string, any>;
|
|
293
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { $hook } from "@alepha/core";
|
|
2
|
+
import { $logger } from "@alepha/logger";
|
|
3
|
+
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
4
|
+
|
|
5
|
+
export class ReactBrowserRendererProvider {
|
|
6
|
+
protected readonly log = $logger();
|
|
7
|
+
protected root?: Root;
|
|
8
|
+
|
|
9
|
+
protected readonly onBrowserRender = $hook({
|
|
10
|
+
on: "react:browser:render",
|
|
11
|
+
handler: async ({ hydration, root, element }) => {
|
|
12
|
+
if (hydration?.layers) {
|
|
13
|
+
this.root = hydrateRoot(root, element);
|
|
14
|
+
this.log.info("Hydrated root element");
|
|
15
|
+
} else {
|
|
16
|
+
this.root ??= createRoot(root);
|
|
17
|
+
this.root.render(element);
|
|
18
|
+
this.log.info("Created root element");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -43,6 +43,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
43
43
|
public async transition(
|
|
44
44
|
url: URL,
|
|
45
45
|
previous: PreviousLayerData[] = [],
|
|
46
|
+
meta = {},
|
|
46
47
|
): Promise<string | void> {
|
|
47
48
|
const { pathname, search } = url;
|
|
48
49
|
|
|
@@ -52,11 +53,15 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
52
53
|
params: {},
|
|
53
54
|
layers: [],
|
|
54
55
|
onError: () => null,
|
|
56
|
+
meta,
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
const state = entry as ReactRouterState;
|
|
58
60
|
|
|
59
|
-
await this.alepha.emit("react:transition:begin", {
|
|
61
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
62
|
+
previous: this.alepha.state.get("react.router.state")!,
|
|
63
|
+
state,
|
|
64
|
+
});
|
|
60
65
|
|
|
61
66
|
try {
|
|
62
67
|
const { route, params } = this.match(pathname);
|
|
@@ -91,7 +96,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
91
96
|
});
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
99
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
95
100
|
} catch (e) {
|
|
96
101
|
this.log.error("Transition has failed", e);
|
|
97
102
|
state.layers = [
|
|
@@ -103,7 +108,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
103
108
|
},
|
|
104
109
|
];
|
|
105
110
|
|
|
106
|
-
await this.alepha.emit("react:transition:error", {
|
|
111
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
107
112
|
error: e as Error,
|
|
108
113
|
state,
|
|
109
114
|
});
|
|
@@ -119,11 +124,11 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
|
|
127
|
+
this.alepha.state.set("react.router.state", state);
|
|
128
|
+
|
|
129
|
+
await this.alepha.events.emit("react:transition:end", {
|
|
123
130
|
state,
|
|
124
131
|
});
|
|
125
|
-
|
|
126
|
-
this.alepha.state("react.router.state", state);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
public root(state: ReactRouterState): ReactNode {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
$env,
|
|
3
|
+
$hook,
|
|
4
|
+
$inject,
|
|
5
|
+
Alepha,
|
|
6
|
+
type Static,
|
|
7
|
+
type TSchema,
|
|
8
|
+
t,
|
|
9
|
+
} from "@alepha/core";
|
|
2
10
|
import { $logger } from "@alepha/logger";
|
|
3
11
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
4
12
|
import ClientOnly from "../components/ClientOnly.tsx";
|
|
@@ -99,6 +107,30 @@ export class ReactPageProvider {
|
|
|
99
107
|
return root;
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
protected convertStringObjectToObject = (
|
|
111
|
+
schema?: TSchema,
|
|
112
|
+
value?: any,
|
|
113
|
+
): any => {
|
|
114
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
115
|
+
for (const key in schema.properties) {
|
|
116
|
+
if (
|
|
117
|
+
t.schema.isObject(schema.properties[key]) &&
|
|
118
|
+
typeof value[key] === "string"
|
|
119
|
+
) {
|
|
120
|
+
try {
|
|
121
|
+
value[key] = this.alepha.parse(
|
|
122
|
+
schema.properties[key],
|
|
123
|
+
decodeURIComponent(value[key]),
|
|
124
|
+
);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
|
|
102
134
|
/**
|
|
103
135
|
* Create a new RouterState based on a given route and request.
|
|
104
136
|
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
@@ -126,6 +158,7 @@ export class ReactPageProvider {
|
|
|
126
158
|
const config: Record<string, any> = {};
|
|
127
159
|
|
|
128
160
|
try {
|
|
161
|
+
this.convertStringObjectToObject(route.schema?.query, state.query);
|
|
129
162
|
config.query = route.schema?.query
|
|
130
163
|
? this.alepha.parse(route.schema.query, state.query)
|
|
131
164
|
: {};
|
|
@@ -331,6 +364,12 @@ export class ReactPageProvider {
|
|
|
331
364
|
page: PageRoute,
|
|
332
365
|
props: Record<string, any>,
|
|
333
366
|
): Promise<ReactNode> {
|
|
367
|
+
if (page.lazy && page.component) {
|
|
368
|
+
this.log.warn(
|
|
369
|
+
`Page ${page.name} has both lazy and component options, lazy will be used`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
334
373
|
if (page.lazy) {
|
|
335
374
|
const component = await page.lazy(); // load component
|
|
336
375
|
return createElement(component.default, props);
|
|
@@ -605,6 +644,11 @@ export interface ReactRouterState {
|
|
|
605
644
|
* Query parameters extracted from the URL for the current page.
|
|
606
645
|
*/
|
|
607
646
|
query: Record<string, string>;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Optional meta information associated with the current page.
|
|
650
|
+
*/
|
|
651
|
+
meta: Record<string, any>;
|
|
608
652
|
}
|
|
609
653
|
|
|
610
654
|
export interface RouterStackItem {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { $logger } from "@alepha/logger";
|
|
13
13
|
import {
|
|
14
14
|
type ServerHandler,
|
|
15
|
+
ServerProvider,
|
|
15
16
|
ServerRouterProvider,
|
|
16
17
|
ServerTimingProvider,
|
|
17
18
|
} from "@alepha/server";
|
|
@@ -21,6 +22,7 @@ import { renderToString } from "react-dom/server";
|
|
|
21
22
|
import {
|
|
22
23
|
$page,
|
|
23
24
|
type PageDescriptorRenderOptions,
|
|
25
|
+
type PageDescriptorRenderResult,
|
|
24
26
|
} from "../descriptors/$page.ts";
|
|
25
27
|
import { Redirection } from "../errors/Redirection.ts";
|
|
26
28
|
import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
|
|
@@ -53,6 +55,7 @@ export class ReactServerProvider {
|
|
|
53
55
|
protected readonly log = $logger();
|
|
54
56
|
protected readonly alepha = $inject(Alepha);
|
|
55
57
|
protected readonly pageApi = $inject(ReactPageProvider);
|
|
58
|
+
protected readonly serverProvider = $inject(ServerProvider);
|
|
56
59
|
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
57
60
|
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
58
61
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
@@ -61,6 +64,7 @@ export class ReactServerProvider {
|
|
|
61
64
|
`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
|
|
62
65
|
"is",
|
|
63
66
|
);
|
|
67
|
+
protected preprocessedTemplate: PreprocessedTemplate | null = null;
|
|
64
68
|
|
|
65
69
|
public readonly onConfigure = $hook({
|
|
66
70
|
on: "configure",
|
|
@@ -70,10 +74,23 @@ export class ReactServerProvider {
|
|
|
70
74
|
const ssrEnabled =
|
|
71
75
|
pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
72
76
|
|
|
73
|
-
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
77
|
+
this.alepha.state.set("react.server.ssr", ssrEnabled);
|
|
74
78
|
|
|
75
79
|
for (const page of pages) {
|
|
76
80
|
page.render = this.createRenderFunction(page.name);
|
|
81
|
+
page.fetch = async (options) => {
|
|
82
|
+
const response = await fetch(
|
|
83
|
+
`${this.serverProvider.hostname}/${page.pathname(options)}`,
|
|
84
|
+
);
|
|
85
|
+
const html = await response.text();
|
|
86
|
+
if (options?.html) return { html, response };
|
|
87
|
+
// take only text inside the root div
|
|
88
|
+
const match = html.match(this.ROOT_DIV_REGEX);
|
|
89
|
+
if (match) {
|
|
90
|
+
return { html: match[3], response };
|
|
91
|
+
}
|
|
92
|
+
throw new AlephaError("Invalid HTML response");
|
|
93
|
+
};
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
// development mode
|
|
@@ -134,6 +151,12 @@ export class ReactServerProvider {
|
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
protected async registerPages(templateLoader: TemplateLoader) {
|
|
154
|
+
// Preprocess template once
|
|
155
|
+
const template = await templateLoader();
|
|
156
|
+
if (template) {
|
|
157
|
+
this.preprocessedTemplate = this.preprocessTemplate(template);
|
|
158
|
+
}
|
|
159
|
+
|
|
137
160
|
for (const page of this.pageApi.getPages()) {
|
|
138
161
|
if (page.children?.length) {
|
|
139
162
|
continue;
|
|
@@ -194,7 +217,9 @@ export class ReactServerProvider {
|
|
|
194
217
|
* For testing purposes, creates a render function that can be used.
|
|
195
218
|
*/
|
|
196
219
|
protected createRenderFunction(name: string, withIndex = false) {
|
|
197
|
-
return async (
|
|
220
|
+
return async (
|
|
221
|
+
options: PageDescriptorRenderOptions = {},
|
|
222
|
+
): Promise<PageDescriptorRenderResult> => {
|
|
198
223
|
const page = this.pageApi.page(name);
|
|
199
224
|
const url = new URL(this.pageApi.url(name, options));
|
|
200
225
|
|
|
@@ -204,6 +229,7 @@ export class ReactServerProvider {
|
|
|
204
229
|
query: options.query ?? {},
|
|
205
230
|
onError: () => null,
|
|
206
231
|
layers: [],
|
|
232
|
+
meta: {},
|
|
207
233
|
};
|
|
208
234
|
|
|
209
235
|
const state = entry as ReactRouterState;
|
|
@@ -212,7 +238,7 @@ export class ReactServerProvider {
|
|
|
212
238
|
url,
|
|
213
239
|
});
|
|
214
240
|
|
|
215
|
-
await this.alepha.emit("react:server:render:begin", {
|
|
241
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
216
242
|
state,
|
|
217
243
|
});
|
|
218
244
|
|
|
@@ -222,11 +248,11 @@ export class ReactServerProvider {
|
|
|
222
248
|
);
|
|
223
249
|
|
|
224
250
|
if (redirect) {
|
|
225
|
-
|
|
251
|
+
return { state, html: "", redirect };
|
|
226
252
|
}
|
|
227
253
|
|
|
228
254
|
if (!withIndex && !options.html) {
|
|
229
|
-
this.alepha.state("react.router.state", state);
|
|
255
|
+
this.alepha.state.set("react.router.state", state);
|
|
230
256
|
|
|
231
257
|
return {
|
|
232
258
|
state,
|
|
@@ -234,14 +260,11 @@ export class ReactServerProvider {
|
|
|
234
260
|
};
|
|
235
261
|
}
|
|
236
262
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
state,
|
|
240
|
-
options.hydration,
|
|
241
|
-
);
|
|
263
|
+
const template = this.template ?? "";
|
|
264
|
+
const html = this.renderToHtml(template, state, options.hydration);
|
|
242
265
|
|
|
243
266
|
if (html instanceof Redirection) {
|
|
244
|
-
|
|
267
|
+
return { state, html: "", redirect };
|
|
245
268
|
}
|
|
246
269
|
|
|
247
270
|
const result = {
|
|
@@ -249,7 +272,7 @@ export class ReactServerProvider {
|
|
|
249
272
|
html,
|
|
250
273
|
};
|
|
251
274
|
|
|
252
|
-
await this.alepha.emit("react:server:render:end", result);
|
|
275
|
+
await this.alepha.events.emit("react:server:render:end", result);
|
|
253
276
|
|
|
254
277
|
return result;
|
|
255
278
|
};
|
|
@@ -281,7 +304,7 @@ export class ReactServerProvider {
|
|
|
281
304
|
const state = entry as ReactRouterState;
|
|
282
305
|
|
|
283
306
|
if (this.alepha.has(ServerLinksProvider)) {
|
|
284
|
-
this.alepha.state(
|
|
307
|
+
this.alepha.state.set(
|
|
285
308
|
"api",
|
|
286
309
|
await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
287
310
|
user: (serverRequest as any).user, // TODO: fix type
|
|
@@ -312,7 +335,7 @@ export class ReactServerProvider {
|
|
|
312
335
|
// return;
|
|
313
336
|
// }
|
|
314
337
|
|
|
315
|
-
await this.alepha.emit("react:server:render:begin", {
|
|
338
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
316
339
|
request: serverRequest,
|
|
317
340
|
state,
|
|
318
341
|
});
|
|
@@ -352,7 +375,7 @@ export class ReactServerProvider {
|
|
|
352
375
|
html,
|
|
353
376
|
};
|
|
354
377
|
|
|
355
|
-
await this.alepha.emit("react:server:render:end", event);
|
|
378
|
+
await this.alepha.events.emit("react:server:render:end", event);
|
|
356
379
|
|
|
357
380
|
route.onServerResponse?.(serverRequest);
|
|
358
381
|
|
|
@@ -372,7 +395,7 @@ export class ReactServerProvider {
|
|
|
372
395
|
const element = this.pageApi.root(state);
|
|
373
396
|
|
|
374
397
|
// attach react router state to the http request context
|
|
375
|
-
this.alepha.state("react.router.state", state);
|
|
398
|
+
this.alepha.state.set("react.router.state", state);
|
|
376
399
|
|
|
377
400
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
378
401
|
let app = "";
|
|
@@ -433,36 +456,80 @@ export class ReactServerProvider {
|
|
|
433
456
|
return response.html;
|
|
434
457
|
}
|
|
435
458
|
|
|
459
|
+
protected preprocessTemplate(template: string): PreprocessedTemplate {
|
|
460
|
+
// Find the body close tag for script injection
|
|
461
|
+
const bodyCloseMatch = template.match(/<\/body>/i);
|
|
462
|
+
const bodyCloseIndex = bodyCloseMatch?.index ?? template.length;
|
|
463
|
+
|
|
464
|
+
const beforeScript = template.substring(0, bodyCloseIndex);
|
|
465
|
+
const afterScript = template.substring(bodyCloseIndex);
|
|
466
|
+
|
|
467
|
+
// Check if there's an existing root div
|
|
468
|
+
const rootDivMatch = beforeScript.match(this.ROOT_DIV_REGEX);
|
|
469
|
+
|
|
470
|
+
if (rootDivMatch) {
|
|
471
|
+
// Split around the existing root div content
|
|
472
|
+
const beforeDiv = beforeScript.substring(0, rootDivMatch.index!);
|
|
473
|
+
const afterDivStart = rootDivMatch.index! + rootDivMatch[0].length;
|
|
474
|
+
const afterDiv = beforeScript.substring(afterDivStart);
|
|
475
|
+
|
|
476
|
+
const beforeApp = `${beforeDiv}<div${rootDivMatch[1]} id="${this.env.REACT_ROOT_ID}"${rootDivMatch[2]}>`;
|
|
477
|
+
const afterApp = `</div>${afterDiv}`;
|
|
478
|
+
|
|
479
|
+
return { beforeApp, afterApp, beforeScript: "", afterScript };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// No existing root div, find body tag to inject new div
|
|
483
|
+
const bodyMatch = beforeScript.match(/<body([^>]*)>/i);
|
|
484
|
+
if (bodyMatch) {
|
|
485
|
+
const beforeBody = beforeScript.substring(
|
|
486
|
+
0,
|
|
487
|
+
bodyMatch.index! + bodyMatch[0].length,
|
|
488
|
+
);
|
|
489
|
+
const afterBody = beforeScript.substring(
|
|
490
|
+
bodyMatch.index! + bodyMatch[0].length,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const beforeApp = `${beforeBody}<div id="${this.env.REACT_ROOT_ID}">`;
|
|
494
|
+
const afterApp = `</div>${afterBody}`;
|
|
495
|
+
|
|
496
|
+
return { beforeApp, afterApp, beforeScript: "", afterScript };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Fallback: no body tag found, just wrap everything
|
|
500
|
+
return {
|
|
501
|
+
beforeApp: `<div id="${this.env.REACT_ROOT_ID}">`,
|
|
502
|
+
afterApp: `</div>`,
|
|
503
|
+
beforeScript,
|
|
504
|
+
afterScript,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
436
508
|
protected fillTemplate(
|
|
437
509
|
response: { html: string },
|
|
438
510
|
app: string,
|
|
439
511
|
script: string,
|
|
440
512
|
) {
|
|
441
|
-
if (this.
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
this.ROOT_DIV_REGEX,
|
|
445
|
-
(_match, beforeId, afterId) => {
|
|
446
|
-
return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
|
|
447
|
-
},
|
|
448
|
-
);
|
|
449
|
-
} else {
|
|
450
|
-
const bodyOpenTag = /<body([^>]*)>/i;
|
|
451
|
-
if (bodyOpenTag.test(response.html)) {
|
|
452
|
-
response.html = response.html.replace(bodyOpenTag, (match) => {
|
|
453
|
-
return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
|
|
454
|
-
});
|
|
455
|
-
}
|
|
513
|
+
if (!this.preprocessedTemplate) {
|
|
514
|
+
// Fallback to old logic if preprocessing failed
|
|
515
|
+
this.preprocessedTemplate = this.preprocessTemplate(response.html);
|
|
456
516
|
}
|
|
457
517
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
518
|
+
// Pure concatenation - no regex replacements needed
|
|
519
|
+
response.html =
|
|
520
|
+
this.preprocessedTemplate.beforeApp +
|
|
521
|
+
app +
|
|
522
|
+
this.preprocessedTemplate.afterApp +
|
|
523
|
+
script +
|
|
524
|
+
this.preprocessedTemplate.afterScript;
|
|
465
525
|
}
|
|
466
526
|
}
|
|
467
527
|
|
|
468
528
|
type TemplateLoader = () => Promise<string | undefined>;
|
|
529
|
+
|
|
530
|
+
interface PreprocessedTemplate {
|
|
531
|
+
beforeApp: string;
|
|
532
|
+
afterApp: string;
|
|
533
|
+
beforeScript: string;
|
|
534
|
+
afterScript: string;
|
|
535
|
+
}
|
|
@@ -15,7 +15,7 @@ export class ReactRouter<T extends object> {
|
|
|
15
15
|
protected readonly pageApi = $inject(ReactPageProvider);
|
|
16
16
|
|
|
17
17
|
public get state(): ReactRouterState {
|
|
18
|
-
return this.alepha.state("react.router.state")!;
|
|
18
|
+
return this.alepha.state.get("react.router.state")!;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
public get pages() {
|
|
@@ -33,8 +33,8 @@ export class ReactRouter<T extends object> {
|
|
|
33
33
|
public path(
|
|
34
34
|
name: keyof VirtualRouter<T>,
|
|
35
35
|
config: {
|
|
36
|
-
params?: Record<string,
|
|
37
|
-
query?: Record<string,
|
|
36
|
+
params?: Record<string, any>;
|
|
37
|
+
query?: Record<string, any>;
|
|
38
38
|
} = {},
|
|
39
39
|
): string {
|
|
40
40
|
return this.pageApi.pathname(name as string, {
|
|
@@ -116,17 +116,14 @@ export class ReactRouter<T extends object> {
|
|
|
116
116
|
await this.browser?.go(path as string, options);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
public anchor(
|
|
120
|
-
path: string,
|
|
121
|
-
options?: { params?: Record<string, any> },
|
|
122
|
-
): AnchorProps;
|
|
119
|
+
public anchor(path: string, options?: RouterGoOptions): AnchorProps;
|
|
123
120
|
public anchor(
|
|
124
121
|
path: keyof VirtualRouter<T>,
|
|
125
|
-
options?:
|
|
122
|
+
options?: RouterGoOptions,
|
|
126
123
|
): AnchorProps;
|
|
127
124
|
public anchor(
|
|
128
125
|
path: string | keyof VirtualRouter<T>,
|
|
129
|
-
options:
|
|
126
|
+
options: RouterGoOptions = {},
|
|
130
127
|
): AnchorProps {
|
|
131
128
|
let href = path as string;
|
|
132
129
|
|