@alepha/react 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.browser.js +1488 -4
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1827 -4
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +58 -937
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +139 -2014
- package/dist/core/index.js.map +1 -1
- package/dist/form/index.d.ts.map +1 -1
- package/dist/form/index.js +6 -1
- package/dist/form/index.js.map +1 -1
- package/dist/head/index.browser.js +3 -1
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +552 -8
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +17 -2
- package/dist/head/index.js.map +1 -1
- package/dist/{core → router}/index.browser.js +126 -516
- package/dist/router/index.browser.js.map +1 -0
- package/dist/router/index.d.ts +1334 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +1939 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +12 -6
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/services/ReactAuth.ts +1 -1
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/components/ClientOnly.tsx +14 -0
- package/src/core/components/ErrorBoundary.tsx +3 -2
- package/src/core/contexts/AlephaContext.ts +3 -0
- package/src/core/contexts/AlephaProvider.tsx +2 -1
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/core/index.ts +13 -102
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/form/services/FormModel.ts +5 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +18 -8
- package/src/head/interfaces/Head.ts +3 -0
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/HeadProvider.ts +6 -1
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/head/providers/ServerHeadProvider.ts +20 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/{core → router}/components/ErrorViewer.tsx +2 -0
- package/src/router/components/Link.tsx +21 -0
- package/src/{core → router}/components/NestedView.tsx +3 -5
- package/src/router/components/NotFound.tsx +30 -0
- package/src/router/errors/Redirection.ts +28 -0
- package/src/{core → router}/hooks/useActive.ts +6 -2
- package/src/{core → router}/hooks/useQueryParams.ts +2 -2
- package/src/{core → router}/hooks/useRouter.ts +1 -1
- package/src/{core → router}/hooks/useRouterState.ts +1 -1
- package/src/{core → router}/index.browser.ts +14 -12
- package/src/{core/index.shared-router.ts → router/index.shared.ts} +6 -3
- package/src/router/index.ts +125 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/{core → router}/primitives/$page.ts +1 -1
- package/src/{core → router}/providers/ReactBrowserProvider.ts +3 -13
- package/src/{core → router}/providers/ReactBrowserRendererProvider.ts +3 -0
- package/src/{core → router}/providers/ReactBrowserRouterProvider.ts +3 -0
- package/src/{core → router}/providers/ReactPageProvider.ts +5 -3
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/{core → router}/providers/ReactServerProvider.ts +12 -30
- package/src/{core → router}/services/ReactPageServerService.ts +3 -0
- package/src/{core → router}/services/ReactPageService.ts +5 -5
- package/src/{core → router}/services/ReactRouter.ts +26 -5
- package/dist/core/index.browser.js.map +0 -1
- package/dist/core/index.native.js +0 -403
- package/dist/core/index.native.js.map +0 -1
- package/src/core/components/Link.tsx +0 -18
- package/src/core/components/NotFound.tsx +0 -27
- package/src/core/errors/Redirection.ts +0 -13
- package/src/core/hooks/useSchema.ts +0 -88
- package/src/core/index.native.ts +0 -21
- package/src/core/index.shared.ts +0 -9
- /package/src/{core → router}/contexts/RouterLayerContext.ts +0 -0
|
@@ -18,6 +18,9 @@ import type {
|
|
|
18
18
|
ReactRouterState,
|
|
19
19
|
TransitionOptions,
|
|
20
20
|
} from "./ReactPageProvider.ts";
|
|
21
|
+
import type { RouterGoOptions } from "../services/ReactRouter.ts";
|
|
22
|
+
|
|
23
|
+
export type { RouterGoOptions } from "../services/ReactRouter.ts";
|
|
21
24
|
|
|
22
25
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
23
26
|
|
|
@@ -297,19 +300,6 @@ export class ReactBrowserProvider {
|
|
|
297
300
|
|
|
298
301
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
299
302
|
|
|
300
|
-
export interface RouterGoOptions {
|
|
301
|
-
replace?: boolean;
|
|
302
|
-
match?: TransitionOptions;
|
|
303
|
-
params?: Record<string, string>;
|
|
304
|
-
query?: Record<string, string>;
|
|
305
|
-
meta?: Record<string, any>;
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Recreate the whole page, ignoring the current state.
|
|
309
|
-
*/
|
|
310
|
-
force?: boolean;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
303
|
export type ReactHydrationState = {
|
|
314
304
|
layers?: Array<PreviousLayerData>;
|
|
315
305
|
} & {
|
|
@@ -2,6 +2,9 @@ import { $hook } from "alepha";
|
|
|
2
2
|
import { $logger } from "alepha/logger";
|
|
3
3
|
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Browser specific React renderer (react-dom/client interface)
|
|
7
|
+
*/
|
|
5
8
|
export class ReactBrowserRendererProvider {
|
|
6
9
|
protected readonly log = $logger();
|
|
7
10
|
protected root?: Root;
|
|
@@ -16,6 +16,9 @@ export interface BrowserRoute extends Route {
|
|
|
16
16
|
page: PageRoute;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Implementation of AlephaRouter for React in browser environment.
|
|
21
|
+
*/
|
|
19
22
|
export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
20
23
|
protected readonly log = $logger();
|
|
21
24
|
protected readonly alepha = $inject(Alepha);
|
|
@@ -10,19 +10,18 @@ import {
|
|
|
10
10
|
} from "alepha";
|
|
11
11
|
import { $logger } from "alepha/logger";
|
|
12
12
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
13
|
-
import ClientOnly from "
|
|
13
|
+
import { AlephaContext, ClientOnly } from "@alepha/react";
|
|
14
14
|
import ErrorViewer from "../components/ErrorViewer.tsx";
|
|
15
15
|
import NestedView from "../components/NestedView.tsx";
|
|
16
16
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
17
|
-
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
18
17
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
18
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
19
19
|
import {
|
|
20
20
|
$page,
|
|
21
21
|
type ErrorHandler,
|
|
22
22
|
type PagePrimitive,
|
|
23
23
|
type PagePrimitiveOptions,
|
|
24
24
|
} from "../primitives/$page.ts";
|
|
25
|
-
import { Redirection } from "../errors/Redirection.ts";
|
|
26
25
|
|
|
27
26
|
const envSchema = t.object({
|
|
28
27
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
@@ -32,6 +31,9 @@ declare module "alepha" {
|
|
|
32
31
|
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
33
32
|
}
|
|
34
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Handle page routes for React applications. (Browser and Server)
|
|
36
|
+
*/
|
|
35
37
|
export class ReactPageProvider {
|
|
36
38
|
protected readonly log = $logger();
|
|
37
39
|
protected readonly env = $env(envSchema);
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { test } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Simple mock for testing template functionality without full Alepha setup
|
|
4
|
+
class MockReactServerProvider {
|
|
5
|
+
protected readonly env = { REACT_ROOT_ID: "root" };
|
|
6
|
+
protected readonly ROOT_DIV_REGEX = new RegExp(
|
|
7
|
+
`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
|
|
8
|
+
"is",
|
|
9
|
+
);
|
|
10
|
+
protected preprocessedTemplate: any = null;
|
|
11
|
+
|
|
12
|
+
public preprocessTemplate(template: string) {
|
|
13
|
+
// Find the body close tag for script injection
|
|
14
|
+
const bodyCloseMatch = template.match(/<\/body>/i);
|
|
15
|
+
const bodyCloseIndex = bodyCloseMatch?.index ?? template.length;
|
|
16
|
+
|
|
17
|
+
const beforeScript = template.substring(0, bodyCloseIndex);
|
|
18
|
+
const afterScript = template.substring(bodyCloseIndex);
|
|
19
|
+
|
|
20
|
+
// Check if there's an existing root div
|
|
21
|
+
const rootDivMatch = beforeScript.match(this.ROOT_DIV_REGEX);
|
|
22
|
+
|
|
23
|
+
if (rootDivMatch) {
|
|
24
|
+
// Split around the existing root div content
|
|
25
|
+
const beforeDiv = beforeScript.substring(0, rootDivMatch.index!);
|
|
26
|
+
const afterDivStart = rootDivMatch.index! + rootDivMatch[0].length;
|
|
27
|
+
const afterDiv = beforeScript.substring(afterDivStart);
|
|
28
|
+
|
|
29
|
+
const beforeApp = `${beforeDiv}<div${rootDivMatch[1]} id="${this.env.REACT_ROOT_ID}"${rootDivMatch[2]}>`;
|
|
30
|
+
const afterApp = `</div>${afterDiv}`;
|
|
31
|
+
|
|
32
|
+
return { beforeApp, afterApp, beforeScript: "", afterScript };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// No existing root div, find body tag to inject new div
|
|
36
|
+
const bodyMatch = beforeScript.match(/<body([^>]*)>/i);
|
|
37
|
+
if (bodyMatch) {
|
|
38
|
+
const beforeBody = beforeScript.substring(
|
|
39
|
+
0,
|
|
40
|
+
bodyMatch.index! + bodyMatch[0].length,
|
|
41
|
+
);
|
|
42
|
+
const afterBody = beforeScript.substring(
|
|
43
|
+
bodyMatch.index! + bodyMatch[0].length,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const beforeApp = `${beforeBody}<div id="${this.env.REACT_ROOT_ID}">`;
|
|
47
|
+
const afterApp = `</div>${afterBody}`;
|
|
48
|
+
|
|
49
|
+
return { beforeApp, afterApp, beforeScript: "", afterScript };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: no body tag found, just wrap everything
|
|
53
|
+
return {
|
|
54
|
+
beforeApp: `<div id="${this.env.REACT_ROOT_ID}">`,
|
|
55
|
+
afterApp: `</div>`,
|
|
56
|
+
beforeScript,
|
|
57
|
+
afterScript,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public fillTemplate(response: { html: string }, app: string, script: string) {
|
|
62
|
+
if (!this.preprocessedTemplate) {
|
|
63
|
+
// Fallback to old logic if preprocessing failed
|
|
64
|
+
this.preprocessedTemplate = this.preprocessTemplate(response.html);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Pure concatenation - no regex replacements needed
|
|
68
|
+
response.html =
|
|
69
|
+
this.preprocessedTemplate.beforeApp +
|
|
70
|
+
app +
|
|
71
|
+
this.preprocessedTemplate.afterApp +
|
|
72
|
+
script +
|
|
73
|
+
this.preprocessedTemplate.afterScript;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const setup = (env?: any) => {
|
|
78
|
+
const provider = new MockReactServerProvider();
|
|
79
|
+
if (env?.REACT_ROOT_ID) {
|
|
80
|
+
(provider as any).env.REACT_ROOT_ID = env.REACT_ROOT_ID;
|
|
81
|
+
(provider as any).ROOT_DIV_REGEX = new RegExp(
|
|
82
|
+
`<div([^>]*)\\s+id=["']${env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
|
|
83
|
+
"is",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return { provider };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
test("ReactServerProvider - preprocessTemplate with existing root div", async ({
|
|
90
|
+
expect,
|
|
91
|
+
}) => {
|
|
92
|
+
const { provider } = setup();
|
|
93
|
+
|
|
94
|
+
const template = `<!DOCTYPE html>
|
|
95
|
+
<html>
|
|
96
|
+
<head><title>Test</title></head>
|
|
97
|
+
<body>
|
|
98
|
+
<div id="root">existing content</div>
|
|
99
|
+
</body>
|
|
100
|
+
</html>`;
|
|
101
|
+
|
|
102
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
103
|
+
|
|
104
|
+
expect(preprocessed.beforeApp).toBe(
|
|
105
|
+
`<!DOCTYPE html>
|
|
106
|
+
<html>
|
|
107
|
+
<head><title>Test</title></head>
|
|
108
|
+
<body>
|
|
109
|
+
<div id="root">`,
|
|
110
|
+
);
|
|
111
|
+
expect(preprocessed.afterApp).toBe(`</div>
|
|
112
|
+
`);
|
|
113
|
+
expect(preprocessed.beforeScript).toBe("");
|
|
114
|
+
expect(preprocessed.afterScript).toBe(`</body>
|
|
115
|
+
</html>`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("ReactServerProvider - preprocessTemplate without root div", async ({
|
|
119
|
+
expect,
|
|
120
|
+
}) => {
|
|
121
|
+
const { provider } = setup();
|
|
122
|
+
|
|
123
|
+
const template = `<!DOCTYPE html>
|
|
124
|
+
<html>
|
|
125
|
+
<head><title>Test</title></head>
|
|
126
|
+
<body>
|
|
127
|
+
<h1>Welcome</h1>
|
|
128
|
+
</body>
|
|
129
|
+
</html>`;
|
|
130
|
+
|
|
131
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
132
|
+
|
|
133
|
+
expect(preprocessed.beforeApp).toBe(
|
|
134
|
+
`<!DOCTYPE html>
|
|
135
|
+
<html>
|
|
136
|
+
<head><title>Test</title></head>
|
|
137
|
+
<body><div id="root">`,
|
|
138
|
+
);
|
|
139
|
+
expect(preprocessed.afterApp).toBe(`</div>
|
|
140
|
+
<h1>Welcome</h1>
|
|
141
|
+
`);
|
|
142
|
+
expect(preprocessed.beforeScript).toBe("");
|
|
143
|
+
expect(preprocessed.afterScript).toBe(`</body>
|
|
144
|
+
</html>`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("ReactServerProvider - preprocessTemplate with custom root ID", async ({
|
|
148
|
+
expect,
|
|
149
|
+
}) => {
|
|
150
|
+
const { provider } = setup({ REACT_ROOT_ID: "app" });
|
|
151
|
+
|
|
152
|
+
const template = `<!DOCTYPE html>
|
|
153
|
+
<html>
|
|
154
|
+
<head><title>Test</title></head>
|
|
155
|
+
<body>
|
|
156
|
+
<div id="app">existing content</div>
|
|
157
|
+
</body>
|
|
158
|
+
</html>`;
|
|
159
|
+
|
|
160
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
161
|
+
|
|
162
|
+
expect(preprocessed.beforeApp).toBe(
|
|
163
|
+
`<!DOCTYPE html>
|
|
164
|
+
<html>
|
|
165
|
+
<head><title>Test</title></head>
|
|
166
|
+
<body>
|
|
167
|
+
<div id="app">`,
|
|
168
|
+
);
|
|
169
|
+
expect(preprocessed.afterApp).toBe(`</div>
|
|
170
|
+
`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("ReactServerProvider - preprocessTemplate with root div with attributes", async ({
|
|
174
|
+
expect,
|
|
175
|
+
}) => {
|
|
176
|
+
const { provider } = setup();
|
|
177
|
+
|
|
178
|
+
const template = `<!DOCTYPE html>
|
|
179
|
+
<html>
|
|
180
|
+
<body>
|
|
181
|
+
<div class="container" id="root" data-test="true">existing content</div>
|
|
182
|
+
</body>
|
|
183
|
+
</html>`;
|
|
184
|
+
|
|
185
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
186
|
+
|
|
187
|
+
expect(preprocessed.beforeApp).toBe(
|
|
188
|
+
`<!DOCTYPE html>
|
|
189
|
+
<html>
|
|
190
|
+
<body>
|
|
191
|
+
<div class="container" id="root" data-test="true">`,
|
|
192
|
+
);
|
|
193
|
+
expect(preprocessed.afterApp).toBe(`</div>
|
|
194
|
+
`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("ReactServerProvider - preprocessTemplate fallback (no body tag)", async ({
|
|
198
|
+
expect,
|
|
199
|
+
}) => {
|
|
200
|
+
const { provider } = setup();
|
|
201
|
+
|
|
202
|
+
const template = `<html><div>content</div></html>`;
|
|
203
|
+
|
|
204
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
205
|
+
|
|
206
|
+
expect(preprocessed.beforeApp).toBe(`<div id="root">`);
|
|
207
|
+
expect(preprocessed.afterApp).toBe(`</div>`);
|
|
208
|
+
expect(preprocessed.beforeScript).toBe(`<html><div>content</div></html>`);
|
|
209
|
+
expect(preprocessed.afterScript).toBe(``);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("ReactServerProvider - fillTemplate concatenation", async ({ expect }) => {
|
|
213
|
+
const { provider } = setup();
|
|
214
|
+
|
|
215
|
+
const template = `<!DOCTYPE html>
|
|
216
|
+
<html>
|
|
217
|
+
<head><title>Test</title></head>
|
|
218
|
+
<body>
|
|
219
|
+
<div id="root">existing</div>
|
|
220
|
+
</body>
|
|
221
|
+
</html>`;
|
|
222
|
+
|
|
223
|
+
// Preprocess the template
|
|
224
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
225
|
+
(provider as any).preprocessedTemplate = preprocessed;
|
|
226
|
+
|
|
227
|
+
const response = { html: "" };
|
|
228
|
+
const app = "<h1>Hello World</h1>";
|
|
229
|
+
const script = '<script>window.__ssr={"test":true}</script>';
|
|
230
|
+
|
|
231
|
+
provider.fillTemplate(response, app, script);
|
|
232
|
+
|
|
233
|
+
const expected = `<!DOCTYPE html>
|
|
234
|
+
<html>
|
|
235
|
+
<head><title>Test</title></head>
|
|
236
|
+
<body>
|
|
237
|
+
<div id="root"><h1>Hello World</h1></div>
|
|
238
|
+
<script>window.__ssr={"test":true}</script></body>
|
|
239
|
+
</html>`;
|
|
240
|
+
|
|
241
|
+
expect(response.html).toBe(expected);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("ReactServerProvider - fillTemplate without preprocessed template (fallback)", async ({
|
|
245
|
+
expect,
|
|
246
|
+
}) => {
|
|
247
|
+
const { provider } = setup();
|
|
248
|
+
|
|
249
|
+
const template = `<!DOCTYPE html>
|
|
250
|
+
<html>
|
|
251
|
+
<body>
|
|
252
|
+
<div id="root">existing</div>
|
|
253
|
+
</body>
|
|
254
|
+
</html>`;
|
|
255
|
+
|
|
256
|
+
const response = { html: template };
|
|
257
|
+
const app = "<h1>Hello World</h1>";
|
|
258
|
+
const script = '<script>window.__ssr={"test":true}</script>';
|
|
259
|
+
|
|
260
|
+
// Don't set preprocessedTemplate to test fallback
|
|
261
|
+
(provider as any).preprocessedTemplate = null;
|
|
262
|
+
|
|
263
|
+
provider.fillTemplate(response, app, script);
|
|
264
|
+
|
|
265
|
+
const expected = `<!DOCTYPE html>
|
|
266
|
+
<html>
|
|
267
|
+
<body>
|
|
268
|
+
<div id="root"><h1>Hello World</h1></div>
|
|
269
|
+
<script>window.__ssr={"test":true}</script></body>
|
|
270
|
+
</html>`;
|
|
271
|
+
|
|
272
|
+
expect(response.html).toBe(expected);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("ReactServerProvider - fillTemplate performance comparison", async ({
|
|
276
|
+
expect,
|
|
277
|
+
}) => {
|
|
278
|
+
const { provider } = setup();
|
|
279
|
+
|
|
280
|
+
const template = `<!DOCTYPE html>
|
|
281
|
+
<html>
|
|
282
|
+
<head><title>Performance Test</title></head>
|
|
283
|
+
<body>
|
|
284
|
+
<div id="root">existing content</div>
|
|
285
|
+
<script>console.log('existing');</script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>`;
|
|
288
|
+
|
|
289
|
+
const app = "<div><h1>Hello World</h1><p>This is a test</p></div>";
|
|
290
|
+
const script = '<script>window.__ssr={"data":"value","count":42}</script>';
|
|
291
|
+
|
|
292
|
+
// Test with preprocessing (should be faster)
|
|
293
|
+
const preprocessed = provider.preprocessTemplate(template);
|
|
294
|
+
(provider as any).preprocessedTemplate = preprocessed;
|
|
295
|
+
|
|
296
|
+
const response1 = { html: "" };
|
|
297
|
+
const start1 = performance.now();
|
|
298
|
+
provider.fillTemplate(response1, app, script);
|
|
299
|
+
const time1 = performance.now() - start1;
|
|
300
|
+
|
|
301
|
+
// Test fallback without preprocessing (should work but potentially slower)
|
|
302
|
+
(provider as any).preprocessedTemplate = null;
|
|
303
|
+
const response2 = { html: template };
|
|
304
|
+
const start2 = performance.now();
|
|
305
|
+
provider.fillTemplate(response2, app, script);
|
|
306
|
+
const time2 = performance.now() - start2;
|
|
307
|
+
|
|
308
|
+
// Both should produce the same result
|
|
309
|
+
expect(response1.html).toBe(response2.html);
|
|
310
|
+
|
|
311
|
+
// The result should contain the app content
|
|
312
|
+
expect(response1.html).toContain("<h1>Hello World</h1>");
|
|
313
|
+
expect(response1.html).toContain(
|
|
314
|
+
'<script>window.__ssr={"data":"value","count":42}</script>',
|
|
315
|
+
);
|
|
316
|
+
});
|
|
@@ -1,38 +1,15 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
$atom,
|
|
5
|
-
$env,
|
|
6
|
-
$hook,
|
|
7
|
-
$inject,
|
|
8
|
-
$use,
|
|
9
|
-
Alepha,
|
|
10
|
-
AlephaError,
|
|
11
|
-
type Static,
|
|
12
|
-
t,
|
|
13
|
-
} from "alepha";
|
|
3
|
+
import { $atom, $env, $hook, $inject, $use, Alepha, AlephaError, type Static, t, } from "alepha";
|
|
14
4
|
import { $logger } from "alepha/logger";
|
|
15
|
-
import {
|
|
16
|
-
type ServerHandler,
|
|
17
|
-
ServerProvider,
|
|
18
|
-
ServerRouterProvider,
|
|
19
|
-
ServerTimingProvider,
|
|
20
|
-
} from "alepha/server";
|
|
5
|
+
import { type ServerHandler, ServerProvider, ServerRouterProvider, ServerTimingProvider, } from "alepha/server";
|
|
21
6
|
import { ServerLinksProvider } from "alepha/server/links";
|
|
22
7
|
import { ServerStaticProvider } from "alepha/server/static";
|
|
23
8
|
import { renderToString } from "react-dom/server";
|
|
24
|
-
import {
|
|
25
|
-
$page,
|
|
26
|
-
type PagePrimitiveRenderOptions,
|
|
27
|
-
type PagePrimitiveRenderResult,
|
|
28
|
-
} from "../primitives/$page.ts";
|
|
29
9
|
import { Redirection } from "../errors/Redirection.ts";
|
|
10
|
+
import { $page, type PagePrimitiveRenderOptions, type PagePrimitiveRenderResult, } from "../primitives/$page.ts";
|
|
30
11
|
import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
|
|
31
|
-
import {
|
|
32
|
-
type PageRoute,
|
|
33
|
-
ReactPageProvider,
|
|
34
|
-
type ReactRouterState,
|
|
35
|
-
} from "./ReactPageProvider.ts";
|
|
12
|
+
import { type PageRoute, ReactPageProvider, type ReactRouterState, } from "./ReactPageProvider.ts";
|
|
36
13
|
|
|
37
14
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
38
15
|
|
|
@@ -84,12 +61,16 @@ declare module "alepha" {
|
|
|
84
61
|
|
|
85
62
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
86
63
|
|
|
64
|
+
/**
|
|
65
|
+
* React server provider responsible for SSR and static file serving.
|
|
66
|
+
*
|
|
67
|
+
* Use `react-dom/server` under the hood.
|
|
68
|
+
*/
|
|
87
69
|
export class ReactServerProvider {
|
|
88
70
|
protected readonly log = $logger();
|
|
89
71
|
protected readonly alepha = $inject(Alepha);
|
|
90
72
|
protected readonly env = $env(envSchema);
|
|
91
73
|
protected readonly pageApi = $inject(ReactPageProvider);
|
|
92
|
-
protected readonly serverProvider = $inject(ServerProvider);
|
|
93
74
|
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
94
75
|
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
95
76
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
@@ -235,9 +216,10 @@ export class ReactServerProvider {
|
|
|
235
216
|
return;
|
|
236
217
|
}
|
|
237
218
|
|
|
238
|
-
this.
|
|
219
|
+
const env = this.alepha.store.get("env") ?? {}
|
|
220
|
+
const url = `http://localhost:${env.SERVER_PORT ?? "5173"}`;
|
|
239
221
|
|
|
240
|
-
|
|
222
|
+
this.log.info("SSR (dev) OK", { url });
|
|
241
223
|
|
|
242
224
|
await this.registerPages(() =>
|
|
243
225
|
fetch(`${url}/index.html`)
|
|
@@ -7,6 +7,9 @@ import type {
|
|
|
7
7
|
import { ReactServerProvider } from "../providers/ReactServerProvider.ts";
|
|
8
8
|
import { ReactPageService } from "./ReactPageService.ts";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* $page methods for server-side.
|
|
12
|
+
*/
|
|
10
13
|
export class ReactPageServerService extends ReactPageService {
|
|
11
14
|
protected readonly reactServerProvider = $inject(ReactServerProvider);
|
|
12
15
|
protected readonly serverProvider = $inject(ServerProvider);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AlephaError } from "alepha";
|
|
2
|
-
import type {
|
|
3
|
-
PagePrimitiveRenderOptions,
|
|
4
|
-
PagePrimitiveRenderResult,
|
|
5
|
-
} from "../primitives/$page.ts";
|
|
2
|
+
import type { PagePrimitiveRenderOptions, PagePrimitiveRenderResult, } from "../../router/primitives/$page.ts";
|
|
6
3
|
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* $page methods interface.
|
|
6
|
+
*/
|
|
7
|
+
export abstract class ReactPageService {
|
|
8
8
|
public fetch(
|
|
9
9
|
pathname: string,
|
|
10
10
|
options: PagePrimitiveRenderOptions = {},
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { $inject, Alepha } from "alepha";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
ReactBrowserProvider,
|
|
5
|
-
type RouterGoOptions,
|
|
6
|
-
} from "../providers/ReactBrowserProvider.ts";
|
|
2
|
+
import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
|
|
7
3
|
import {
|
|
8
4
|
type AnchorProps,
|
|
9
5
|
ReactPageProvider,
|
|
10
6
|
type ReactRouterState,
|
|
11
7
|
} from "../providers/ReactPageProvider.ts";
|
|
8
|
+
import type { PagePrimitive } from "../primitives/$page.ts";
|
|
9
|
+
|
|
10
|
+
export interface RouterGoOptions {
|
|
11
|
+
replace?: boolean;
|
|
12
|
+
params?: Record<string, string>;
|
|
13
|
+
query?: Record<string, string>;
|
|
14
|
+
meta?: Record<string, any>;
|
|
15
|
+
/**
|
|
16
|
+
* Recreate the whole page, ignoring the current state.
|
|
17
|
+
*/
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
12
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Friendly browser router API.
|
|
23
|
+
*
|
|
24
|
+
* Can be safely used server-side, but most methods will be no-op.
|
|
25
|
+
*/
|
|
13
26
|
export class ReactRouter<T extends object> {
|
|
14
27
|
protected readonly alepha = $inject(Alepha);
|
|
15
28
|
protected readonly pageApi = $inject(ReactPageProvider);
|
|
@@ -59,6 +72,14 @@ export class ReactRouter<T extends object> {
|
|
|
59
72
|
} = {},
|
|
60
73
|
) {
|
|
61
74
|
const page = this.pageApi.page(name as string);
|
|
75
|
+
if (!page.lazy && !page.component) {
|
|
76
|
+
return {
|
|
77
|
+
...page,
|
|
78
|
+
label: page.label ?? page.name,
|
|
79
|
+
children: undefined,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
return {
|
|
63
84
|
...page,
|
|
64
85
|
label: page.label ?? page.name,
|