@alepha/react 0.15.0 → 0.15.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.
Files changed (81) hide show
  1. package/dist/auth/index.browser.js +603 -242
  2. package/dist/auth/index.browser.js.map +1 -1
  3. package/dist/auth/index.d.ts +6 -6
  4. package/dist/auth/index.d.ts.map +1 -1
  5. package/dist/auth/index.js +1296 -922
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/core/index.d.ts +128 -128
  8. package/dist/core/index.d.ts.map +1 -1
  9. package/dist/core/index.js +20 -20
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/form/index.d.ts +36 -36
  12. package/dist/form/index.d.ts.map +1 -1
  13. package/dist/form/index.js +15 -15
  14. package/dist/form/index.js.map +1 -1
  15. package/dist/head/index.browser.js +20 -0
  16. package/dist/head/index.browser.js.map +1 -1
  17. package/dist/head/index.d.ts +73 -65
  18. package/dist/head/index.d.ts.map +1 -1
  19. package/dist/head/index.js +20 -0
  20. package/dist/head/index.js.map +1 -1
  21. package/dist/i18n/index.d.ts +37 -37
  22. package/dist/i18n/index.d.ts.map +1 -1
  23. package/dist/i18n/index.js.map +1 -1
  24. package/dist/router/index.browser.js +605 -244
  25. package/dist/router/index.browser.js.map +1 -1
  26. package/dist/router/index.d.ts +539 -550
  27. package/dist/router/index.d.ts.map +1 -1
  28. package/dist/router/index.js +1296 -922
  29. package/dist/router/index.js.map +1 -1
  30. package/dist/websocket/index.d.ts +38 -38
  31. package/dist/websocket/index.d.ts.map +1 -1
  32. package/package.json +6 -6
  33. package/src/auth/__tests__/$auth.spec.ts +162 -147
  34. package/src/auth/index.ts +9 -3
  35. package/src/auth/services/ReactAuth.ts +15 -5
  36. package/src/core/hooks/useAction.ts +1 -2
  37. package/src/core/index.ts +4 -4
  38. package/src/form/errors/FormValidationError.ts +4 -6
  39. package/src/form/hooks/useFormState.ts +1 -1
  40. package/src/form/index.ts +1 -1
  41. package/src/form/services/FormModel.ts +31 -25
  42. package/src/head/helpers/SeoExpander.ts +2 -1
  43. package/src/head/hooks/useHead.spec.tsx +2 -2
  44. package/src/head/index.browser.ts +2 -2
  45. package/src/head/index.ts +4 -4
  46. package/src/head/interfaces/Head.ts +15 -3
  47. package/src/head/primitives/$head.ts +2 -5
  48. package/src/head/providers/BrowserHeadProvider.ts +55 -0
  49. package/src/head/providers/HeadProvider.ts +4 -1
  50. package/src/i18n/__tests__/integration.spec.tsx +1 -1
  51. package/src/i18n/components/Localize.spec.tsx +2 -2
  52. package/src/i18n/hooks/useI18n.browser.spec.tsx +2 -2
  53. package/src/i18n/index.ts +1 -1
  54. package/src/i18n/primitives/$dictionary.ts +1 -1
  55. package/src/i18n/providers/I18nProvider.spec.ts +1 -1
  56. package/src/i18n/providers/I18nProvider.ts +1 -1
  57. package/src/router/__tests__/page-head-browser.browser.spec.ts +5 -1
  58. package/src/router/__tests__/page-head.spec.ts +11 -7
  59. package/src/router/__tests__/seo-head.spec.ts +7 -3
  60. package/src/router/atoms/ssrManifestAtom.ts +2 -11
  61. package/src/router/components/ErrorViewer.tsx +626 -167
  62. package/src/router/components/Link.tsx +4 -2
  63. package/src/router/components/NestedView.tsx +7 -9
  64. package/src/router/components/NotFound.tsx +2 -2
  65. package/src/router/hooks/useQueryParams.ts +1 -1
  66. package/src/router/hooks/useRouter.ts +1 -1
  67. package/src/router/hooks/useRouterState.ts +1 -1
  68. package/src/router/index.browser.ts +10 -11
  69. package/src/router/index.shared.ts +7 -7
  70. package/src/router/index.ts +10 -7
  71. package/src/router/primitives/$page.browser.spec.tsx +6 -1
  72. package/src/router/primitives/$page.spec.tsx +7 -1
  73. package/src/router/primitives/$page.ts +5 -9
  74. package/src/router/providers/ReactBrowserProvider.ts +17 -6
  75. package/src/router/providers/ReactBrowserRouterProvider.ts +1 -1
  76. package/src/router/providers/ReactPageProvider.ts +4 -3
  77. package/src/router/providers/ReactServerProvider.ts +29 -37
  78. package/src/router/providers/ReactServerTemplateProvider.ts +300 -137
  79. package/src/router/providers/SSRManifestProvider.ts +17 -60
  80. package/src/router/services/ReactPageService.ts +4 -1
  81. package/src/router/services/ReactRouter.ts +6 -5
