@alepha/react 0.14.2 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/index.browser.js +29 -14
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.js +960 -195
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.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 +99 -560
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +92 -87
- package/dist/head/index.js.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 +616 -192
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +961 -196
- package/dist/router/index.js.map +1 -1
- package/package.json +4 -4
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/head/helpers/SeoExpander.spec.ts +203 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +11 -28
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
- 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/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/router/__tests__/page-head-browser.browser.spec.ts +91 -0
- package/src/router/__tests__/page-head.spec.ts +44 -0
- package/src/router/__tests__/seo-head.spec.ts +121 -0
- 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 +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- 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.spec.tsx +316 -0
- package/src/router/providers/ReactServerProvider.ts +331 -315
- package/src/router/providers/ReactServerTemplateProvider.ts +775 -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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { $inject, Alepha, type Static } from "alepha";
|
|
2
|
+
import {
|
|
3
|
+
ssrManifestAtom,
|
|
4
|
+
type SsrManifestAtomSchema,
|
|
5
|
+
} from "../atoms/ssrManifestAtom.ts";
|
|
6
|
+
import type { PageRoute } from "./ReactPageProvider.ts";
|
|
7
|
+
import { PAGE_PRELOAD_KEY } from "../constants/PAGE_PRELOAD_KEY.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Provider for SSR manifest data used for module preloading.
|
|
11
|
+
*
|
|
12
|
+
* The manifest is populated at build time by embedding data into the
|
|
13
|
+
* generated index.js via the ssrManifestAtom. This eliminates filesystem
|
|
14
|
+
* reads at runtime, making it optimal for serverless deployments.
|
|
15
|
+
*
|
|
16
|
+
* Manifest files are generated during `vite build`:
|
|
17
|
+
* - manifest.json (client manifest)
|
|
18
|
+
* - ssr-manifest.json (SSR manifest)
|
|
19
|
+
* - preload-manifest.json (from viteAlephaSsrPreload plugin)
|
|
20
|
+
*/
|
|
21
|
+
export class SSRManifestProvider {
|
|
22
|
+
protected readonly alepha = $inject(Alepha);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the manifest from the store at runtime.
|
|
26
|
+
* This ensures the manifest is available even when set after module load.
|
|
27
|
+
*/
|
|
28
|
+
protected get manifest(): Static<SsrManifestAtomSchema> {
|
|
29
|
+
return (this.alepha.store.get(ssrManifestAtom) as Static<SsrManifestAtomSchema>) ?? {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the preload manifest.
|
|
34
|
+
*/
|
|
35
|
+
protected get preloadManifest(): PreloadManifest | undefined {
|
|
36
|
+
return this.manifest.preload;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the SSR manifest.
|
|
41
|
+
*/
|
|
42
|
+
protected get ssrManifest(): SSRManifest | undefined {
|
|
43
|
+
return this.manifest.ssr;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the client manifest.
|
|
48
|
+
*/
|
|
49
|
+
protected get clientManifest(): ClientManifest | undefined {
|
|
50
|
+
return this.manifest.client;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a preload key to its source path.
|
|
55
|
+
*
|
|
56
|
+
* The key is a short hash injected by viteAlephaSsrPreload plugin,
|
|
57
|
+
* which maps to the full source path in the preload manifest.
|
|
58
|
+
*
|
|
59
|
+
* @param key - Short hash key (e.g., "a1b2c3d4")
|
|
60
|
+
* @returns Source path (e.g., "src/pages/UserDetail.tsx") or undefined
|
|
61
|
+
*/
|
|
62
|
+
public resolvePreloadKey(key: string): string | undefined {
|
|
63
|
+
return this.preloadManifest?.[key];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all chunks required for a source file, including transitive dependencies.
|
|
68
|
+
*
|
|
69
|
+
* Uses the client manifest to recursively resolve all imported chunks,
|
|
70
|
+
* not just the direct chunks from the SSR manifest.
|
|
71
|
+
*
|
|
72
|
+
* @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
|
|
73
|
+
* @returns Array of chunk URLs to preload, or empty array if not found
|
|
74
|
+
*/
|
|
75
|
+
public getChunks(sourcePath: string): string[] {
|
|
76
|
+
if (!this.clientManifest) {
|
|
77
|
+
// Fallback to SSR manifest if client manifest not available
|
|
78
|
+
return this.getChunksFromSSRManifest(sourcePath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Find entry in client manifest
|
|
82
|
+
const entry = this.findManifestEntry(sourcePath);
|
|
83
|
+
if (!entry) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Recursively collect all chunks
|
|
88
|
+
const chunks = new Set<string>();
|
|
89
|
+
const visited = new Set<string>();
|
|
90
|
+
|
|
91
|
+
this.collectChunksRecursive(sourcePath, chunks, visited);
|
|
92
|
+
|
|
93
|
+
return Array.from(chunks);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find manifest entry for a source path, trying different extensions.
|
|
98
|
+
*/
|
|
99
|
+
protected findManifestEntry(sourcePath: string) {
|
|
100
|
+
if (!this.clientManifest) return undefined;
|
|
101
|
+
|
|
102
|
+
// Try exact match
|
|
103
|
+
if (this.clientManifest[sourcePath]) {
|
|
104
|
+
return this.clientManifest[sourcePath];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Try with different extensions
|
|
108
|
+
const basePath = sourcePath.replace(/\.[^.]+$/, "");
|
|
109
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
110
|
+
const pathWithExt = basePath + ext;
|
|
111
|
+
if (this.clientManifest[pathWithExt]) {
|
|
112
|
+
return this.clientManifest[pathWithExt];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Recursively collect all chunk URLs for a manifest entry.
|
|
121
|
+
*/
|
|
122
|
+
protected collectChunksRecursive(
|
|
123
|
+
key: string,
|
|
124
|
+
chunks: Set<string>,
|
|
125
|
+
visited: Set<string>,
|
|
126
|
+
): void {
|
|
127
|
+
if (visited.has(key)) return;
|
|
128
|
+
visited.add(key);
|
|
129
|
+
|
|
130
|
+
if (!this.clientManifest) return;
|
|
131
|
+
|
|
132
|
+
const entry = this.clientManifest[key];
|
|
133
|
+
if (!entry) return;
|
|
134
|
+
|
|
135
|
+
// Add main chunk file (with leading slash for URL)
|
|
136
|
+
if (entry.file) {
|
|
137
|
+
chunks.add("/" + entry.file);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Add CSS files
|
|
141
|
+
if (entry.css) {
|
|
142
|
+
for (const css of entry.css) {
|
|
143
|
+
chunks.add("/" + css);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Recursively process imports (but skip entry point)
|
|
148
|
+
if (entry.imports) {
|
|
149
|
+
for (const imp of entry.imports) {
|
|
150
|
+
// Skip the main entry point (index.html) - it's already being loaded
|
|
151
|
+
if (imp === "index.html" || imp.endsWith(".html")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
this.collectChunksRecursive(imp, chunks, visited);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Note: We intentionally do NOT follow dynamicImports
|
|
159
|
+
// Those are lazy-loaded and shouldn't be preloaded
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fallback to SSR manifest for chunk lookup.
|
|
164
|
+
*/
|
|
165
|
+
protected getChunksFromSSRManifest(sourcePath: string): string[] {
|
|
166
|
+
if (!this.ssrManifest) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Try exact match
|
|
171
|
+
if (this.ssrManifest[sourcePath]) {
|
|
172
|
+
return this.ssrManifest[sourcePath];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Try with different extensions
|
|
176
|
+
const basePath = sourcePath.replace(/\.[^.]+$/, "");
|
|
177
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
178
|
+
const pathWithExt = basePath + ext;
|
|
179
|
+
if (this.ssrManifest[pathWithExt]) {
|
|
180
|
+
return this.ssrManifest[pathWithExt];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Collect modulepreload links for a route and its parent chain.
|
|
189
|
+
*/
|
|
190
|
+
public collectPreloadLinks(
|
|
191
|
+
route: PageRoute,
|
|
192
|
+
): Array<{ rel: string; href: string; as?: string; crossorigin?: string }> {
|
|
193
|
+
if (!this.isAvailable()) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const preloadPaths: string[] = [];
|
|
198
|
+
let current: PageRoute | undefined = route;
|
|
199
|
+
|
|
200
|
+
while (current) {
|
|
201
|
+
const preloadKey = current[PAGE_PRELOAD_KEY];
|
|
202
|
+
if (preloadKey) {
|
|
203
|
+
const sourcePath =
|
|
204
|
+
this.resolvePreloadKey(preloadKey);
|
|
205
|
+
if (sourcePath) {
|
|
206
|
+
preloadPaths.push(sourcePath);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
current = current.parent;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (preloadPaths.length === 0) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const chunks = this.getChunksForMultiple(preloadPaths);
|
|
217
|
+
|
|
218
|
+
return chunks.map((href) => {
|
|
219
|
+
if (href.endsWith(".css")) {
|
|
220
|
+
return { rel: "preload", href, as: "style", crossorigin: "" };
|
|
221
|
+
}
|
|
222
|
+
return { rel: "modulepreload", href };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get all chunks for multiple source files.
|
|
228
|
+
*
|
|
229
|
+
* @param sourcePaths - Array of source file paths
|
|
230
|
+
* @returns Deduplicated array of chunk URLs
|
|
231
|
+
*/
|
|
232
|
+
public getChunksForMultiple(sourcePaths: string[]): string[] {
|
|
233
|
+
const allChunks = new Set<string>();
|
|
234
|
+
|
|
235
|
+
for (const path of sourcePaths) {
|
|
236
|
+
const chunks = this.getChunks(path);
|
|
237
|
+
for (const chunk of chunks) {
|
|
238
|
+
allChunks.add(chunk);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return Array.from(allChunks);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if manifests are loaded and available.
|
|
247
|
+
*/
|
|
248
|
+
public isAvailable(): boolean {
|
|
249
|
+
return this.clientManifest !== undefined || this.ssrManifest !== undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Cached entry assets - computed once at first access.
|
|
254
|
+
*/
|
|
255
|
+
protected cachedEntryAssets: EntryAssets | null = null;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the entry point assets (main entry.js and associated CSS files).
|
|
259
|
+
*
|
|
260
|
+
* These assets are always required for all pages and can be preloaded
|
|
261
|
+
* before page-specific loaders run.
|
|
262
|
+
*
|
|
263
|
+
* @returns Entry assets with js and css paths, or null if manifest unavailable
|
|
264
|
+
*/
|
|
265
|
+
public getEntryAssets(): EntryAssets | null {
|
|
266
|
+
if (this.cachedEntryAssets) {
|
|
267
|
+
return this.cachedEntryAssets;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!this.clientManifest) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Find the entry point in the client manifest
|
|
275
|
+
for (const [key, entry] of Object.entries(this.clientManifest)) {
|
|
276
|
+
if (entry.isEntry) {
|
|
277
|
+
this.cachedEntryAssets = {
|
|
278
|
+
js: "/" + entry.file,
|
|
279
|
+
css: entry.css?.map((css) => "/" + css) ?? [],
|
|
280
|
+
};
|
|
281
|
+
return this.cachedEntryAssets;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build preload link tags for entry assets.
|
|
290
|
+
*
|
|
291
|
+
* @returns Array of link objects ready to be rendered
|
|
292
|
+
*/
|
|
293
|
+
public getEntryPreloadLinks(): Array<{
|
|
294
|
+
rel: string;
|
|
295
|
+
href: string;
|
|
296
|
+
as?: string;
|
|
297
|
+
crossorigin?: string;
|
|
298
|
+
}> {
|
|
299
|
+
const assets = this.getEntryAssets();
|
|
300
|
+
if (!assets) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const links: Array<{
|
|
305
|
+
rel: string;
|
|
306
|
+
href: string;
|
|
307
|
+
as?: string;
|
|
308
|
+
crossorigin?: string;
|
|
309
|
+
}> = [];
|
|
310
|
+
|
|
311
|
+
// Add CSS preloads first (critical for rendering)
|
|
312
|
+
for (const css of assets.css) {
|
|
313
|
+
links.push({ rel: "stylesheet", href: css, crossorigin: "" });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add entry JS modulepreload
|
|
317
|
+
if (assets.js) {
|
|
318
|
+
links.push({ rel: "modulepreload", href: assets.js });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return links;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Entry assets structure containing the main entry JS and associated CSS files.
|
|
330
|
+
*/
|
|
331
|
+
export interface EntryAssets {
|
|
332
|
+
/** Main entry JavaScript file (e.g., "/assets/entry.abc123.js") */
|
|
333
|
+
js?: string;
|
|
334
|
+
/** Associated CSS files (e.g., ["/assets/style.abc123.css"]) */
|
|
335
|
+
css: string[];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* SSR Manifest structure from Vite.
|
|
340
|
+
* Maps source file paths to their required chunks/assets.
|
|
341
|
+
*/
|
|
342
|
+
export type SSRManifest = Record<string, string[]>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Client manifest structure from Vite.
|
|
346
|
+
* Maps source files to their output information.
|
|
347
|
+
*/
|
|
348
|
+
export interface ClientManifest {
|
|
349
|
+
[key: string]: {
|
|
350
|
+
file: string;
|
|
351
|
+
src?: string;
|
|
352
|
+
isEntry?: boolean;
|
|
353
|
+
isDynamicEntry?: boolean;
|
|
354
|
+
imports?: string[];
|
|
355
|
+
dynamicImports?: string[];
|
|
356
|
+
css?: string[];
|
|
357
|
+
assets?: string[];
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Preload manifest mapping short keys to source paths.
|
|
363
|
+
* Generated by viteAlephaSsrPreload plugin at build time.
|
|
364
|
+
*/
|
|
365
|
+
export type PreloadManifest = Record<string, string>;
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
PagePrimitiveRenderResult,
|
|
6
6
|
} from "../primitives/$page.ts";
|
|
7
7
|
import { ReactServerProvider } from "../providers/ReactServerProvider.ts";
|
|
8
|
+
import { ReactServerTemplateProvider } from "../providers/ReactServerTemplateProvider.ts";
|
|
8
9
|
import { ReactPageService } from "./ReactPageService.ts";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -12,6 +13,7 @@ import { ReactPageService } from "./ReactPageService.ts";
|
|
|
12
13
|
*/
|
|
13
14
|
export class ReactPageServerService extends ReactPageService {
|
|
14
15
|
protected readonly reactServerProvider = $inject(ReactServerProvider);
|
|
16
|
+
protected readonly templateProvider = $inject(ReactServerTemplateProvider);
|
|
15
17
|
protected readonly serverProvider = $inject(ServerProvider);
|
|
16
18
|
|
|
17
19
|
public async render(
|
|
@@ -36,9 +38,9 @@ export class ReactPageServerService extends ReactPageService {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
// take only text inside the root div
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
return { html:
|
|
41
|
+
const rootContent = this.templateProvider.extractRootContent(html);
|
|
42
|
+
if (rootContent !== undefined) {
|
|
43
|
+
return { html: rootContent, response };
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
throw new AlephaError("Invalid HTML response");
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { $inject, Alepha } from "alepha";
|
|
2
2
|
import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
|
|
3
3
|
import {
|
|
4
|
-
type AnchorProps,
|
|
4
|
+
type AnchorProps, type PageRoute,
|
|
5
5
|
ReactPageProvider,
|
|
6
6
|
type ReactRouterState,
|
|
7
7
|
} from "../providers/ReactPageProvider.ts";
|
|
8
|
-
import type { PagePrimitive } from "../primitives/$page.ts";
|
|
8
|
+
import type { PagePrimitive, PagePrimitiveOptions } from "../primitives/$page.ts";
|
|
9
9
|
|
|
10
10
|
export interface RouterGoOptions {
|
|
11
11
|
replace?: boolean;
|
|
@@ -70,7 +70,7 @@ export class ReactRouter<T extends object> {
|
|
|
70
70
|
params?: Record<string, any>;
|
|
71
71
|
query?: Record<string, any>;
|
|
72
72
|
} = {},
|
|
73
|
-
) {
|
|
73
|
+
): any { // TODO: improve typing (or just remove this method)
|
|
74
74
|
const page = this.pageApi.page(name as string);
|
|
75
75
|
if (!page.lazy && !page.component) {
|
|
76
76
|
return {
|