@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.
Files changed (57) hide show
  1. package/README.md +10 -0
  2. package/dist/auth/index.browser.js +29 -14
  3. package/dist/auth/index.browser.js.map +1 -1
  4. package/dist/auth/index.d.ts +4 -4
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/index.js +950 -194
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/core/index.d.ts +118 -118
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/form/index.d.ts +27 -28
  11. package/dist/form/index.d.ts.map +1 -1
  12. package/dist/head/index.browser.js +59 -19
  13. package/dist/head/index.browser.js.map +1 -1
  14. package/dist/head/index.d.ts +105 -576
  15. package/dist/head/index.d.ts.map +1 -1
  16. package/dist/head/index.js +91 -87
  17. package/dist/head/index.js.map +1 -1
  18. package/dist/i18n/index.d.ts +33 -33
  19. package/dist/i18n/index.d.ts.map +1 -1
  20. package/dist/router/index.browser.js +30 -15
  21. package/dist/router/index.browser.js.map +1 -1
  22. package/dist/router/index.d.ts +827 -403
  23. package/dist/router/index.d.ts.map +1 -1
  24. package/dist/router/index.js +951 -195
  25. package/dist/router/index.js.map +1 -1
  26. package/dist/websocket/index.d.ts +38 -39
  27. package/dist/websocket/index.d.ts.map +1 -1
  28. package/package.json +5 -5
  29. package/src/auth/__tests__/$auth.spec.ts +10 -11
  30. package/src/core/__tests__/Router.spec.tsx +4 -4
  31. package/src/head/{__tests__/expandSeo.spec.ts → helpers/SeoExpander.spec.ts} +1 -1
  32. package/src/head/index.ts +10 -28
  33. package/src/head/providers/BrowserHeadProvider.browser.spec.ts +1 -76
  34. package/src/head/providers/BrowserHeadProvider.ts +25 -19
  35. package/src/head/providers/HeadProvider.ts +76 -10
  36. package/src/head/providers/ServerHeadProvider.ts +22 -138
  37. package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
  38. package/src/router/__tests__/page-head.spec.ts +44 -0
  39. package/src/{head → router}/__tests__/seo-head.spec.ts +2 -2
  40. package/src/router/atoms/ssrManifestAtom.ts +60 -0
  41. package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  42. package/src/router/errors/Redirection.ts +1 -1
  43. package/src/router/index.shared.ts +1 -0
  44. package/src/router/index.ts +16 -2
  45. package/src/router/primitives/$page.browser.spec.tsx +15 -15
  46. package/src/router/primitives/$page.spec.tsx +18 -18
  47. package/src/router/primitives/$page.ts +46 -10
  48. package/src/router/providers/ReactBrowserProvider.ts +14 -29
  49. package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
  50. package/src/router/providers/ReactPageProvider.ts +11 -4
  51. package/src/router/providers/ReactServerProvider.ts +321 -316
  52. package/src/router/providers/ReactServerTemplateProvider.ts +793 -0
  53. package/src/router/providers/SSRManifestProvider.ts +365 -0
  54. package/src/router/services/ReactPageServerService.ts +5 -3
  55. package/src/router/services/ReactRouter.ts +3 -3
  56. package/src/head/__tests__/page-head.spec.ts +0 -39
  57. package/src/head/providers/ServerHeadProvider.spec.ts +0 -163
@@ -1,147 +1,31 @@
1
- import { $hook, $inject } from "alepha";
2
- import { ServerTimingProvider } from "alepha/server";
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
- protected readonly onServerRenderEnd = $hook({
11
- on: "react:server:render:end",
12
- handler: async (ev) => {
13
- this.serverTimingProvider.beginTiming("renderHead");
14
- this.headProvider.fillHead(ev.state);
15
- if (ev.state.head) {
16
- ev.html = this.renderHead(ev.html, ev.state.head);
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, "&amp;")
116
- .replace(/</g, "&lt;")
117
- .replace(/>/g, "&gt;")
118
- .replace(/"/g, "&quot;")
119
- .replace(/'/g, "&#039;");
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
- protected renderScriptTag(script: Record<string, string | boolean>): string {
135
- const attrs = Object.entries(script)
136
- .filter(([, value]) => value !== false)
137
- .map(([key, value]) => {
138
- // Boolean attributes - render without value if true
139
- if (value === true) {
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 "../index.ts";
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
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Symbol key for SSR module preloading path.
3
+ * Using Symbol.for() allows the Vite plugin to inject this at build time.
4
+ * @internal
5
+ */
6
+ export const PAGE_PRELOAD_KEY = Symbol.for("alepha.page.preload");
@@ -10,7 +10,7 @@ import { AlephaError } from "alepha";
10
10
  * import { Redirection } from "@alepha/react";
11
11
  *
12
12
  * const MyPage = $page({
13
- * resolve: async () => {
13
+ * loader: async () => {
14
14
  * if (needRedirect) {
15
15
  * throw new Redirection("/new-path");
16
16
  * }
@@ -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";
@@ -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 `resolve` function
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, ReactServerProvider, ReactPageServerService],
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
- resolve: ({ params }) => ({
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
- resolve: async () => {
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
- resolve: () => {
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
- resolve: () => {
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
- resolve: () => ({ appName: "My App" }),
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
- resolve: () => ({ theme: "dark" }),
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
- resolve: ({ theme }) => ({
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
- resolve: () => ({ level: "root" }),
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
- resolve: ({ level }) => ({ level: `${level} > section` }),
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
- resolve: ({ level }) => ({ level: `${level} > page` }),
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
- resolve: ({ params }) => ({
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
- resolve: ({ query }) => ({
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
- resolve: ({ query }) => ({
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
- resolve: ({ params, query }) => ({
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
- resolve: ({ query }) => ({
663
+ loader: ({ query }) => ({
664
664
  searchQuery: query.q,
665
665
  currentPage: query.page,
666
666
  }),