@@ -1,5 +1,12 @@
1
1
  import type { TArray } from "alepha";
2
- import { $inject, Alepha, type Static, t, type TObject, type TSchema, } from "alepha";
2
+ import {
3
+ $inject,
4
+ Alepha,
5
+ type Static,
6
+ type TObject,
7
+ type TSchema,
8
+ t,
9
+ } from "alepha";
3
10
  import { $logger } from "alepha/logger";
4
11
  import type { ChangeEvent, InputHTMLAttributes } from "react";
5
12
 
@@ -217,7 +224,6 @@ export class FormModel<T extends TObject> {
217
224
  }
218
225
 
219
226
  if (prop in schema.properties) {
220
-
221
227
  // // it's a nested object, create another proxy
222
228
  // if (t.schema.isObject(schema.properties[prop])) {
223
229
  // return this.createProxyFromSchema(
@@ -284,13 +290,17 @@ export class FormModel<T extends TObject> {
284
290
  options.onChange(key, typedValue, context.store);
285
291
  }
286
292
 
287
- this.alepha.events.emit("form:change", {
288
- id: this.id,
289
- path: path,
290
- value: typedValue,
291
- }, {
292
- catch: true
293
- });
293
+ this.alepha.events.emit(
294
+ "form:change",
295
+ {
296
+ id: this.id,
297
+ path: path,
298
+ value: typedValue,
299
+ },
300
+ {
301
+ catch: true,
302
+ },
303
+ );
294
304
 
295
305
  if (sync) {
296
306
  const inputElement = window.document.querySelector(
@@ -408,14 +418,10 @@ export class FormModel<T extends TObject> {
408
418
  set,
409
419
  form: this,
410
420
  required,
411
- items: this.createProxyFromSchema(
412
- options,
413
- field,
414
- {
415
- parent: key,
416
- store: context.store,
417
- },
418
- )
421
+ items: this.createProxyFromSchema(options, field, {
422
+ parent: key,
423
+ store: context.store,
424
+ }),
419
425
  } as ObjectInputField<any>;
420
426
  }
421
427
 
@@ -460,7 +466,8 @@ export class FormModel<T extends TObject> {
460
466
  // Handle string representations from Select components (Yes/No dropdown)
461
467
  if (input === "true") return true;
462
468
  if (input === "false") return false;
463
- if (input === "" || input === null || input === undefined) return undefined;
469
+ if (input === "" || input === null || input === undefined)
470
+ return undefined;
464
471
  // Handle actual boolean values
465
472
  return !!input;
466
473
  }
@@ -520,12 +527,11 @@ export interface FormEventLike {
520
527
  stopPropagation?: () => void;
521
528
  }
522
529
 
523
- export type InputField<T extends TSchema> =
524
- T extends TObject
525
- ? ObjectInputField<T>
526
- : T extends TArray<infer U>
527
- ? ArrayInputField<U>
528
- : BaseInputField;
530
+ export type InputField<T extends TSchema> = T extends TObject
531
+ ? ObjectInputField<T>
532
+ : T extends TArray<infer U>
533
+ ? ArrayInputField<U>
534
+ : BaseInputField;
529
535
 
530
536
  export interface BaseInputField {
531
537
  path: string;
@@ -542,7 +548,7 @@ export interface ObjectInputField<T extends TObject> extends BaseInputField {
542
548
  }
543
549
 
544
550
  export interface ArrayInputField<T extends TSchema> extends BaseInputField {
545
- items: Array<InputField<T>>
551
+ items: Array<InputField<T>>;
546
552
  }
547
553
 
548
554
  export type InputHTMLAttributesLike = Pick<
@@ -113,7 +113,8 @@ export class SeoExpander {
113
113
  meta.push({
114
114
  name: "twitter:card",
115
115
  content:
116
- head.twitter?.card ?? (twitterImage ? "summary_large_image" : "summary"),
116
+ head.twitter?.card ??
117
+ (twitterImage ? "summary_large_image" : "summary"),
117
118
  });
118
119
  }
119
120
  if (head.url) {
@@ -1,10 +1,10 @@
1
1
  import { AlephaContext } from "@alepha/react";
2
- import type { Head } from "../index.ts";
3
- import { useHead } from "../index.ts";
4
2
  import { render } from "@testing-library/react";
5
3
  import { Alepha } from "alepha";
6
4
  import { act, type ReactNode } from "react";
7
5
  import { beforeEach, describe, it, vi } from "vitest";
6
+ import type { Head } from "../index.ts";
7
+ import { useHead } from "../index.ts";
8
8
 
9
9
  /**
10
10
  * @vitest-environment jsdom
@@ -5,10 +5,10 @@ import { BrowserHeadProvider } from "./providers/BrowserHeadProvider.ts";
5
5
 
6
6
  // ---------------------------------------------------------------------------------------------------------------------
7
7
 
8
- export * from "./primitives/$head.ts";
8
+ export * from "./helpers/SeoExpander.ts";
9
9
  export * from "./hooks/useHead.ts";
10
10
  export * from "./interfaces/Head.ts";
11
- export * from "./helpers/SeoExpander.ts";
11
+ export * from "./primitives/$head.ts";
12
12
  export * from "./providers/BrowserHeadProvider.ts";
13
13
 
14
14
  // ---------------------------------------------------------------------------------------------------------------------
package/src/head/index.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import { AlephaReact } from "@alepha/react";
2
2
  import { $module } from "alepha";
3
+ import { SeoExpander } from "./helpers/SeoExpander.ts";
3
4
  import { $head } from "./primitives/$head.ts";
4
5
  import { BrowserHeadProvider } from "./providers/BrowserHeadProvider.ts";
5
6
  import { HeadProvider } from "./providers/HeadProvider.ts";
6
- import { SeoExpander } from "./helpers/SeoExpander.ts";
7
7
  import { ServerHeadProvider } from "./providers/ServerHeadProvider.ts";
8
8
 
9
9
  // ---------------------------------------------------------------------------------------------------------------------
10
10
 
11
- export * from "./primitives/$head.ts";
11
+ export * from "./helpers/SeoExpander.ts";
12
12
  export * from "./hooks/useHead.ts";
13
13
  export * from "./interfaces/Head.ts";
14
- export * from "./helpers/SeoExpander.ts";
15
- export * from "./providers/ServerHeadProvider.ts";
14
+ export * from "./primitives/$head.ts";
16
15
  export * from "./providers/BrowserHeadProvider.ts";
16
+ export * from "./providers/ServerHeadProvider.ts";
17
17
 
18
18
  // ---------------------------------------------------------------------------------------------------------------------
19
19
 
@@ -74,9 +74,21 @@ export interface SimpleHead {
74
74
  /** Meta tags - supports both name and property attributes */
75
75
  meta?: Array<HeadMeta>;
76
76
  /** Link tags (e.g., stylesheets, preload, canonical) */
77
- link?: Array<{ rel: string; href: string }>;
78
- /** Script tags - any valid script attributes (src, type, async, defer, etc.) */
79
- script?: Array<Record<string, string | boolean>>;
77
+ link?: Array<{
78
+ rel: string;
79
+ href: string;
80
+ type?: string;
81
+ as?: string;
82
+ crossorigin?: string;
83
+ }>;
84
+ /** Script tags - string for inline code, or object with attributes */
85
+ script?: Array<
86
+ | string
87
+ | (Record<string, string | boolean | undefined> & {
88
+ /** Inline JavaScript code */
89
+ content?: string;
90
+ })
91
+ >;
80
92
  }
81
93
 
82
94
  export interface HeadMeta {
@@ -1,4 +1,4 @@
1
- import { $inject, createPrimitive, Primitive, KIND } from "alepha";
1
+ import { $inject, createPrimitive, KIND, Primitive } from "alepha";
2
2
  import type { Head } from "../interfaces/Head.ts";
3
3
  import { HeadProvider } from "../providers/HeadProvider.ts";
4
4
 
@@ -18,10 +18,7 @@ export type HeadPrimitiveOptions = Head | (() => Head);
18
18
  export class HeadPrimitive extends Primitive<HeadPrimitiveOptions> {
19
19
  protected readonly provider = $inject(HeadProvider);
20
20
  protected onInit() {
21
- this.provider.global = [
22
- ...(this.provider.global ?? []),
23
- this.options,
24
- ];
21
+ this.provider.global = [...(this.provider.global ?? []), this.options];
25
22
  }
26
23
  }
27
24
 
@@ -116,10 +116,65 @@ export class BrowserHeadProvider {
116
116
  link = document.createElement("link");
117
117
  link.setAttribute("rel", rel);
118
118
  link.setAttribute("href", href);
119
+ if (it.type) {
120
+ link.setAttribute("type", it.type);
121
+ }
122
+ if (it.as) {
123
+ link.setAttribute("as", it.as);
124
+ }
125
+ if (it.crossorigin != null) {
126
+ link.setAttribute("crossorigin", "");
127
+ }
119
128
  document.head.appendChild(link);
120
129
  }
121
130
  }
122
131
  }
132
+
133
+ if (head.script) {
134
+ for (const it of head.script) {
135
+ this.renderScriptTag(document, it);
136
+ }
137
+ }
138
+ }
139
+
140
+ protected renderScriptTag(
141
+ document: Document,
142
+ script:
143
+ | string
144
+ | (Record<string, string | boolean | undefined> & { content?: string }),
145
+ ): void {
146
+ const el = document.createElement("script");
147
+
148
+ // Handle plain string as inline script
149
+ if (typeof script === "string") {
150
+ el.textContent = script;
151
+ document.head.appendChild(el);
152
+ return;
153
+ }
154
+
155
+ const { content, ...attrs } = script;
156
+
157
+ // For scripts with src, check if already exists
158
+ if (attrs.src) {
159
+ const existing = document.querySelector(`script[src="${attrs.src}"]`);
160
+ if (existing) {
161
+ return;
162
+ }
163
+ }
164
+
165
+ for (const [key, value] of Object.entries(attrs)) {
166
+ if (value === true) {
167
+ el.setAttribute(key, "");
168
+ } else if (value !== undefined && value !== false) {
169
+ el.setAttribute(key, String(value));
170
+ }
171
+ }
172
+
173
+ if (content) {
174
+ el.textContent = content;
175
+ }
176
+
177
+ document.head.appendChild(el);
123
178
  }
124
179
 
125
180
  protected renderMetaTag(document: Document, meta: HeadMeta): void {
@@ -136,7 +136,10 @@ export class HeadProvider {
136
136
  }
137
137
 
138
138
  if (head.script) {
139
- state.head.script = [...(state.head.script ?? []), ...(head.script ?? [])];
139
+ state.head.script = [
140
+ ...(state.head.script ?? []),
141
+ ...(head.script ?? []),
142
+ ];
140
143
  }
141
144
  }
142
145
  }
