@alepha/react 0.14.3 → 0.15.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 +10 -0
- package/dist/auth/index.browser.js +29 -14
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +950 -194
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +118 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/form/index.d.ts +27 -28
- package/dist/form/index.d.ts.map +1 -1
- package/dist/head/index.browser.js +59 -19
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +105 -576
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +91 -87
- package/dist/head/index.js.map +1 -1
- package/dist/i18n/index.d.ts +33 -33
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/router/index.browser.js +30 -15
- package/dist/router/index.browser.js.map +1 -1
- package/dist/router/index.d.ts +827 -403
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +951 -195
- package/dist/router/index.js.map +1 -1
- package/dist/websocket/index.d.ts +38 -39
- package/dist/websocket/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/auth/__tests__/$auth.spec.ts +10 -11
- package/src/core/__tests__/Router.spec.tsx +4 -4
- package/src/head/{__tests__/expandSeo.spec.ts → helpers/SeoExpander.spec.ts} +1 -1
- package/src/head/index.ts +10 -28
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +1 -76
- package/src/head/providers/BrowserHeadProvider.ts +25 -19
- package/src/head/providers/HeadProvider.ts +76 -10
- package/src/head/providers/ServerHeadProvider.ts +22 -138
- package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
- package/src/router/__tests__/page-head.spec.ts +44 -0
- package/src/{head → router}/__tests__/seo-head.spec.ts +2 -2
- package/src/router/atoms/ssrManifestAtom.ts +60 -0
- package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
- package/src/router/errors/Redirection.ts +1 -1
- package/src/router/index.shared.ts +1 -0
- package/src/router/index.ts +16 -2
- package/src/router/primitives/$page.browser.spec.tsx +15 -15
- package/src/router/primitives/$page.spec.tsx +18 -18
- package/src/router/primitives/$page.ts +46 -10
- package/src/router/providers/ReactBrowserProvider.ts +14 -29
- package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
- package/src/router/providers/ReactPageProvider.ts +11 -4
- package/src/router/providers/ReactServerProvider.ts +321 -316
- package/src/router/providers/ReactServerTemplateProvider.ts +793 -0
- package/src/router/providers/SSRManifestProvider.ts +365 -0
- package/src/router/services/ReactPageServerService.ts +5 -3
- package/src/router/services/ReactRouter.ts +3 -3
- package/src/head/__tests__/page-head.spec.ts +0 -39
- package/src/head/providers/ServerHeadProvider.spec.ts +0 -163
|
@@ -1,147 +1,31 @@
|
|
|
1
|
-
import { $
|
|
2
|
-
import {
|
|
3
|
-
import type { HeadMeta, SimpleHead } from "../interfaces/Head.ts";
|
|
1
|
+
import { $inject } from "alepha";
|
|
2
|
+
import type { Head, SimpleHead } from "../interfaces/Head.ts";
|
|
4
3
|
import { HeadProvider } from "./HeadProvider.ts";
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Server-side head provider that fills head content from route configurations.
|
|
7
|
+
*
|
|
8
|
+
* Used by ReactServerProvider to collect title, meta tags, and other head
|
|
9
|
+
* elements which are then rendered by ReactServerTemplateProvider.
|
|
10
|
+
*/
|
|
6
11
|
export class ServerHeadProvider {
|
|
7
12
|
protected readonly headProvider = $inject(HeadProvider);
|
|
8
|
-
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.serverTimingProvider.endTiming("renderHead");
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
public renderHead(template: string, head: SimpleHead): string {
|
|
23
|
-
let result = template;
|
|
24
|
-
|
|
25
|
-
// Inject htmlAttributes
|
|
26
|
-
const htmlAttributes = head.htmlAttributes;
|
|
27
|
-
if (htmlAttributes) {
|
|
28
|
-
result = result.replace(
|
|
29
|
-
/<html([^>]*)>/i,
|
|
30
|
-
(_, existingAttrs) =>
|
|
31
|
-
`<html${this.mergeAttributes(existingAttrs, htmlAttributes)}>`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Inject bodyAttributes
|
|
36
|
-
const bodyAttributes = head.bodyAttributes;
|
|
37
|
-
if (bodyAttributes) {
|
|
38
|
-
result = result.replace(
|
|
39
|
-
/<body([^>]*)>/i,
|
|
40
|
-
(_, existingAttrs) =>
|
|
41
|
-
`<body${this.mergeAttributes(existingAttrs, bodyAttributes)}>`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Build head content
|
|
46
|
-
let headContent = "";
|
|
47
|
-
const title = head.title;
|
|
48
|
-
if (title) {
|
|
49
|
-
if (template.includes("<title>")) {
|
|
50
|
-
result = result.replace(
|
|
51
|
-
/<title>(.*?)<\/title>/i,
|
|
52
|
-
() => `<title>${this.escapeHtml(title)}</title>`,
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
headContent += `<title>${this.escapeHtml(title)}</title>\n`;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (head.meta) {
|
|
60
|
-
for (const meta of head.meta) {
|
|
61
|
-
headContent += this.renderMetaTag(meta);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (head.link) {
|
|
66
|
-
for (const link of head.link) {
|
|
67
|
-
headContent += `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}">\n`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (head.script) {
|
|
72
|
-
for (const script of head.script) {
|
|
73
|
-
headContent += this.renderScriptTag(script);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Inject into <head>...</head>
|
|
78
|
-
result = result.replace(
|
|
79
|
-
/<head([^>]*)>(.*?)<\/head>/is,
|
|
80
|
-
(_, existingAttrs, existingHead) =>
|
|
81
|
-
`<head${existingAttrs}>${existingHead}${headContent}</head>`,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
return result.trim();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
protected mergeAttributes(
|
|
88
|
-
existing: string,
|
|
89
|
-
attrs: Record<string, string>,
|
|
90
|
-
): string {
|
|
91
|
-
const existingAttrs = this.parseAttributes(existing);
|
|
92
|
-
const merged = { ...existingAttrs, ...attrs };
|
|
93
|
-
return Object.entries(merged)
|
|
94
|
-
.map(([k, v]) => ` ${k}="${this.escapeHtml(v)}"`)
|
|
95
|
-
.join("");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
protected parseAttributes(attrStr: string): Record<string, string> {
|
|
99
|
-
attrStr = attrStr.replaceAll("'", '"');
|
|
100
|
-
|
|
101
|
-
const attrs: Record<string, string> = {};
|
|
102
|
-
const attrRegex = /([^\s=]+)(?:="([^"]*)")?/g;
|
|
103
|
-
let match: RegExpExecArray | null = attrRegex.exec(attrStr);
|
|
104
|
-
|
|
105
|
-
while (match) {
|
|
106
|
-
attrs[match[1]] = match[2] ?? "";
|
|
107
|
-
match = attrRegex.exec(attrStr);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return attrs;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
protected escapeHtml(str: string): string {
|
|
114
|
-
return str
|
|
115
|
-
.replace(/&/g, "&")
|
|
116
|
-
.replace(/</g, "<")
|
|
117
|
-
.replace(/>/g, ">")
|
|
118
|
-
.replace(/"/g, """)
|
|
119
|
-
.replace(/'/g, "'");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
protected renderMetaTag(meta: HeadMeta): string {
|
|
123
|
-
// OpenGraph tags use property attribute
|
|
124
|
-
if (meta.property) {
|
|
125
|
-
return `<meta property="${this.escapeHtml(meta.property)}" content="${this.escapeHtml(meta.content)}">\n`;
|
|
126
|
-
}
|
|
127
|
-
// Standard meta tags use name attribute
|
|
128
|
-
if (meta.name) {
|
|
129
|
-
return `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
|
|
130
|
-
}
|
|
131
|
-
return "";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve global head configuration (htmlAttributes only).
|
|
16
|
+
*
|
|
17
|
+
* Used for early streaming optimization - htmlAttributes can be sent
|
|
18
|
+
* before page loaders run since they come from global $head only.
|
|
19
|
+
*/
|
|
20
|
+
public resolveGlobalHead(): Head {
|
|
21
|
+
return this.headProvider.resolveGlobalHead();
|
|
132
22
|
}
|
|
133
23
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return key;
|
|
141
|
-
}
|
|
142
|
-
return `${key}="${this.escapeHtml(String(value))}"`;
|
|
143
|
-
})
|
|
144
|
-
.join(" ");
|
|
145
|
-
return `<script ${attrs}></script>\n`;
|
|
24
|
+
/**
|
|
25
|
+
* Fill head state from route configurations.
|
|
26
|
+
* Delegates to HeadProvider to merge head data from all route layers.
|
|
27
|
+
*/
|
|
28
|
+
public fillHead(state: { head: SimpleHead; layers: Array<any> }): void {
|
|
29
|
+
this.headProvider.fillHead(state as any);
|
|
146
30
|
}
|
|
147
31
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AlephaReactHead, BrowserHeadProvider, type Head } from "@alepha/react/head";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { $page } from "../index.browser.ts";
|
|
5
|
+
|
|
6
|
+
describe("$page head integration (browser)", () => {
|
|
7
|
+
let provider: BrowserHeadProvider;
|
|
8
|
+
|
|
9
|
+
class TestApp {
|
|
10
|
+
simplePage = $page({
|
|
11
|
+
path: "/",
|
|
12
|
+
head: {
|
|
13
|
+
title: "Simple Page",
|
|
14
|
+
bodyAttributes: { class: "simple-page" },
|
|
15
|
+
},
|
|
16
|
+
component: () => "Simple content",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
complexPage = $page({
|
|
20
|
+
path: "/complex",
|
|
21
|
+
head: {
|
|
22
|
+
title: "Complex Page",
|
|
23
|
+
htmlAttributes: {
|
|
24
|
+
lang: "en",
|
|
25
|
+
"data-theme": "dark",
|
|
26
|
+
},
|
|
27
|
+
bodyAttributes: {
|
|
28
|
+
class: "complex-page",
|
|
29
|
+
style: "background: black;",
|
|
30
|
+
},
|
|
31
|
+
meta: [
|
|
32
|
+
{ name: "description", content: "Complex test page" },
|
|
33
|
+
{
|
|
34
|
+
name: "viewport",
|
|
35
|
+
content: "width=device-width, initial-scale=1",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
component: () => "Complex content",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Reset document state
|
|
45
|
+
document.title = "";
|
|
46
|
+
document.head.innerHTML = "";
|
|
47
|
+
document.body.removeAttribute("class");
|
|
48
|
+
document.body.removeAttribute("style");
|
|
49
|
+
document.documentElement.removeAttribute("lang");
|
|
50
|
+
document.documentElement.removeAttribute("class");
|
|
51
|
+
document.documentElement.removeAttribute("data-theme");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
document.body.querySelector("#root")?.remove();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should render simple page head configuration", async () => {
|
|
59
|
+
const alepha = Alepha.create().with(AlephaReactHead).with(TestApp);
|
|
60
|
+
await alepha.start();
|
|
61
|
+
|
|
62
|
+
expect(document.title).toBe("Simple Page");
|
|
63
|
+
expect(document.body.getAttribute("class")).toBe("simple-page");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should get current head state and match page configuration", async () => {
|
|
67
|
+
const alepha = Alepha.create().with(AlephaReactHead);
|
|
68
|
+
provider = alepha.inject(BrowserHeadProvider);
|
|
69
|
+
const app = alepha.inject(TestApp);
|
|
70
|
+
await alepha.start();
|
|
71
|
+
|
|
72
|
+
// Apply complex page head
|
|
73
|
+
const headConfig = app.complexPage.options.head as Head;
|
|
74
|
+
provider.renderHead(document, headConfig);
|
|
75
|
+
|
|
76
|
+
// Get current head state
|
|
77
|
+
const currentHead = provider.getHead(document);
|
|
78
|
+
|
|
79
|
+
expect(currentHead.title).toBe(headConfig.title);
|
|
80
|
+
expect(currentHead.htmlAttributes?.lang).toBe(
|
|
81
|
+
headConfig.htmlAttributes?.lang,
|
|
82
|
+
);
|
|
83
|
+
expect(currentHead.bodyAttributes?.class).toBe(
|
|
84
|
+
headConfig.bodyAttributes?.class,
|
|
85
|
+
);
|
|
86
|
+
expect(currentHead.meta).toContainEqual({
|
|
87
|
+
name: "description",
|
|
88
|
+
content: "Complex test page",
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { describe, it } from "vitest";
|
|
3
|
+
import { $head, AlephaReactHead } from "@alepha/react/head";
|
|
4
|
+
import { $page } from "../index.ts";
|
|
5
|
+
|
|
6
|
+
class App {
|
|
7
|
+
head = $head({
|
|
8
|
+
htmlAttributes: { lang: "fr", "x-data-custom": "ok" },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
hello = $page({
|
|
12
|
+
head: {
|
|
13
|
+
title: "Hello World",
|
|
14
|
+
bodyAttributes: { class: "hello-world" },
|
|
15
|
+
meta: [
|
|
16
|
+
{ name: "description", content: "This is a test page." },
|
|
17
|
+
{ name: "keywords", content: "test, alepha, react" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
component: () => "",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const alepha = Alepha.create().with(AlephaReactHead);
|
|
25
|
+
const a = alepha.inject(App);
|
|
26
|
+
|
|
27
|
+
describe("PageHead", () => {
|
|
28
|
+
it("should render page with custom head and body attributes", async ({
|
|
29
|
+
expect,
|
|
30
|
+
}) => {
|
|
31
|
+
const result = await a.hello.render({ html: true, hydration: false });
|
|
32
|
+
|
|
33
|
+
// Check key parts of the HTML output (streaming adds newlines between sections)
|
|
34
|
+
expect(result.html).toContain('<!DOCTYPE html>');
|
|
35
|
+
expect(result.html).toContain('<html lang="fr" x-data-custom="ok">');
|
|
36
|
+
expect(result.html).toContain('<title>Hello World</title>');
|
|
37
|
+
expect(result.html).toContain('<meta name="description" content="This is a test page.">');
|
|
38
|
+
expect(result.html).toContain('<meta name="keywords" content="test, alepha, react">');
|
|
39
|
+
expect(result.html).toContain('<body class="hello-world">');
|
|
40
|
+
expect(result.html).toContain('<div id="root">');
|
|
41
|
+
expect(result.html).toContain('</body>');
|
|
42
|
+
expect(result.html).toContain('</html>');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $page } from "@alepha/react/router";
|
|
2
1
|
import { Alepha } from "alepha";
|
|
3
2
|
import { describe, it } from "vitest";
|
|
4
|
-
import { $head, AlephaReactHead } from "
|
|
3
|
+
import { $head, AlephaReactHead } from "@alepha/react/head";
|
|
4
|
+
import { $page } from "../index.ts";
|
|
5
5
|
|
|
6
6
|
class App {
|
|
7
7
|
head = $head({
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { $atom, t } from "alepha";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for the SSR manifest atom.
|
|
5
|
+
*/
|
|
6
|
+
export const ssrManifestAtomSchema = t.object({
|
|
7
|
+
/**
|
|
8
|
+
* Preload manifest mapping short keys to source paths.
|
|
9
|
+
* Generated by viteAlephaSsrPreload plugin at build time.
|
|
10
|
+
*/
|
|
11
|
+
preload: t.optional(t.record(t.string(), t.string())),
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SSR manifest mapping source files to their required chunks.
|
|
15
|
+
*/
|
|
16
|
+
ssr: t.optional(t.record(t.string(), t.array(t.string()))),
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Client manifest mapping source files to their output information.
|
|
20
|
+
*/
|
|
21
|
+
client: t.optional(
|
|
22
|
+
t.record(
|
|
23
|
+
t.string(),
|
|
24
|
+
t.object({
|
|
25
|
+
file: t.string(),
|
|
26
|
+
src: t.optional(t.string()),
|
|
27
|
+
isEntry: t.optional(t.boolean()),
|
|
28
|
+
isDynamicEntry: t.optional(t.boolean()),
|
|
29
|
+
imports: t.optional(t.array(t.string())),
|
|
30
|
+
dynamicImports: t.optional(t.array(t.string())),
|
|
31
|
+
css: t.optional(t.array(t.string())),
|
|
32
|
+
assets: t.optional(t.array(t.string())),
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type for the SSR manifest schema.
|
|
40
|
+
*/
|
|
41
|
+
export type SsrManifestAtomSchema = typeof ssrManifestAtomSchema;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* SSR Manifest atom containing all manifest data for SSR module preloading.
|
|
45
|
+
*
|
|
46
|
+
* This atom is populated at build time by embedding manifest data into the
|
|
47
|
+
* generated index.js. This approach is optimal for serverless deployments
|
|
48
|
+
* as it eliminates filesystem reads at runtime.
|
|
49
|
+
*
|
|
50
|
+
* The manifest includes:
|
|
51
|
+
* - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
|
|
52
|
+
* - ssr: Maps source files to their required chunks
|
|
53
|
+
* - client: Maps source files to their output info including imports/css
|
|
54
|
+
*/
|
|
55
|
+
export const ssrManifestAtom = $atom({
|
|
56
|
+
name: "alepha.react.ssr.manifest",
|
|
57
|
+
description: "SSR manifest for module preloading",
|
|
58
|
+
schema: ssrManifestAtomSchema,
|
|
59
|
+
default: {},
|
|
60
|
+
});
|
|
@@ -6,6 +6,7 @@ export { default as NestedView } from "./components/NestedView.tsx";
|
|
|
6
6
|
export type * from "./components/NestedView.tsx";
|
|
7
7
|
export { default as NotFound } from "./components/NotFound.tsx";
|
|
8
8
|
export type * from "./components/NotFound.tsx";
|
|
9
|
+
export * from "./constants/PAGE_PRELOAD_KEY.ts";
|
|
9
10
|
export * from "./contexts/RouterLayerContext.ts";
|
|
10
11
|
export * from "./primitives/$page.ts";
|
|
11
12
|
export * from "./errors/Redirection.ts";
|
package/src/router/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { AlephaServer, type ServerRequest } from "alepha/server";
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
|
|
9
9
|
import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
|
|
10
|
+
import { ReactServerTemplateProvider } from "./providers/ReactServerTemplateProvider.ts";
|
|
11
|
+
import { SSRManifestProvider } from "./providers/SSRManifestProvider.ts";
|
|
10
12
|
import { ReactPageServerService } from "./services/ReactPageServerService.ts";
|
|
11
13
|
import { AlephaServerCache } from "alepha/server/cache";
|
|
12
14
|
import { AlephaServerLinks } from "alepha/server/links";
|
|
@@ -19,6 +21,8 @@ export * from "./index.shared.ts";
|
|
|
19
21
|
export * from "./providers/ReactPageProvider.ts";
|
|
20
22
|
export * from "./providers/ReactBrowserProvider.ts";
|
|
21
23
|
export * from "./providers/ReactServerProvider.ts";
|
|
24
|
+
export * from "./providers/ReactServerTemplateProvider.ts";
|
|
25
|
+
export * from "./providers/SSRManifestProvider.ts";
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
24
28
|
|
|
@@ -46,6 +50,9 @@ declare module "alepha" {
|
|
|
46
50
|
// -----------------------------------------------------------------------------------------------------------------
|
|
47
51
|
/**
|
|
48
52
|
* Fires when the React application is being rendered on the browser.
|
|
53
|
+
*
|
|
54
|
+
* Note: this one is not really necessary, it's a hack because we need to isolate renderer from server code in order
|
|
55
|
+
* to avoid including react-dom/client in server bundles.
|
|
49
56
|
*/
|
|
50
57
|
"react:browser:render": {
|
|
51
58
|
root: HTMLElement;
|
|
@@ -94,7 +101,7 @@ declare module "alepha" {
|
|
|
94
101
|
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
95
102
|
* - Nested routing with parent-child relationships
|
|
96
103
|
* - Type-safe URL parameter and query string validation
|
|
97
|
-
* - Server-side data fetching with the `
|
|
104
|
+
* - Server-side data fetching with the `loader` function
|
|
98
105
|
* - Lazy loading and code splitting
|
|
99
106
|
* - Page animations and error handling
|
|
100
107
|
*
|
|
@@ -107,7 +114,12 @@ export const AlephaReactRouter = $module({
|
|
|
107
114
|
services: [
|
|
108
115
|
ReactPageProvider,
|
|
109
116
|
ReactPageService,
|
|
110
|
-
ReactRouter,
|
|
117
|
+
ReactRouter,
|
|
118
|
+
ReactServerProvider,
|
|
119
|
+
ReactServerTemplateProvider,
|
|
120
|
+
SSRManifestProvider,
|
|
121
|
+
ReactPageServerService,
|
|
122
|
+
],
|
|
111
123
|
register: (alepha) =>
|
|
112
124
|
alepha
|
|
113
125
|
.with(AlephaReact)
|
|
@@ -119,6 +131,8 @@ export const AlephaReactRouter = $module({
|
|
|
119
131
|
provide: ReactPageService,
|
|
120
132
|
use: ReactPageServerService,
|
|
121
133
|
})
|
|
134
|
+
.with(SSRManifestProvider)
|
|
135
|
+
.with(ReactServerTemplateProvider)
|
|
122
136
|
.with(ReactServerProvider)
|
|
123
137
|
.with(ReactPageProvider)
|
|
124
138
|
.with(ReactRouter),
|
|
@@ -89,7 +89,7 @@ describe("$page browser tests", () => {
|
|
|
89
89
|
id: t.text(),
|
|
90
90
|
}),
|
|
91
91
|
},
|
|
92
|
-
|
|
92
|
+
loader: ({ params }) => ({
|
|
93
93
|
userId: params.id,
|
|
94
94
|
userName: `User ${params.id}`,
|
|
95
95
|
}),
|
|
@@ -132,7 +132,7 @@ describe("$page browser tests", () => {
|
|
|
132
132
|
class App {
|
|
133
133
|
async = $page({
|
|
134
134
|
path: "/async",
|
|
135
|
-
|
|
135
|
+
loader: async () => {
|
|
136
136
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
137
137
|
return { message: "Loaded async data" };
|
|
138
138
|
},
|
|
@@ -174,7 +174,7 @@ describe("$page browser tests", () => {
|
|
|
174
174
|
|
|
175
175
|
protected = $page({
|
|
176
176
|
path: "/protected",
|
|
177
|
-
|
|
177
|
+
loader: () => {
|
|
178
178
|
if (!isAuthenticated) {
|
|
179
179
|
throw new Error("Unauthorized");
|
|
180
180
|
}
|
|
@@ -227,7 +227,7 @@ describe("$page browser tests", () => {
|
|
|
227
227
|
class App {
|
|
228
228
|
errorPage = $page({
|
|
229
229
|
path: "/error",
|
|
230
|
-
|
|
230
|
+
loader: () => {
|
|
231
231
|
throw new Error("Something went wrong");
|
|
232
232
|
},
|
|
233
233
|
errorHandler: (error) => (
|
|
@@ -259,7 +259,7 @@ describe("$page browser tests", () => {
|
|
|
259
259
|
class App {
|
|
260
260
|
layout = $page({
|
|
261
261
|
path: "/",
|
|
262
|
-
|
|
262
|
+
loader: () => ({ appName: "My App" }),
|
|
263
263
|
component: ({ appName }: { appName: string }) => (
|
|
264
264
|
<div data-testid="layout">
|
|
265
265
|
<header data-testid="header">{appName}</header>
|
|
@@ -316,7 +316,7 @@ describe("$page browser tests", () => {
|
|
|
316
316
|
class App {
|
|
317
317
|
layout = $page({
|
|
318
318
|
path: "/",
|
|
319
|
-
|
|
319
|
+
loader: () => ({ theme: "dark" }),
|
|
320
320
|
component: ({ theme }: { theme: string }) => (
|
|
321
321
|
<div data-testid="layout" data-theme={theme}>
|
|
322
322
|
<NestedView />
|
|
@@ -328,7 +328,7 @@ describe("$page browser tests", () => {
|
|
|
328
328
|
page = $page({
|
|
329
329
|
path: "/page",
|
|
330
330
|
parent: this.layout,
|
|
331
|
-
|
|
331
|
+
loader: ({ theme }) => ({
|
|
332
332
|
message: `Theme is ${theme}`,
|
|
333
333
|
}),
|
|
334
334
|
component: ({ message }: { message: string }) => (
|
|
@@ -362,7 +362,7 @@ describe("$page browser tests", () => {
|
|
|
362
362
|
class App {
|
|
363
363
|
root = $page({
|
|
364
364
|
path: "/",
|
|
365
|
-
|
|
365
|
+
loader: () => ({ level: "root" }),
|
|
366
366
|
component: ({ level }: { level: string }) => (
|
|
367
367
|
<div data-testid="root">
|
|
368
368
|
{level}
|
|
@@ -374,7 +374,7 @@ describe("$page browser tests", () => {
|
|
|
374
374
|
section = $page({
|
|
375
375
|
path: "/section",
|
|
376
376
|
parent: this.root,
|
|
377
|
-
|
|
377
|
+
loader: ({ level }) => ({ level: `${level} > section` }),
|
|
378
378
|
component: ({ level }: { level: string }) => (
|
|
379
379
|
<div data-testid="section">
|
|
380
380
|
{level}
|
|
@@ -386,7 +386,7 @@ describe("$page browser tests", () => {
|
|
|
386
386
|
page = $page({
|
|
387
387
|
path: "/page",
|
|
388
388
|
parent: this.section,
|
|
389
|
-
|
|
389
|
+
loader: ({ level }) => ({ level: `${level} > page` }),
|
|
390
390
|
component: ({ level }: { level: string }) => (
|
|
391
391
|
<div data-testid="page">{level}</div>
|
|
392
392
|
),
|
|
@@ -469,7 +469,7 @@ describe("$page browser tests", () => {
|
|
|
469
469
|
id: t.text(),
|
|
470
470
|
}),
|
|
471
471
|
},
|
|
472
|
-
|
|
472
|
+
loader: ({ params }) => ({
|
|
473
473
|
userId: params.id,
|
|
474
474
|
}),
|
|
475
475
|
component: ({ userId }: { userId: string }) => (
|
|
@@ -504,7 +504,7 @@ describe("$page browser tests", () => {
|
|
|
504
504
|
page: t.number({ default: 1 }),
|
|
505
505
|
}),
|
|
506
506
|
},
|
|
507
|
-
|
|
507
|
+
loader: ({ query }) => ({
|
|
508
508
|
searchQuery: query.q,
|
|
509
509
|
currentPage: query.page,
|
|
510
510
|
}),
|
|
@@ -553,7 +553,7 @@ describe("$page browser tests", () => {
|
|
|
553
553
|
page: t.number({ default: 1 }),
|
|
554
554
|
}),
|
|
555
555
|
},
|
|
556
|
-
|
|
556
|
+
loader: ({ query }) => ({
|
|
557
557
|
searchQuery: query.q,
|
|
558
558
|
currentPage: query.page,
|
|
559
559
|
}),
|
|
@@ -605,7 +605,7 @@ describe("$page browser tests", () => {
|
|
|
605
605
|
limit: t.number({ default: 10 }),
|
|
606
606
|
}),
|
|
607
607
|
},
|
|
608
|
-
|
|
608
|
+
loader: ({ params, query }) => ({
|
|
609
609
|
userId: params.userId,
|
|
610
610
|
sortBy: query.sort,
|
|
611
611
|
limit: query.limit,
|
|
@@ -660,7 +660,7 @@ describe("$page browser tests", () => {
|
|
|
660
660
|
page: t.number({ default: 1 }),
|
|
661
661
|
}),
|
|
662
662
|
},
|
|
663
|
-
|
|
663
|
+
loader: ({ query }) => ({
|
|
664
664
|
searchQuery: query.q,
|
|
665
665
|
currentPage: query.page,
|
|
666
666
|
}),
|