@btst/stack 3.0.2 → 3.1.0-rc.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 (55) hide show
  1. package/dist/context/index.d.cts +3 -1
  2. package/dist/context/index.d.mts +3 -1
  3. package/dist/context/index.d.ts +3 -1
  4. package/dist/packages/stack/src/context/auth.cjs +20 -14
  5. package/dist/packages/stack/src/context/auth.mjs +20 -14
  6. package/dist/packages/stack/src/context/provider.cjs +42 -22
  7. package/dist/packages/stack/src/context/provider.mjs +43 -23
  8. package/dist/packages/stack/src/plugins/blog/client/components/pages/home-page.cjs +32 -28
  9. package/dist/packages/stack/src/plugins/blog/client/components/pages/home-page.mjs +32 -29
  10. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.cjs +53 -47
  11. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.mjs +53 -48
  12. package/dist/packages/stack/src/plugins/blog/client/components/pages/tag-page.cjs +30 -24
  13. package/dist/packages/stack/src/plugins/blog/client/components/pages/tag-page.mjs +30 -25
  14. package/dist/plugins/blog/client/components/index.d.cts +6 -10
  15. package/dist/plugins/blog/client/components/index.d.mts +6 -10
  16. package/dist/plugins/blog/client/components/index.d.ts +6 -10
  17. package/dist/plugins/blog/client/pages/post.cjs +9 -0
  18. package/dist/plugins/blog/client/pages/post.d.cts +8 -0
  19. package/dist/plugins/blog/client/pages/post.d.mts +8 -0
  20. package/dist/plugins/blog/client/pages/post.d.ts +8 -0
  21. package/dist/plugins/blog/client/pages/post.mjs +7 -0
  22. package/dist/plugins/blog/client/pages/posts.cjs +9 -0
  23. package/dist/plugins/blog/client/pages/posts.d.cts +8 -0
  24. package/dist/plugins/blog/client/pages/posts.d.mts +8 -0
  25. package/dist/plugins/blog/client/pages/posts.d.ts +8 -0
  26. package/dist/plugins/blog/client/pages/posts.mjs +7 -0
  27. package/dist/plugins/blog/client/pages/tag.cjs +9 -0
  28. package/dist/plugins/blog/client/pages/tag.d.cts +8 -0
  29. package/dist/plugins/blog/client/pages/tag.d.mts +8 -0
  30. package/dist/plugins/blog/client/pages/tag.d.ts +8 -0
  31. package/dist/plugins/blog/client/pages/tag.mjs +7 -0
  32. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  33. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  34. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  35. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  36. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  37. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  38. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  39. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  40. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  41. package/package.json +41 -2
  42. package/src/__tests__/provider-hydration.test.tsx +164 -0
  43. package/src/context/auth.tsx +21 -14
  44. package/src/context/provider.tsx +51 -28
  45. package/src/plugins/blog/__tests__/page-load-error.test.tsx +53 -0
  46. package/src/plugins/blog/__tests__/page-loading.test.tsx +106 -0
  47. package/src/plugins/blog/client/components/pages/home-page.tsx +39 -35
  48. package/src/plugins/blog/client/components/pages/post-page.tsx +62 -54
  49. package/src/plugins/blog/client/components/pages/tag-page.tsx +35 -29
  50. package/src/plugins/blog/client/pages/post.tsx +7 -0
  51. package/src/plugins/blog/client/pages/posts.tsx +7 -0
  52. package/src/plugins/blog/client/pages/tag.tsx +7 -0
  53. package/dist/shared/{stack.D20bOqfr.d.ts → stack.BuxUQqL8.d.ts} +20 -20
  54. package/dist/shared/{stack.AWSs6TXN.d.cts → stack.CX83YNuS.d.cts} +20 -20
  55. package/dist/shared/{stack.HB5uGLpE.d.mts → stack.Egr3RWhA.d.mts} +20 -20