@@ -1,7 +1,7 @@
1
1
  import { Alepha } from "alepha";
2
2
  import { describe, test } from "vitest";
3
- import { $dictionary } from "../primitives/$dictionary.ts";
4
3
  import { AlephaReactI18n } from "../index.ts";
4
+ import { $dictionary } from "../primitives/$dictionary.ts";
5
5
  import { I18nProvider } from "../providers/I18nProvider.ts";
6
6
 
7
7
  describe("I18n Integration Tests", () => {
@@ -1,10 +1,10 @@
1
1
  import { AlephaContext } from "@alepha/react";
2
- import { I18nProvider, Localize } from "../index.ts";
3
- import { Alepha, t } from "alepha";
2
+ import { Alepha } from "alepha";
4
3
  import { DateTimeProvider } from "alepha/datetime";
5
4
  import type { ReactNode } from "react";
6
5
  import { renderToString } from "react-dom/server";
7
6
  import { describe, expect, it } from "vitest";
7
+ import { I18nProvider, Localize } from "../index.ts";
8
8
 
9
9
  describe("<Localize/>", () => {
10
10
  const setup = () => {
@@ -4,10 +4,10 @@ import { Alepha } from "alepha";
4
4
  import type { ReactNode } from "react";
5
5
  import { act } from "react";
6
6
  import { describe, test } from "vitest";
7
- import { $dictionary } from "../primitives/$dictionary.ts";
8
- import { useI18n } from "./useI18n.ts";
9
7
  import { AlephaReactI18n } from "../index.ts";
8
+ import { $dictionary } from "../primitives/$dictionary.ts";
10
9
  import { I18nProvider } from "../providers/I18nProvider.ts";
10
+ import { useI18n } from "./useI18n.ts";
11
11
 
12
12
  describe("useI18n hook", () => {
13
13
  const renderWithAlepha = (alepha: Alepha, element: ReactNode) => {
package/src/i18n/index.ts CHANGED
@@ -6,8 +6,8 @@ import { I18nProvider } from "./providers/I18nProvider.ts";
6
6
 
7
7
  export type { LocalizeProps } from "./components/Localize.tsx";
8
8
  export { default as Localize } from "./components/Localize.tsx";
9
- export * from "./primitives/$dictionary.ts";
10
9
  export * from "./hooks/useI18n.ts";
10
+ export * from "./primitives/$dictionary.ts";
11
11
  export * from "./providers/I18nProvider.ts";
12
12
 
13
13
  // ---------------------------------------------------------------------------------------------------------------------
@@ -1,4 +1,4 @@
1
- import { $inject, type Async, createPrimitive, Primitive, KIND } from "alepha";
1
+ import { $inject, type Async, createPrimitive, KIND, Primitive } from "alepha";
2
2
  import { I18nProvider } from "../providers/I18nProvider.ts";
3
3
 
4
4
  /**
@@ -1,7 +1,7 @@
1
1
  import { Alepha } from "alepha";
2
2
  import { describe, test } from "vitest";
3
- import { $dictionary } from "../primitives/$dictionary.ts";
4
3
  import { AlephaReactI18n } from "../index.ts";
4
+ import { $dictionary } from "../primitives/$dictionary.ts";
5
5
  import { I18nProvider } from "./I18nProvider.ts";
6
6
 
7
7
  describe("I18nProvider", () => {
@@ -15,7 +15,7 @@ export class I18nProvider<
15
15
  protected cookie = $cookie({
16
16
  name: "lang",
17
17
  schema: t.text(),
18
- ttl: [1, "year"]
18
+ ttl: [1, "year"],
19
19
  });
20
20
 
21
21
  public readonly registry: Array<{
@@ -1,4 +1,8 @@
1
- import { AlephaReactHead, BrowserHeadProvider, type Head } from "@alepha/react/head";
1
+ import {
2
+ AlephaReactHead,
3
+ BrowserHeadProvider,
4
+ type Head,
5
+ } from "@alepha/react/head";
2
6
  import { Alepha } from "alepha";
3
7
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
8
  import { $page } from "../index.browser.ts";
@@ -1,6 +1,6 @@
1
+ import { $head, AlephaReactHead } from "@alepha/react/head";
1
2
  import { Alepha } from "alepha";
2
3
  import { describe, it } from "vitest";
3
- import { $head, AlephaReactHead } from "@alepha/react/head";
4
4
  import { $page } from "../index.ts";
5
5
 
6
6
  class App {
@@ -31,14 +31,18 @@ describe("PageHead", () => {
31
31
  const result = await a.hello.render({ html: true, hydration: false });
32
32
 
33
33
  // Check key parts of the HTML output (streaming adds newlines between sections)
34
- expect(result.html).toContain('<!DOCTYPE html>');
34
+ expect(result.html).toContain("<!DOCTYPE html>");
35
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">');
36
+ expect(result.html).toContain("<title>Hello World</title>");
37
+ expect(result.html).toContain(
38
+ '<meta name="description" content="This is a test page.">',
39
+ );
40
+ expect(result.html).toContain(
41
+ '<meta name="keywords" content="test, alepha, react">',
42
+ );
39
43
  expect(result.html).toContain('<body class="hello-world">');
40
44
  expect(result.html).toContain('<div id="root">');
41
- expect(result.html).toContain('</body>');
42
- expect(result.html).toContain('</html>');
45
+ expect(result.html).toContain("</body>");
46
+ expect(result.html).toContain("</html>");
43
47
  });
44
48
  });
@@ -1,6 +1,6 @@
1
+ import { $head, AlephaReactHead } from "@alepha/react/head";
1
2
  import { Alepha } from "alepha";
2
3
  import { describe, it } from "vitest";
3
- import { $head, AlephaReactHead } from "@alepha/react/head";
4
4
  import { $page } from "../index.ts";
5
5
 
6
6
  class App {
@@ -49,7 +49,9 @@ describe("SEO Head", () => {
49
49
  expect(result.html).toContain(
50
50
  '<meta property="og:url" content="https://alepha.dev/">',
51
51
  );
52
- expect(result.html).toContain('<meta property="og:type" content="website">');
52
+ expect(result.html).toContain(
53
+ '<meta property="og:type" content="website">',
54
+ );
53
55
  expect(result.html).toContain(
54
56
  '<meta property="og:site_name" content="Alepha">',
55
57
  );
@@ -113,7 +115,9 @@ describe("SEO Head on Page", () => {
113
115
  expect(result.html).toContain(
114
116
  '<meta name="description" content="Read our latest articles">',
115
117
  );
116
- expect(result.html).toContain('<meta property="og:type" content="article">');
118
+ expect(result.html).toContain(
119
+ '<meta property="og:type" content="article">',
120
+ );
117
121
  expect(result.html).toContain(
118
122
  '<meta property="og:image" content="https://example.com/blog-og.png">',
119
123
  );
@@ -10,26 +10,18 @@ export const ssrManifestAtomSchema = t.object({
10
10
  */
11
11
  preload: t.optional(t.record(t.string(), t.string())),
12
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
13
  /**
19
14
  * Client manifest mapping source files to their output information.
15
+ * Only includes fields actually used for preloading.
20
16
  */
21
17
  client: t.optional(
22
18
  t.record(
23
19
  t.string(),
24
20
  t.object({
25
21
  file: t.string(),
26
- src: t.optional(t.string()),
27
22
  isEntry: t.optional(t.boolean()),
28
- isDynamicEntry: t.optional(t.boolean()),
29
23
  imports: t.optional(t.array(t.string())),
30
- dynamicImports: t.optional(t.array(t.string())),
31
24
  css: t.optional(t.array(t.string())),
32
- assets: t.optional(t.array(t.string())),
33
25
  }),
34
26
  ),
35
27
  ),
@@ -49,8 +41,7 @@ export type SsrManifestAtomSchema = typeof ssrManifestAtomSchema;
49
41
  *
50
42
  * The manifest includes:
51
43
  * - 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
44
+ * - client: Maps source files to their output info (file, imports, css)
54
45
  */
55
46
  export const ssrManifestAtom = $atom({
56
47
  name: "alepha.react.ssr.manifest",