@alepha/react 0.9.5 → 0.10.1
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 +85 -3
- package/dist/index.browser.js +110 -25
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +123 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -45
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
- package/src/descriptors/$page.ts +85 -0
- package/src/hooks/useAlepha.ts +1 -1
- package/src/hooks/useQueryParams.ts +9 -5
- package/src/hooks/useRouterEvents.ts +4 -4
- package/src/hooks/useStore.ts +5 -5
- package/src/providers/ReactBrowserProvider.ts +3 -3
- package/src/providers/ReactBrowserRouterProvider.ts +6 -6
- package/src/providers/ReactPageProvider.ts +2 -3
- package/src/providers/ReactServerProvider.ts +83 -35
- package/src/services/ReactRouter.ts +1 -1
- package/dist/index.cjs +0 -1631
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -811
- package/dist/index.d.cts.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alepha/react",
|
|
3
3
|
"description": "Build server-side rendered (SSR) or single-page React applications.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -17,27 +17,28 @@
|
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@alepha/core": "0.
|
|
21
|
-
"@alepha/datetime": "0.
|
|
22
|
-
"@alepha/logger": "0.
|
|
23
|
-
"@alepha/router": "0.
|
|
24
|
-
"@alepha/server": "0.
|
|
25
|
-
"@alepha/server-cache": "0.
|
|
26
|
-
"@alepha/server-links": "0.
|
|
27
|
-
"@alepha/server-static": "0.
|
|
28
|
-
"react-dom": "^19.1.1"
|
|
20
|
+
"@alepha/core": "0.10.1",
|
|
21
|
+
"@alepha/datetime": "0.10.1",
|
|
22
|
+
"@alepha/logger": "0.10.1",
|
|
23
|
+
"@alepha/router": "0.10.1",
|
|
24
|
+
"@alepha/server": "0.10.1",
|
|
25
|
+
"@alepha/server-cache": "0.10.1",
|
|
26
|
+
"@alepha/server-links": "0.10.1",
|
|
27
|
+
"@alepha/server-static": "0.10.1"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
30
|
"@biomejs/biome": "^2.2.4",
|
|
32
|
-
"@types/react": "^19.1.
|
|
31
|
+
"@types/react": "^19.1.15",
|
|
33
32
|
"@types/react-dom": "^19.1.9",
|
|
34
33
|
"react": "^19.1.1",
|
|
35
|
-
"
|
|
34
|
+
"react-dom": "^19.1.1",
|
|
35
|
+
"tsdown": "^0.15.5",
|
|
36
36
|
"typescript": "^5.9.2",
|
|
37
37
|
"vitest": "^3.2.4"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"react": "
|
|
40
|
+
"react": "*",
|
|
41
|
+
"react-dom": "*"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"check": "tsc",
|
package/src/descriptors/$page.ts
CHANGED
|
@@ -16,6 +16,91 @@ import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Main descriptor for defining a React route in the application.
|
|
19
|
+
*
|
|
20
|
+
* The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
|
|
21
|
+
* It provides a declarative way to define pages with powerful features:
|
|
22
|
+
*
|
|
23
|
+
* **Routing & Navigation**
|
|
24
|
+
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
25
|
+
* - Nested routing with parent-child relationships
|
|
26
|
+
* - Type-safe URL parameter and query string validation
|
|
27
|
+
*
|
|
28
|
+
* **Data Loading**
|
|
29
|
+
* - Server-side data fetching with the `resolve` function
|
|
30
|
+
* - Automatic serialization and hydration for SSR
|
|
31
|
+
* - Access to request context, URL params, and parent data
|
|
32
|
+
*
|
|
33
|
+
* **Component Loading**
|
|
34
|
+
* - Direct component rendering or lazy loading for code splitting
|
|
35
|
+
* - Client-only rendering when browser APIs are needed
|
|
36
|
+
* - Automatic fallback handling during hydration
|
|
37
|
+
*
|
|
38
|
+
* **Performance Optimization**
|
|
39
|
+
* - Static generation for pre-rendered pages at build time
|
|
40
|
+
* - Server-side caching with configurable TTL and providers
|
|
41
|
+
* - Code splitting through lazy component loading
|
|
42
|
+
*
|
|
43
|
+
* **Error Handling**
|
|
44
|
+
* - Custom error handlers with support for redirects
|
|
45
|
+
* - Hierarchical error handling (child → parent)
|
|
46
|
+
* - HTTP status code handling (404, 401, etc.)
|
|
47
|
+
*
|
|
48
|
+
* **Page Animations**
|
|
49
|
+
* - CSS-based enter/exit animations
|
|
50
|
+
* - Dynamic animations based on page state
|
|
51
|
+
* - Custom timing and easing functions
|
|
52
|
+
*
|
|
53
|
+
* **Lifecycle Management**
|
|
54
|
+
* - Server response hooks for headers and status codes
|
|
55
|
+
* - Page leave handlers for cleanup (browser only)
|
|
56
|
+
* - Permission-based access control
|
|
57
|
+
*
|
|
58
|
+
* @example Simple page with data fetching
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const userProfile = $page({
|
|
61
|
+
* path: "/users/:id",
|
|
62
|
+
* schema: {
|
|
63
|
+
* params: t.object({ id: t.int() }),
|
|
64
|
+
* query: t.object({ tab: t.optional(t.string()) })
|
|
65
|
+
* },
|
|
66
|
+
* resolve: async ({ params }) => {
|
|
67
|
+
* const user = await userApi.getUser(params.id);
|
|
68
|
+
* return { user };
|
|
69
|
+
* },
|
|
70
|
+
* lazy: () => import("./UserProfile.tsx")
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example Nested routing with error handling
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const projectSection = $page({
|
|
77
|
+
* path: "/projects/:id",
|
|
78
|
+
* children: () => [projectBoard, projectSettings],
|
|
79
|
+
* resolve: async ({ params }) => {
|
|
80
|
+
* const project = await projectApi.get(params.id);
|
|
81
|
+
* return { project };
|
|
82
|
+
* },
|
|
83
|
+
* errorHandler: (error) => {
|
|
84
|
+
* if (HttpError.is(error, 404)) {
|
|
85
|
+
* return <ProjectNotFound />;
|
|
86
|
+
* }
|
|
87
|
+
* }
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @example Static generation with caching
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const blogPost = $page({
|
|
94
|
+
* path: "/blog/:slug",
|
|
95
|
+
* static: {
|
|
96
|
+
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
97
|
+
* },
|
|
98
|
+
* resolve: async ({ params }) => {
|
|
99
|
+
* const post = await loadPost(params.slug);
|
|
100
|
+
* return { post };
|
|
101
|
+
* }
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
19
104
|
*/
|
|
20
105
|
export const $page = <
|
|
21
106
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
package/src/hooks/useAlepha.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
|
11
11
|
*
|
|
12
12
|
* - alepha.state() for state management
|
|
13
13
|
* - alepha.inject() for dependency injection
|
|
14
|
-
* - alepha.emit() for event handling
|
|
14
|
+
* - alepha.events.emit() for event handling
|
|
15
15
|
* etc...
|
|
16
16
|
*/
|
|
17
17
|
export const useAlepha = (): Alepha => {
|
|
@@ -9,14 +9,14 @@ import { useRouter } from "./useRouter.ts";
|
|
|
9
9
|
export const useQueryParams = <T extends TObject>(
|
|
10
10
|
schema: T,
|
|
11
11
|
options: UseQueryParamsHookOptions = {},
|
|
12
|
-
): [Static<T
|
|
12
|
+
): [Partial<Static<T>>, (data: Static<T>) => void] => {
|
|
13
13
|
const alepha = useAlepha();
|
|
14
14
|
|
|
15
15
|
const key = options.key ?? "q";
|
|
16
16
|
const router = useRouter();
|
|
17
17
|
const querystring = router.query[key];
|
|
18
18
|
|
|
19
|
-
const [queryParams, setQueryParams] = useState(
|
|
19
|
+
const [queryParams = {}, setQueryParams] = useState<Static<T> | undefined>(
|
|
20
20
|
decode(alepha, schema, router.query[key]),
|
|
21
21
|
);
|
|
22
22
|
|
|
@@ -47,10 +47,14 @@ const encode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
|
47
47
|
return btoa(JSON.stringify(alepha.parse(schema, data)));
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const decode =
|
|
50
|
+
const decode = <T extends TObject>(
|
|
51
|
+
alepha: Alepha,
|
|
52
|
+
schema: T,
|
|
53
|
+
data: any,
|
|
54
|
+
): Static<T> | undefined => {
|
|
51
55
|
try {
|
|
52
56
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
53
|
-
} catch
|
|
54
|
-
return
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
55
59
|
}
|
|
56
60
|
};
|
|
@@ -42,19 +42,19 @@ export const useRouterEvents = (
|
|
|
42
42
|
const onSuccess = opts.onSuccess;
|
|
43
43
|
|
|
44
44
|
if (onBegin) {
|
|
45
|
-
subs.push(alepha.on("react:transition:begin", cb(onBegin)));
|
|
45
|
+
subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
if (onEnd) {
|
|
49
|
-
subs.push(alepha.on("react:transition:end", cb(onEnd)));
|
|
49
|
+
subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (onError) {
|
|
53
|
-
subs.push(alepha.on("react:transition:error", cb(onError)));
|
|
53
|
+
subs.push(alepha.events.on("react:transition:error", cb(onError)));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if (onSuccess) {
|
|
57
|
-
subs.push(alepha.on("react:transition:success", cb(onSuccess)));
|
|
57
|
+
subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
return () => {
|
package/src/hooks/useStore.ts
CHANGED
|
@@ -12,19 +12,19 @@ export const useStore = <Key extends keyof State>(
|
|
|
12
12
|
const alepha = useAlepha();
|
|
13
13
|
|
|
14
14
|
useMemo(() => {
|
|
15
|
-
if (defaultValue != null && alepha.state(key) == null) {
|
|
16
|
-
alepha.state(key, defaultValue);
|
|
15
|
+
if (defaultValue != null && alepha.state.get(key) == null) {
|
|
16
|
+
alepha.state.set(key, defaultValue);
|
|
17
17
|
}
|
|
18
18
|
}, [defaultValue]);
|
|
19
19
|
|
|
20
|
-
const [state, setState] = useState(alepha.state(key));
|
|
20
|
+
const [state, setState] = useState(alepha.state.get(key));
|
|
21
21
|
|
|
22
22
|
useEffect(() => {
|
|
23
23
|
if (!alepha.isBrowser()) {
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
return alepha.on("state:mutate", (ev) => {
|
|
27
|
+
return alepha.events.on("state:mutate", (ev) => {
|
|
28
28
|
if (ev.key === key) {
|
|
29
29
|
setState(ev.value);
|
|
30
30
|
}
|
|
@@ -34,7 +34,7 @@ export const useStore = <Key extends keyof State>(
|
|
|
34
34
|
return [
|
|
35
35
|
state,
|
|
36
36
|
(value: State[Key]) => {
|
|
37
|
-
alepha.state(key, value);
|
|
37
|
+
alepha.state.set(key, value);
|
|
38
38
|
},
|
|
39
39
|
] as const;
|
|
40
40
|
};
|
|
@@ -61,7 +61,7 @@ export class ReactBrowserProvider {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
public get state(): ReactRouterState {
|
|
64
|
-
return this.alepha.state("react.router.state")!;
|
|
64
|
+
return this.alepha.state.get("react.router.state")!;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -232,7 +232,7 @@ export class ReactBrowserProvider {
|
|
|
232
232
|
// low budget, but works for now
|
|
233
233
|
for (const [key, value] of Object.entries(hydration)) {
|
|
234
234
|
if (key !== "layers") {
|
|
235
|
-
this.alepha.state(key as keyof State, value);
|
|
235
|
+
this.alepha.state.set(key as keyof State, value);
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
}
|
|
@@ -241,7 +241,7 @@ export class ReactBrowserProvider {
|
|
|
241
241
|
|
|
242
242
|
const element = this.router.root(this.state);
|
|
243
243
|
|
|
244
|
-
await this.alepha.emit("react:browser:render", {
|
|
244
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
245
245
|
element,
|
|
246
246
|
root: this.getRootElement(),
|
|
247
247
|
hydration,
|
|
@@ -58,8 +58,8 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
58
58
|
|
|
59
59
|
const state = entry as ReactRouterState;
|
|
60
60
|
|
|
61
|
-
await this.alepha.emit("react:transition:begin", {
|
|
62
|
-
previous: this.alepha.state("react.router.state")!,
|
|
61
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
62
|
+
previous: this.alepha.state.get("react.router.state")!,
|
|
63
63
|
state,
|
|
64
64
|
});
|
|
65
65
|
|
|
@@ -96,7 +96,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
99
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
100
100
|
} catch (e) {
|
|
101
101
|
this.log.error("Transition has failed", e);
|
|
102
102
|
state.layers = [
|
|
@@ -108,7 +108,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
108
108
|
},
|
|
109
109
|
];
|
|
110
110
|
|
|
111
|
-
await this.alepha.emit("react:transition:error", {
|
|
111
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
112
112
|
error: e as Error,
|
|
113
113
|
state,
|
|
114
114
|
});
|
|
@@ -124,9 +124,9 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
this.alepha.state("react.router.state", state);
|
|
127
|
+
this.alepha.state.set("react.router.state", state);
|
|
128
128
|
|
|
129
|
-
await this.alepha.emit("react:transition:end", {
|
|
129
|
+
await this.alepha.events.emit("react:transition:end", {
|
|
130
130
|
state,
|
|
131
131
|
});
|
|
132
132
|
}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
Alepha,
|
|
6
6
|
type Static,
|
|
7
7
|
type TSchema,
|
|
8
|
-
TypeGuard,
|
|
9
8
|
t,
|
|
10
9
|
} from "@alepha/core";
|
|
11
10
|
import { $logger } from "@alepha/logger";
|
|
@@ -112,10 +111,10 @@ export class ReactPageProvider {
|
|
|
112
111
|
schema?: TSchema,
|
|
113
112
|
value?: any,
|
|
114
113
|
): any => {
|
|
115
|
-
if (
|
|
114
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
116
115
|
for (const key in schema.properties) {
|
|
117
116
|
if (
|
|
118
|
-
|
|
117
|
+
t.schema.isObject(schema.properties[key]) &&
|
|
119
118
|
typeof value[key] === "string"
|
|
120
119
|
) {
|
|
121
120
|
try {
|
|
@@ -64,6 +64,7 @@ export class ReactServerProvider {
|
|
|
64
64
|
`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
|
|
65
65
|
"is",
|
|
66
66
|
);
|
|
67
|
+
protected preprocessedTemplate: PreprocessedTemplate | null = null;
|
|
67
68
|
|
|
68
69
|
public readonly onConfigure = $hook({
|
|
69
70
|
on: "configure",
|
|
@@ -73,7 +74,7 @@ export class ReactServerProvider {
|
|
|
73
74
|
const ssrEnabled =
|
|
74
75
|
pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
75
76
|
|
|
76
|
-
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
77
|
+
this.alepha.state.set("react.server.ssr", ssrEnabled);
|
|
77
78
|
|
|
78
79
|
for (const page of pages) {
|
|
79
80
|
page.render = this.createRenderFunction(page.name);
|
|
@@ -150,6 +151,12 @@ export class ReactServerProvider {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
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
|
+
|
|
153
160
|
for (const page of this.pageApi.getPages()) {
|
|
154
161
|
if (page.children?.length) {
|
|
155
162
|
continue;
|
|
@@ -231,7 +238,7 @@ export class ReactServerProvider {
|
|
|
231
238
|
url,
|
|
232
239
|
});
|
|
233
240
|
|
|
234
|
-
await this.alepha.emit("react:server:render:begin", {
|
|
241
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
235
242
|
state,
|
|
236
243
|
});
|
|
237
244
|
|
|
@@ -245,7 +252,7 @@ export class ReactServerProvider {
|
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
if (!withIndex && !options.html) {
|
|
248
|
-
this.alepha.state("react.router.state", state);
|
|
255
|
+
this.alepha.state.set("react.router.state", state);
|
|
249
256
|
|
|
250
257
|
return {
|
|
251
258
|
state,
|
|
@@ -253,11 +260,8 @@ export class ReactServerProvider {
|
|
|
253
260
|
};
|
|
254
261
|
}
|
|
255
262
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
state,
|
|
259
|
-
options.hydration,
|
|
260
|
-
);
|
|
263
|
+
const template = this.template ?? "";
|
|
264
|
+
const html = this.renderToHtml(template, state, options.hydration);
|
|
261
265
|
|
|
262
266
|
if (html instanceof Redirection) {
|
|
263
267
|
return { state, html: "", redirect };
|
|
@@ -268,7 +272,7 @@ export class ReactServerProvider {
|
|
|
268
272
|
html,
|
|
269
273
|
};
|
|
270
274
|
|
|
271
|
-
await this.alepha.emit("react:server:render:end", result);
|
|
275
|
+
await this.alepha.events.emit("react:server:render:end", result);
|
|
272
276
|
|
|
273
277
|
return result;
|
|
274
278
|
};
|
|
@@ -300,7 +304,7 @@ export class ReactServerProvider {
|
|
|
300
304
|
const state = entry as ReactRouterState;
|
|
301
305
|
|
|
302
306
|
if (this.alepha.has(ServerLinksProvider)) {
|
|
303
|
-
this.alepha.state(
|
|
307
|
+
this.alepha.state.set(
|
|
304
308
|
"api",
|
|
305
309
|
await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
306
310
|
user: (serverRequest as any).user, // TODO: fix type
|
|
@@ -331,7 +335,7 @@ export class ReactServerProvider {
|
|
|
331
335
|
// return;
|
|
332
336
|
// }
|
|
333
337
|
|
|
334
|
-
await this.alepha.emit("react:server:render:begin", {
|
|
338
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
335
339
|
request: serverRequest,
|
|
336
340
|
state,
|
|
337
341
|
});
|
|
@@ -371,7 +375,7 @@ export class ReactServerProvider {
|
|
|
371
375
|
html,
|
|
372
376
|
};
|
|
373
377
|
|
|
374
|
-
await this.alepha.emit("react:server:render:end", event);
|
|
378
|
+
await this.alepha.events.emit("react:server:render:end", event);
|
|
375
379
|
|
|
376
380
|
route.onServerResponse?.(serverRequest);
|
|
377
381
|
|
|
@@ -391,7 +395,7 @@ export class ReactServerProvider {
|
|
|
391
395
|
const element = this.pageApi.root(state);
|
|
392
396
|
|
|
393
397
|
// attach react router state to the http request context
|
|
394
|
-
this.alepha.state("react.router.state", state);
|
|
398
|
+
this.alepha.state.set("react.router.state", state);
|
|
395
399
|
|
|
396
400
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
397
401
|
let app = "";
|
|
@@ -452,36 +456,80 @@ export class ReactServerProvider {
|
|
|
452
456
|
return response.html;
|
|
453
457
|
}
|
|
454
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
|
+
|
|
455
508
|
protected fillTemplate(
|
|
456
509
|
response: { html: string },
|
|
457
510
|
app: string,
|
|
458
511
|
script: string,
|
|
459
512
|
) {
|
|
460
|
-
if (this.
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
this.ROOT_DIV_REGEX,
|
|
464
|
-
(_match, beforeId, afterId) => {
|
|
465
|
-
return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
|
|
466
|
-
},
|
|
467
|
-
);
|
|
468
|
-
} else {
|
|
469
|
-
const bodyOpenTag = /<body([^>]*)>/i;
|
|
470
|
-
if (bodyOpenTag.test(response.html)) {
|
|
471
|
-
response.html = response.html.replace(bodyOpenTag, (match) => {
|
|
472
|
-
return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
|
|
473
|
-
});
|
|
474
|
-
}
|
|
513
|
+
if (!this.preprocessedTemplate) {
|
|
514
|
+
// Fallback to old logic if preprocessing failed
|
|
515
|
+
this.preprocessedTemplate = this.preprocessTemplate(response.html);
|
|
475
516
|
}
|
|
476
517
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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;
|
|
484
525
|
}
|
|
485
526
|
}
|
|
486
527
|
|
|
487
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() {
|