@@ -0,0 +1,164 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ act,
4
+ createContext,
5
+ lazy,
6
+ useContext,
7
+ useEffect,
8
+ useLayoutEffect,
9
+ useState,
10
+ type ReactNode,
11
+ type ComponentType,
12
+ } from "react";
13
+ import { createRoot, hydrateRoot } from "react-dom/client";
14
+ import { renderToString } from "react-dom/server";
15
+ import { QueryClient } from "@tanstack/react-query";
16
+ import { expect, it, vi } from "vitest";
17
+ import { z } from "zod";
18
+ import {
19
+ defineAuthorization,
20
+ definePermissions,
21
+ permission,
22
+ } from "../authorization";
23
+ import { createClientAuth } from "../authorization/client";
24
+ import { createClientStack } from "../client";
25
+ import { ComposedRoute } from "../client/components/compose";
26
+ import { StackProvider, useStack } from "../context";
27
+
28
+ (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
29
+
30
+ const permissions = definePermissions("records", {
31
+ read: permission(z.object({ id: z.string() })),
32
+ });
33
+ const authorization = defineAuthorization({
34
+ identity: z.object({ id: z.string() }),
35
+ permissions: [permissions],
36
+ rules: ({ records }) => [
37
+ records.read.when(({ identity }) => identity?.id === "owner"),
38
+ ],
39
+ });
40
+ const auth = createClientAuth({ authorization, getIdentity: () => null });
41
+ const owner = { id: "owner" };
42
+ const access = permissions.read({ id: "one" });
43
+ const navigate = () => {};
44
+ const useRouter = () => undefinedRouter;
45
+ const undefinedRouter = {};
46
+ const onError = () => {};
47
+ const Content = () => <div data-content>Prefetched content</div>;
48
+ const Loading = () => <div data-loading>Loading</div>;
49
+ const ErrorPage = () => <div data-denied>Denied</div>;
50
+
51
+ function createStack() {
52
+ return createClientStack({
53
+ api: { baseURL: "http://test.local", basePath: "/api/data" },
54
+ site: { baseURL: "http://test.local", basePath: "/pages" },
55
+ queryClient: new QueryClient(),
56
+ plugins: {},
57
+ });
58
+ }
59
+
60
+ it.each([false, true])(
61
+ "keeps pending page hydration safe when a provider rerenders (revoke: %s)",
62
+ async (revoke) => {
63
+ const stack = createStack();
64
+ function Provider({ children }: { children: ReactNode }) {
65
+ const [updated, setUpdated] = useState(false);
66
+ useEffect(() => setUpdated(true), []);
67
+ return (
68
+ <StackProvider
69
+ stack={stack}
70
+ router={{ navigate, useRouter }}
71
+ auth={auth}
72
+ initialIdentity={updated && revoke ? null : owner}
73
+ >
74
+ <output>{updated ? "updated" : "initial"}</output>
75
+ {children}
76
+ </StackProvider>
77
+ );
78
+ }
79
+ const tree = (Page: ComponentType) => (
80
+ <Provider>
81
+ <ComposedRoute
82
+ path="/record/one"
83
+ permission={access}
84
+ PageComponent={Page}
85
+ LoadingComponent={Loading}
86
+ ErrorComponent={ErrorPage}
87
+ onError={onError}
88
+ />
89
+ </Provider>
90
+ );
91
+ const container = document.createElement("div");
92
+ container.innerHTML = renderToString(tree(Content));
93
+ expect(container.querySelector("[data-content]")).not.toBeNull();
94
+ const Pending = lazy(
95
+ () => new Promise<{ default: typeof Content }>(() => {}),
96
+ );
97
+ const consoleError = vi
98
+ .spyOn(console, "error")
99
+ .mockImplementation(() => {});
100
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
101
+ const onRecoverableError = vi.fn();
102
+ let root: ReturnType<typeof hydrateRoot> | undefined;
103
+ try {
104
+ await act(async () => {
105
+ root = hydrateRoot(container, tree(Pending), { onRecoverableError });
106
+ });
107
+ expect(container.querySelector("output")?.textContent).toBe("updated");
108
+ expect(container.querySelector("[data-content]") !== null).toBe(!revoke);
109
+ expect(container.querySelector("[data-denied]") !== null).toBe(revoke);
110
+ expect(container.querySelector("[data-loading]")).toBeNull();
111
+ expect(onRecoverableError).not.toHaveBeenCalled();
112
+ } finally {
113
+ await act(async () => root?.unmount());
114
+ stack.provider.queryClient.clear();
115
+ consoleError.mockRestore();
116
+ consoleWarn.mockRestore();
117
+ }
118
+ },
119
+ );
120
+
121
+ it("applies new router callbacks with the current host context before page effects", async () => {
122
+ const Workspace = createContext("A");
123
+ const stack = createStack();
124
+ const observations: string[] = [];
125
+ function Page() {
126
+ const workspace = useContext(Workspace);
127
+ const { router } = useStack();
128
+ useLayoutEffect(() => {
129
+ void router?.navigate?.(workspace);
130
+ }, [workspace]);
131
+ return null;
132
+ }
133
+ function App() {
134
+ const [workspace, setWorkspace] = useState("A");
135
+ useEffect(() => setWorkspace("B"), []);
136
+ return (
137
+ <Workspace.Provider value={workspace}>
138
+ <StackProvider
139
+ stack={stack}
140
+ router={{
141
+ navigate: (current) => {
142
+ observations.push(`${current}:${workspace}`);
143
+ },
144
+ }}
145
+ >
146
+ <ComposedRoute
147
+ path="/record"
148
+ PageComponent={Page}
149
+ LoadingComponent={Loading}
150
+ onError={onError}
151
+ />
152
+ </StackProvider>
153
+ </Workspace.Provider>
154
+ );
155
+ }
156
+ const root = createRoot(document.createElement("div"));
157
+ try {
158
+ await act(async () => root.render(<App />));
159
+ expect(observations).toEqual(["A:A", "B:B"]);
160
+ } finally {
161
+ await act(async () => root.unmount());
162
+ stack.provider.queryClient.clear();
163
+ }
164
+ });
@@ -193,21 +193,28 @@ export function StackAuthBoundary({
193
193
  void resolveIdentity(false);
194
194
  }, [initialIdentity, resolveIdentity]);
195
195
 
196
- return (
197
- <AuthContext.Provider
198
- value={{
199
- provider,
200
- identity: currentState.identity,
201
- isPending: currentState.isPending,
202
- sourceGeneration: currentResolutionGeneration,
203
- ...(currentState.error ? { error: currentState.error } : {}),
204
- refetch,
205
- waitForResolution,
206
- }}
207
- >
208
- {children}
209
- </AuthContext.Provider>
196
+ const value = useMemo(
197
+ () => ({
198
+ provider,
199
+ identity: currentState.identity,
200
+ isPending: currentState.isPending,
201
+ sourceGeneration: currentResolutionGeneration,
202
+ ...(currentState.error ? { error: currentState.error } : {}),
203
+ refetch,
204
+ waitForResolution,
205
+ }),
206
+ [
207
+ provider,
208
+ currentState.identity,
209
+ currentState.isPending,
210
+ currentResolutionGeneration,
211
+ currentState.error,
212
+ refetch,
213
+ waitForResolution,
214
+ ],
210
215
  );
216
+
217
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
211
218
  }
212
219
 
213
220
  /** @internal Access the raw auth context (or `null` when no provider is set). */
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  createContext,
4
4
  useContext,
5
+ useMemo,
5
6
  type ReactElement,
6
7
  type ReactNode,
7
8
  } from "react";
