@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.
Files changed (57) hide show
  1. package/dist/auth/index.browser.js +29 -14
  2. package/dist/auth/index.browser.js.map +1 -1
  3. package/dist/auth/index.js +960 -195
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/core/index.d.ts +4 -0
  6. package/dist/core/index.d.ts.map +1 -1
  7. package/dist/core/index.js +7 -4
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/head/index.browser.js +59 -19
  10. package/dist/head/index.browser.js.map +1 -1
  11. package/dist/head/index.d.ts +99 -560
  12. package/dist/head/index.d.ts.map +1 -1
  13. package/dist/head/index.js +92 -87
  14. package/dist/head/index.js.map +1 -1
  15. package/dist/router/index.browser.js +30 -15
  16. package/dist/router/index.browser.js.map +1 -1
  17. package/dist/router/index.d.ts +616 -192
  18. package/dist/router/index.d.ts.map +1 -1
  19. package/dist/router/index.js +961 -196
  20. package/dist/router/index.js.map +1 -1
  21. package/package.json +4 -4
  22. package/src/auth/__tests__/$auth.spec.ts +188 -0
  23. package/src/core/__tests__/Router.spec.tsx +169 -0
  24. package/src/core/hooks/useAction.browser.spec.tsx +569 -0
  25. package/src/core/hooks/useAction.ts +11 -0
  26. package/src/form/hooks/useForm.browser.spec.tsx +366 -0
  27. package/src/head/helpers/SeoExpander.spec.ts +203 -0
  28. package/src/head/hooks/useHead.spec.tsx +288 -0
  29. package/src/head/index.ts +11 -28
  30. package/src/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  31. package/src/head/providers/BrowserHeadProvider.ts +25 -19
  32. package/src/head/providers/HeadProvider.ts +76 -10
  33. package/src/head/providers/ServerHeadProvider.ts +22 -138
  34. package/src/i18n/__tests__/integration.spec.tsx +239 -0
  35. package/src/i18n/components/Localize.spec.tsx +357 -0
  36. package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  37. package/src/i18n/providers/I18nProvider.spec.ts +389 -0
  38. package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
  39. package/src/router/__tests__/page-head.spec.ts +44 -0
  40. package/src/router/__tests__/seo-head.spec.ts +121 -0
  41. package/src/router/atoms/ssrManifestAtom.ts +60 -0
  42. package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  43. package/src/router/errors/Redirection.ts +1 -1
  44. package/src/router/index.shared.ts +1 -0
  45. package/src/router/index.ts +16 -2
  46. package/src/router/primitives/$page.browser.spec.tsx +702 -0
  47. package/src/router/primitives/$page.spec.tsx +702 -0
  48. package/src/router/primitives/$page.ts +46 -10
  49. package/src/router/providers/ReactBrowserProvider.ts +14 -29
  50. package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
  51. package/src/router/providers/ReactPageProvider.ts +11 -4
  52. package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
  53. package/src/router/providers/ReactServerProvider.ts +331 -315
  54. package/src/router/providers/ReactServerTemplateProvider.ts +775 -0
  55. package/src/router/providers/SSRManifestProvider.ts +365 -0
  56. package/src/router/services/ReactPageServerService.ts +5 -3
  57. 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 match = html.match(this.reactServerProvider.ROOT_DIV_REGEX);
40
- if (match) {
41
- return { html: match[3], response };
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 {