@@ -79,6 +80,10 @@ type StackProviderServices = {
79
80
  i18n?: StackI18nProvider;
80
81
  };
81
82
 
83
+ /** Plugin override configuration inferred from a resolved client stack. */
84
+ export type ClientStackOverrides<TStack extends ResolvedClientStack<any, any>> =
85
+ InferredPluginOverrides<RegisteredClientPlugins<TStack>>;
86
+
82
87
  type CanonicalStackProviderOverrideProps<
83
88
  TStack extends ResolvedClientStack<any, any>,
84
89
  > = InferredPluginOverrides<
@@ -110,12 +115,24 @@ function stripUndefined<T extends Record<string, any>>(obj: T): Partial<T> {
110
115
  return result as Partial<T>;
111
116
  }
112
117
 
113
- function resolveStaticRouter(
114
- router: StackRouterConfig | undefined,
115
- ): StackRouter | undefined {
116
- if (!router) return undefined;
117
- const { useRouter: _useRouter, ...staticFields } = router;
118
- return stripUndefined(staticFields);
118
+ function useStaticRouter(router: StackRouterConfig | undefined) {
119
+ const { Link, Image, navigate, refresh, getSearchParams, setSearchParams } =
120
+ router ?? {};
121
+ const present = router !== undefined;
122
+ return useMemo(
123
+ () =>
124
+ present
125
+ ? stripUndefined({
126
+ Link,
127
+ Image,
128
+ navigate,
129
+ refresh,
130
+ getSearchParams,
131
+ setSearchParams,
132
+ })
133
+ : undefined,
134
+ [present, Link, Image, navigate, refresh, getSearchParams, setSearchParams],
135
+ );
119
136
  }
120
137
 
121
138
  /**
@@ -135,15 +152,16 @@ function RouterBridge({
135
152
  children?: ReactNode;
136
153
  }) {
137
154
  const hookRouter = useRouter();
138
- const router: StackRouter = {
139
- ...staticRouter,
140
- ...stripUndefined(hookRouter),
141
- };
155
+ const context = useMemo(
156
+ () => ({
157
+ ...value,
158
+ router: { ...staticRouter, ...stripUndefined(hookRouter) },
159
+ }),
160
+ [value, staticRouter, hookRouter],
161
+ );
142
162
 
143
163
  return (
144
- <StackContext.Provider value={{ ...value, router }}>
145
- {children}
146
- </StackContext.Provider>
164
+ <StackContext.Provider value={context}>{children}</StackContext.Provider>
147
165
  );
148
166
  }
149
167
 
@@ -205,18 +223,25 @@ export function StackProvider<
205
223
  i18n,
206
224
  }: CanonicalStackProviderProps<TStack>): ReactElement {
207
225
  const projection = stack.provider;
208
- const staticRouter = resolveStaticRouter(router);
209
- const value: Omit<StackContextValue<any>, "router"> = {
210
- overrides: overrides ?? {},
211
- basePath: projection.site.basePath,
212
- api: projection.api,
213
- site: projection.site,
214
- plugins: projection.plugins,
215
- queryClient: projection.queryClient,
216
- clientStackContext: stack.context,
217
- resolvedStack: stack,
218
- auth,
219
- };
226
+ const staticRouter = useStaticRouter(router);
227
+ const value = useMemo(
228
+ () => ({
229
+ overrides: overrides ?? {},
230
+ basePath: projection.site.basePath,
231
+ api: projection.api,
232
+ site: projection.site,
233
+ plugins: projection.plugins,
234
+ queryClient: projection.queryClient,
235
+ clientStackContext: stack.context,
236
+ resolvedStack: stack,
237
+ auth,
238
+ }),
239
+ [overrides, stack, auth],
240
+ );
241
+ const context = useMemo(
242
+ () => ({ ...value, router: staticRouter }),
243
+ [value, staticRouter],
244
+ );
220
245
 
221
246
  const content = auth ? (
222
247
  <StackAuthBoundary provider={auth} initialIdentity={initialIdentity}>
@@ -235,9 +260,7 @@ export function StackProvider<
235
260
  {content}
236
261
  </RouterBridge>
237
262
  ) : (
238
- <StackContext.Provider value={{ ...value, router: staticRouter }}>
239
- {content}
240
- </StackContext.Provider>
263
+ <StackContext.Provider value={context}>{content}</StackContext.Provider>
241
264
  );
242
265
 
243
266
  return (
@@ -0,0 +1,53 @@
1
+ // @vitest-environment jsdom
2
+ import { act, lazy } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { expect, it, vi } from "vitest";
6
+ import { createClientStack } from "@btst/stack/client";
7
+ import { StackProvider } from "@btst/stack/context";
8
+ import { blogClientPlugin } from "@btst/stack/plugins/blog/client";
9
+ import { createHomePage } from "../client/components/pages/home-page";
10
+
11
+ (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
12
+
13
+ it("reports a failed page module through the existing blog error hook and UI", async () => {
14
+ const error = new Error("Page module unavailable");
15
+ const Page = createHomePage(lazy(() => Promise.reject(error)));
16
+ const onRouteError = vi.fn();
17
+ const queryClient = new QueryClient();
18
+ const stack = createClientStack({
19
+ api: { baseURL: "http://test.local", basePath: "/api/data" },
20
+ site: { baseURL: "http://test.local", basePath: "/pages" },
21
+ queryClient,
22
+ plugins: { blog: blogClientPlugin() },
23
+ });
24
+ const container = document.createElement("div");
25
+ const root = createRoot(container);
26
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
27
+ try {
28
+ await act(async () =>
29
+ root.render(
30
+ <QueryClientProvider client={queryClient}>
31
+ <StackProvider
32
+ stack={stack}
33
+ overrides={{ blog: { onRouteError, uploadImage: async () => "" } }}
34
+ >
35
+ <Page />
36
+ </StackProvider>
37
+ </QueryClientProvider>,
38
+ ),
39
+ );
40
+ expect(
41
+ container.querySelector('[data-testid="error-placeholder"]'),
42
+ ).not.toBeNull();
43
+ expect(onRouteError).toHaveBeenCalledExactlyOnceWith("posts", error, {
44
+ path: "/blog",
45
+ isSSR: false,
46
+ published: true,
47
+ });
48
+ } finally {
49
+ await act(async () => root.unmount());
50
+ queryClient.clear();
51
+ consoleError.mockRestore();
52
+ }
53
+ });
@@ -0,0 +1,106 @@
1
+ import { renderToString } from "react-dom/server";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { StackProvider } from "@btst/stack/context";
5
+ import { createClientStack } from "@btst/stack/client";
6
+ import { blogClientPlugin } from "@btst/stack/plugins/blog/client";
7
+ import { PostListPage } from "@btst/stack/plugins/blog/client/pages/posts";
8
+ import { PostPage } from "@btst/stack/plugins/blog/client/pages/post";
9
+ import { TagPage } from "@btst/stack/plugins/blog/client/pages/tag";
10
+ import { defineAuthorization } from "@btst/stack/authorization";
11
+ import { createClientAuth } from "@btst/stack/authorization/client";
12
+ import { blogPermissions } from "@btst/stack/plugins/blog/permissions";
13
+ import { z } from "zod";
14
+ import { BLOG_QUERY_KEYS } from "../api/query-key-defs";
15
+
16
+ const post = {
17
+ id: "prefetched-post",
18
+ slug: "prefetched-post",
19
+ title: "Prefetched article",
20
+ content: "Article available before hydration.",
21
+ excerpt: "Article excerpt",
22
+ published: true,
23
+ tags: [],
24
+ image: null,
25
+ authorId: null,
26
+ createdAt: "2026-09-16T12:00:00.000Z",
27
+ updatedAt: "2026-09-16T12:00:00.000Z",
28
+ publishedAt: "2026-09-16T12:00:00.000Z",
29
+ };
30
+
31
+ const authorization = defineAuthorization({
32
+ identity: z.object({ id: z.string() }),
33
+ permissions: [blogPermissions] as const,
34
+ rules: ({ blog }) => [
35
+ blog.post.read.when(
36
+ ({ facts }) =>
37
+ facts.scope === "published" ||
38
+ (facts.scope === "post" && (!facts.exists || facts.published)),
39
+ ),
40
+ blog.tag.read.allow(),
41
+ ],
42
+ });
43
+ const auth = createClientAuth({ authorization, getIdentity: () => null });
44
+
45
+ describe("blog pages in native framework routes", () => {
46
+ it.each(["list", "post", "tag", "draft list", "draft post", "missing post"])(
47
+ "renders prefetched %s with the existing public-access rules",
48
+ (page) => {
49
+ const fetch = vi
50
+ .spyOn(globalThis, "fetch")
51
+ .mockRejectedValue(new Error("Unexpected fetch"));
52
+ const published = !page.startsWith("draft");
53
+ const queryClient = new QueryClient({
54
+ defaultOptions: { queries: { retry: false, staleTime: Infinity } },
55
+ });
56
+ queryClient.setQueryData(
57
+ BLOG_QUERY_KEYS.postDetail(post.slug),
58
+ page === "missing post" ? null : { ...post, published },
59
+ );
60
+ queryClient.setQueryData(
61
+ BLOG_QUERY_KEYS.postsList({
62
+ published,
63
+ ...(page === "tag" ? { tagSlug: "news" } : {}),
64
+ }),
65
+ {
66
+ pages: [[post]],
67
+ pageParams: [0],
68
+ },
69
+ );
70
+ queryClient.setQueryData(BLOG_QUERY_KEYS.tagsList(), [
71
+ { id: "news", slug: "news", name: "News" },
72
+ ]);
73
+ const stack = createClientStack({
74
+ api: { baseURL: "http://test.local", basePath: "/api/data" },
75
+ site: { baseURL: "http://test.local", basePath: "/pages" },
76
+ queryClient,
77
+ plugins: { blog: blogClientPlugin() },
78
+ });
79
+ try {
80
+ const html = renderToString(
81
+ <QueryClientProvider client={queryClient}>
82
+ <StackProvider stack={stack} auth={auth} initialIdentity={null}>
83
+ {page.endsWith("list") ? (
84
+ <PostListPage published={published} />
85
+ ) : page === "tag" ? (
86
+ <TagPage tagSlug="news" />
87
+ ) : (
88
+ <PostPage slug={post.slug} />
89
+ )}
90
+ </StackProvider>
91
+ </QueryClientProvider>,
92
+ );
93
+ if (published && page !== "missing post")
94
+ expect(html).toContain(post.title);
95
+ else expect(html).not.toContain(post.title);
96
+ if (page === "missing post") expect(html).toContain("does not exist");
97
+ if (published)
98
+ expect(html).not.toMatch(/data-testid="(?:posts|post)-skeleton"/);
99
+ expect(fetch).not.toHaveBeenCalled();
100
+ } finally {
101
+ queryClient.clear();
102
+ fetch.mockRestore();
103
+ }
104
+ },
105
+ );
106
+ });
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { lazy } from "react";
3
+ import { lazy, type ComponentType } from "react";
4
4
  import { usePluginOverrides } from "@btst/stack/context";
5
5
  import type { BlogPluginOverrides } from "../../overrides";
6
6
  import { BLOG_PLUGIN_ID } from "../../constants";
@@ -10,39 +10,43 @@ import { PostsLoading } from "../loading";
10
10
  import { NotFoundPage } from "./404-page";
11
11
  import { blogPermissions } from "../../../permissions";
12
12
 
13
- // Lazy load the internal component with actual page content
14
- const HomePage = lazy(() =>
15
- import("./home-page.internal").then((m) => ({ default: m.HomePage })),
16
- );
13
+ // Share the guarded page between the lazy registry and native route entry.
14
+ export function createHomePage(
15
+ HomePage: ComponentType<{ published: boolean }>,
16
+ ) {
17
+ // Exported wrapped component with error and loading boundaries
18
+ function HomePageComponent({ published = true }: { published?: boolean }) {
19
+ const { onRouteError } =
20
+ usePluginOverrides<BlogPluginOverrides>(BLOG_PLUGIN_ID);
21
+ return (
22
+ <ComposedRoute
23
+ path={published ? "/blog" : "/blog/drafts"}
24
+ PageComponent={HomePage}
25
+ ErrorComponent={DefaultError}
26
+ LoadingComponent={PostsLoading}
27
+ NotFoundComponent={NotFoundPage}
28
+ permission={blogPermissions.post.read({
29
+ scope: published ? "published" : "drafts",
30
+ })}
31
+ props={{ published }}
32
+ onError={(error) => {
33
+ if (onRouteError) {
34
+ onRouteError("posts", error, {
35
+ path: published ? "/blog" : "/blog/drafts",
36
+ isSSR: typeof window === "undefined",
37
+ published,
38
+ });
39
+ }
40
+ }}
41
+ />
42
+ );
43
+ }
17
44
 
18
- // Exported wrapped component with error and loading boundaries
19
- export function HomePageComponent({
20
- published = true,
21
- }: {
22
- published?: boolean;
23
- }) {
24
- const { onRouteError } =
25
- usePluginOverrides<BlogPluginOverrides>(BLOG_PLUGIN_ID);
26
- return (
27
- <ComposedRoute
28
- path={published ? "/blog" : "/blog/drafts"}
29
- PageComponent={HomePage}
30
- ErrorComponent={DefaultError}
31
- LoadingComponent={PostsLoading}
32
- NotFoundComponent={NotFoundPage}
33
- permission={blogPermissions.post.read({
34
- scope: published ? "published" : "drafts",
35
- })}
36
- props={{ published }}
37
- onError={(error) => {
38
- if (onRouteError) {
39
- onRouteError("posts", error, {
40
- path: published ? "/blog" : "/blog/drafts",
41
- isSSR: typeof window === "undefined",
42
- published,
43
- });
44
- }
45
- }}
46
- />
47
- );
45
+ return HomePageComponent;
48
46
  }
47
+
48
+ export const HomePageComponent = createHomePage(
49
+ lazy(() =>
50
+ import("./home-page.internal").then((m) => ({ default: m.HomePage })),
51
+ ),
52
+ );