@btst/stack 3.0.0-rc.1 → 3.0.0-rc.2

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 (87) hide show
  1. package/README.md +4 -8
  2. package/dist/next/index.d.cts +13 -7
  3. package/dist/next/index.d.mts +13 -7
  4. package/dist/next/index.d.ts +13 -7
  5. package/dist/packages/stack/src/next/page.cjs +8 -6
  6. package/dist/packages/stack/src/next/page.mjs +8 -6
  7. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +1 -1
  8. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +1 -1
  9. package/dist/packages/stack/src/react-router/page.cjs +17 -11
  10. package/dist/packages/stack/src/react-router/page.mjs +17 -11
  11. package/dist/packages/stack/src/tanstack/page.cjs +5 -3
  12. package/dist/packages/stack/src/tanstack/page.mjs +5 -3
  13. package/dist/plugins/blog/api/index.d.cts +2 -2
  14. package/dist/plugins/blog/api/index.d.mts +2 -2
  15. package/dist/plugins/blog/api/index.d.ts +2 -2
  16. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  17. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  18. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  19. package/dist/plugins/blog/client/index.d.cts +2 -2
  20. package/dist/plugins/blog/client/index.d.mts +2 -2
  21. package/dist/plugins/blog/client/index.d.ts +2 -2
  22. package/dist/plugins/blog/query-keys.d.cts +2 -2
  23. package/dist/plugins/blog/query-keys.d.mts +2 -2
  24. package/dist/plugins/blog/query-keys.d.ts +2 -2
  25. package/dist/plugins/cms/api/index.d.cts +1 -1
  26. package/dist/plugins/cms/api/index.d.mts +1 -1
  27. package/dist/plugins/cms/api/index.d.ts +1 -1
  28. package/dist/plugins/cms/query-keys.d.cts +1 -1
  29. package/dist/plugins/cms/query-keys.d.mts +1 -1
  30. package/dist/plugins/cms/query-keys.d.ts +1 -1
  31. package/dist/plugins/comments/client/components/index.d.cts +5 -0
  32. package/dist/plugins/comments/client/components/index.d.mts +5 -0
  33. package/dist/plugins/comments/client/components/index.d.ts +5 -0
  34. package/dist/plugins/kanban/api/index.d.cts +1 -1
  35. package/dist/plugins/kanban/api/index.d.mts +1 -1
  36. package/dist/plugins/kanban/api/index.d.ts +1 -1
  37. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  38. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  39. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  40. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  41. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  42. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  43. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  44. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  45. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  46. package/dist/plugins/ui-builder/client/index.d.cts +1 -1
  47. package/dist/plugins/ui-builder/client/index.d.mts +1 -1
  48. package/dist/plugins/ui-builder/client/index.d.ts +1 -1
  49. package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
  50. package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
  51. package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
  52. package/dist/react-router/index.d.cts +14 -4
  53. package/dist/react-router/index.d.mts +14 -4
  54. package/dist/react-router/index.d.ts +14 -4
  55. package/dist/shared/{stack.H5B8miAV.d.cts → stack.CKGjOI3Z.d.cts} +11 -11
  56. package/dist/shared/{stack.80-y2TTR.d.mts → stack.DZHEHd-S.d.mts} +11 -11
  57. package/dist/shared/{stack.CRelKJfH.d.ts → stack.DseBJmGF.d.ts} +4 -4
  58. package/dist/shared/{stack.INBa9-TO.d.cts → stack.DzwZw-Ck.d.cts} +4 -4
  59. package/dist/shared/{stack.DrUBX-QE.d.mts → stack.EOPVnJrF.d.cts} +6 -1
  60. package/dist/shared/{stack.DrUBX-QE.d.ts → stack.EOPVnJrF.d.mts} +6 -1
  61. package/dist/shared/{stack.DrUBX-QE.d.cts → stack.EOPVnJrF.d.ts} +6 -1
  62. package/dist/shared/{stack.Bk5UB_BT.d.ts → stack.WELCDuMs.d.ts} +11 -11
  63. package/dist/shared/{stack.3di7vQs6.d.mts → stack.oVIdV7XS.d.mts} +4 -4
  64. package/dist/tanstack/index.d.cts +20 -12
  65. package/dist/tanstack/index.d.mts +20 -12
  66. package/dist/tanstack/index.d.ts +20 -12
  67. package/package.json +6 -6
  68. package/src/__tests__/entry-factories.test.tsx +104 -4
  69. package/src/__tests__/package-metadata.test.ts +21 -0
  70. package/src/next/index.tsx +2 -0
  71. package/src/next/page.tsx +17 -9
  72. package/src/plugins/comments/__tests__/client-sweep.test.tsx +23 -0
  73. package/src/plugins/comments/client/components/comment-thread.tsx +6 -2
  74. package/src/react-router/index.tsx +1 -0
  75. package/src/react-router/page.tsx +34 -12
  76. package/src/shared/entry-factories.ts +9 -0
  77. package/src/tanstack/index.tsx +2 -0
  78. package/src/tanstack/page.tsx +28 -11
  79. package/dist/shared/{stack.BvxHSZYT.d.cts → stack.BsmDN8r9.d.cts} +3 -3
  80. package/dist/shared/{stack.eq5eg1yt.d.cts → stack.CuI_-z-_.d.cts} +22 -22
  81. package/dist/shared/{stack.B0muOwUF.d.mts → stack.D18Dq3ve.d.cts} +7 -7
  82. package/dist/shared/{stack.B0muOwUF.d.ts → stack.D18Dq3ve.d.mts} +7 -7
  83. package/dist/shared/{stack.B0muOwUF.d.cts → stack.D18Dq3ve.d.ts} +7 -7
  84. package/dist/shared/{stack.Dj04W2c3.d.mts → stack.D2RVkQD8.d.mts} +22 -22
  85. package/dist/shared/{stack.B-hEIoUw.d.ts → stack.DOzsgzkY.d.ts} +3 -3
  86. package/dist/shared/{stack.CMbX8Q5C.d.ts → stack.DXrOtuek.d.ts} +22 -22
  87. package/dist/shared/{stack.f8Vke-eH.d.mts → stack.NGKtwA6S.d.mts} +3 -3
@@ -1,4 +1,4 @@
1
- import { S as StackRequestHandler, G as GetStackClient } from '../shared/stack.DrUBX-QE.cjs';
1
+ import { S as StackRequestHandler, G as GetStackClient, R as ResolveStackClient } from '../shared/stack.EOPVnJrF.cjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { QueryClient } from '@tanstack/react-query';
4
4
  import { S as StackRouterConfig } from '../shared/stack.aqVZ6PEW.cjs';
@@ -38,9 +38,23 @@ declare function toTanStackHandlers(handler: StackRequestHandler): {
38
38
  }) => Response | Promise<Response>;
39
39
  };
40
40
 
41
- interface CreateTanStackPageOptions {
41
+ interface TanStackPageLoaderArgs<TContext = unknown> {
42
+ /** Parameters for the TanStack catch-all route. */
43
+ params: {
44
+ _splat?: string;
45
+ };
46
+ /** The current TanStack router context. */
47
+ context: TContext;
48
+ }
49
+ interface CreateTanStackPageOptions<TContext = unknown> {
42
50
  /** Returns the stack client for a given QueryClient (`lib/stack-client`). */
43
51
  getStackClient: GetStackClient;
52
+ /**
53
+ * Resolves the stack client for the isomorphic route loader. Use an
54
+ * isomorphic adapter when request-aware setup differs on the server and
55
+ * client. Defaults to `getStackClient`.
56
+ */
57
+ getLoaderStackClient?: ResolveStackClient<TanStackPageLoaderArgs<TContext>>;
44
58
  /**
45
59
  * Returns the QueryClient for the current context. Defaults to the
46
60
  * `queryClient` from the router context (set up by
@@ -48,12 +62,6 @@ interface CreateTanStackPageOptions {
48
62
  */
49
63
  getQueryClient?: () => QueryClient;
50
64
  }
51
- interface TanStackPageLoaderArgs {
52
- params: {
53
- _splat?: string;
54
- };
55
- context?: unknown;
56
- }
57
65
  /**
58
66
  * Creates the route options for the TanStack Start catch-all page, to spread
59
67
  * into `createFileRoute("/pages/$")(...)`: SSR prefetch via `route.loader()`,
@@ -73,10 +81,10 @@ interface TanStackPageLoaderArgs {
73
81
  * );
74
82
  * ```
75
83
  */
76
- declare function createTanStackPageOptions(options: CreateTanStackPageOptions): {
84
+ declare function createTanStackPageOptions<TContext = unknown>(options: CreateTanStackPageOptions<TContext>): {
77
85
  readonly ssr: true;
78
86
  readonly component: () => react_jsx_runtime.JSX.Element;
79
- readonly loader: ({ params, context }: TanStackPageLoaderArgs) => Promise<{
87
+ readonly loader: (loaderArgs: TanStackPageLoaderArgs<TContext>) => Promise<{
80
88
  meta: any;
81
89
  }>;
82
90
  readonly head: ({ loaderData }: {
@@ -106,5 +114,5 @@ declare function createTanStackPageOptions(options: CreateTanStackPageOptions):
106
114
  */
107
115
  declare function tanstackRouter(): StackRouterConfig;
108
116
 
109
- export { GetStackClient, StackRequestHandler, createTanStackPageOptions, tanstackRouter, toTanStackHandlers };
110
- export type { CreateTanStackPageOptions };
117
+ export { GetStackClient, ResolveStackClient, StackRequestHandler, createTanStackPageOptions, tanstackRouter, toTanStackHandlers };
118
+ export type { CreateTanStackPageOptions, TanStackPageLoaderArgs };
@@ -1,4 +1,4 @@
1
- import { S as StackRequestHandler, G as GetStackClient } from '../shared/stack.DrUBX-QE.mjs';
1
+ import { S as StackRequestHandler, G as GetStackClient, R as ResolveStackClient } from '../shared/stack.EOPVnJrF.mjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { QueryClient } from '@tanstack/react-query';
4
4
  import { S as StackRouterConfig } from '../shared/stack.aqVZ6PEW.mjs';
@@ -38,9 +38,23 @@ declare function toTanStackHandlers(handler: StackRequestHandler): {
38
38
  }) => Response | Promise<Response>;
39
39
  };
40
40
 
41
- interface CreateTanStackPageOptions {
41
+ interface TanStackPageLoaderArgs<TContext = unknown> {
42
+ /** Parameters for the TanStack catch-all route. */
43
+ params: {
44
+ _splat?: string;
45
+ };
46
+ /** The current TanStack router context. */
47
+ context: TContext;
48
+ }
49
+ interface CreateTanStackPageOptions<TContext = unknown> {
42
50
  /** Returns the stack client for a given QueryClient (`lib/stack-client`). */
43
51
  getStackClient: GetStackClient;
52
+ /**
53
+ * Resolves the stack client for the isomorphic route loader. Use an
54
+ * isomorphic adapter when request-aware setup differs on the server and
55
+ * client. Defaults to `getStackClient`.
56
+ */
57
+ getLoaderStackClient?: ResolveStackClient<TanStackPageLoaderArgs<TContext>>;
44
58
  /**
45
59
  * Returns the QueryClient for the current context. Defaults to the
46
60
  * `queryClient` from the router context (set up by
@@ -48,12 +62,6 @@ interface CreateTanStackPageOptions {
48
62
  */
49
63
  getQueryClient?: () => QueryClient;
50
64
  }
51
- interface TanStackPageLoaderArgs {
52
- params: {
53
- _splat?: string;
54
- };
55
- context?: unknown;
56
- }
57
65
  /**
58
66
  * Creates the route options for the TanStack Start catch-all page, to spread
59
67
  * into `createFileRoute("/pages/$")(...)`: SSR prefetch via `route.loader()`,
@@ -73,10 +81,10 @@ interface TanStackPageLoaderArgs {
73
81
  * );
74
82
  * ```
75
83
  */
76
- declare function createTanStackPageOptions(options: CreateTanStackPageOptions): {
84
+ declare function createTanStackPageOptions<TContext = unknown>(options: CreateTanStackPageOptions<TContext>): {
77
85
  readonly ssr: true;
78
86
  readonly component: () => react_jsx_runtime.JSX.Element;
79
- readonly loader: ({ params, context }: TanStackPageLoaderArgs) => Promise<{
87
+ readonly loader: (loaderArgs: TanStackPageLoaderArgs<TContext>) => Promise<{
80
88
  meta: any;
81
89
  }>;
82
90
  readonly head: ({ loaderData }: {
@@ -106,5 +114,5 @@ declare function createTanStackPageOptions(options: CreateTanStackPageOptions):
106
114
  */
107
115
  declare function tanstackRouter(): StackRouterConfig;
108
116
 
109
- export { GetStackClient, StackRequestHandler, createTanStackPageOptions, tanstackRouter, toTanStackHandlers };
110
- export type { CreateTanStackPageOptions };
117
+ export { GetStackClient, ResolveStackClient, StackRequestHandler, createTanStackPageOptions, tanstackRouter, toTanStackHandlers };
118
+ export type { CreateTanStackPageOptions, TanStackPageLoaderArgs };
@@ -1,4 +1,4 @@
1
- import { S as StackRequestHandler, G as GetStackClient } from '../shared/stack.DrUBX-QE.js';
1
+ import { S as StackRequestHandler, G as GetStackClient, R as ResolveStackClient } from '../shared/stack.EOPVnJrF.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { QueryClient } from '@tanstack/react-query';
4
4
  import { S as StackRouterConfig } from '../shared/stack.aqVZ6PEW.js';
@@ -38,9 +38,23 @@ declare function toTanStackHandlers(handler: StackRequestHandler): {
38
38
  }) => Response | Promise<Response>;
39
39
  };
40
40
 
41
- interface CreateTanStackPageOptions {
41
+ interface TanStackPageLoaderArgs<TContext = unknown> {
42
+ /** Parameters for the TanStack catch-all route. */
43
+ params: {
44
+ _splat?: string;
45
+ };
46
+ /** The current TanStack router context. */
47
+ context: TContext;
48
+ }
49
+ interface CreateTanStackPageOptions<TContext = unknown> {
42
50
  /** Returns the stack client for a given QueryClient (`lib/stack-client`). */
43
51
  getStackClient: GetStackClient;
52
+ /**
53
+ * Resolves the stack client for the isomorphic route loader. Use an
54
+ * isomorphic adapter when request-aware setup differs on the server and
55
+ * client. Defaults to `getStackClient`.
56
+ */
57
+ getLoaderStackClient?: ResolveStackClient<TanStackPageLoaderArgs<TContext>>;
44
58
  /**
45
59
  * Returns the QueryClient for the current context. Defaults to the
46
60
  * `queryClient` from the router context (set up by
@@ -48,12 +62,6 @@ interface CreateTanStackPageOptions {
48
62
  */
49
63
  getQueryClient?: () => QueryClient;
50
64
  }
51
- interface TanStackPageLoaderArgs {
52
- params: {
53
- _splat?: string;
54
- };
55
- context?: unknown;
56
- }
57
65
  /**
58
66
  * Creates the route options for the TanStack Start catch-all page, to spread
59
67
  * into `createFileRoute("/pages/$")(...)`: SSR prefetch via `route.loader()`,
@@ -73,10 +81,10 @@ interface TanStackPageLoaderArgs {
73
81
  * );
74
82
  * ```
75
83
  */
76
- declare function createTanStackPageOptions(options: CreateTanStackPageOptions): {
84
+ declare function createTanStackPageOptions<TContext = unknown>(options: CreateTanStackPageOptions<TContext>): {
77
85
  readonly ssr: true;
78
86
  readonly component: () => react_jsx_runtime.JSX.Element;
79
- readonly loader: ({ params, context }: TanStackPageLoaderArgs) => Promise<{
87
+ readonly loader: (loaderArgs: TanStackPageLoaderArgs<TContext>) => Promise<{
80
88
  meta: any;
81
89
  }>;
82
90
  readonly head: ({ loaderData }: {
@@ -106,5 +114,5 @@ declare function createTanStackPageOptions(options: CreateTanStackPageOptions):
106
114
  */
107
115
  declare function tanstackRouter(): StackRouterConfig;
108
116
 
109
- export { GetStackClient, StackRequestHandler, createTanStackPageOptions, tanstackRouter, toTanStackHandlers };
110
- export type { CreateTanStackPageOptions };
117
+ export { GetStackClient, ResolveStackClient, StackRequestHandler, createTanStackPageOptions, tanstackRouter, toTanStackHandlers };
118
+ export type { CreateTanStackPageOptions, TanStackPageLoaderArgs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "3.0.0-rc.1",
3
+ "version": "3.0.0-rc.2",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -833,7 +833,7 @@
833
833
  }
834
834
  },
835
835
  "dependencies": {
836
- "@btst/db": "2.2.2",
836
+ "@btst/db": "2.2.3",
837
837
  "@milkdown/crepe": "^7.17.1",
838
838
  "@milkdown/kit": "^7.17.1",
839
839
  "remend": "^1.0.1",
@@ -843,7 +843,7 @@
843
843
  "@ai-sdk/react": ">=2.0.0",
844
844
  "@aws-sdk/client-s3": ">=3.0.0",
845
845
  "@aws-sdk/s3-request-presigner": ">=3.0.0",
846
- "@btst/yar": ">=1.3.0",
846
+ "@btst/yar": ">=1.3.2",
847
847
  "@hookform/resolvers": ">=5.0.0",
848
848
  "@radix-ui/react-dialog": ">=1.1.0",
849
849
  "@radix-ui/react-label": ">=2.1.0",
@@ -854,7 +854,7 @@
854
854
  "@tanstack/react-router": ">=1.0.0",
855
855
  "@vercel/blob": ">=0.14.0",
856
856
  "ai": ">=5.0.0",
857
- "better-call": ">=1.3.5",
857
+ "better-call": "1.3.6",
858
858
  "class-variance-authority": ">=0.7.0",
859
859
  "clsx": ">=2.1.0",
860
860
  "cmdk": ">=1.1.0",
@@ -903,8 +903,8 @@
903
903
  "@ai-sdk/react": "^2.0.94",
904
904
  "@aws-sdk/client-s3": "^3.1011.0",
905
905
  "@aws-sdk/s3-request-presigner": "^3.1011.0",
906
- "@btst/adapter-memory": "2.2.2",
907
- "@btst/yar": "1.3.0",
906
+ "@btst/adapter-memory": "2.2.3",
907
+ "@btst/yar": "1.3.2",
908
908
  "@tanstack/react-router": "1.168.10",
909
909
  "@types/react": "^19.0.0",
910
910
  "@types/react-dom": "^19.0.0",
@@ -23,6 +23,14 @@ function makeQueryClient() {
23
23
  });
24
24
  }
25
25
 
26
+ function reactRouterLoaderArgs(splat: string) {
27
+ return {
28
+ request: new Request(`http://test.local/${splat}`),
29
+ params: { "*": splat },
30
+ context: undefined,
31
+ };
32
+ }
33
+
26
34
  const okHandler = async (request: Request) =>
27
35
  new Response(`ok:${request.method}`);
28
36
 
@@ -96,6 +104,28 @@ describe("createNextPage", () => {
96
104
  expect(html).toContain("blog page");
97
105
  });
98
106
 
107
+ it("supports an async request-aware stack client when rendering", async () => {
108
+ const queryClient = makeQueryClient();
109
+ const pageProps = { params: params(["account"]) };
110
+ const page = createNextPage({
111
+ getStackClient: async (currentQueryClient, currentPageProps) => {
112
+ const { all } = await currentPageProps.params;
113
+ return makeStackClient({
114
+ [`/${all?.join("/")}`]: {
115
+ PageComponent: () => <div>request-aware page</div>,
116
+ },
117
+ })(currentQueryClient);
118
+ },
119
+ getQueryClient: () => queryClient,
120
+ });
121
+
122
+ const element = await page.Page(pageProps);
123
+ const html = renderToString(
124
+ <QueryClientProvider client={queryClient}>{element}</QueryClientProvider>,
125
+ );
126
+ expect(html).toContain("request-aware page");
127
+ });
128
+
99
129
  it("calls notFound when no route matches", async () => {
100
130
  const notFound = vi.fn(() => {
101
131
  throw new Error("NEXT_NOT_FOUND_TEST");
@@ -140,6 +170,26 @@ describe("createNextPage", () => {
140
170
  });
141
171
  });
142
172
 
173
+ it("generateMetadata awaits a request-aware stack client", async () => {
174
+ const page = createNextPage({
175
+ getStackClient: async (queryClient, { params: pageParams }) => {
176
+ const { all } = await pageParams;
177
+ return makeStackClient({
178
+ [`/${all?.join("/")}`]: {
179
+ meta: () => [{ name: "title", content: "Request Metadata" }],
180
+ },
181
+ })(queryClient);
182
+ },
183
+ getQueryClient: makeQueryClient,
184
+ });
185
+
186
+ const metadata = await page.generateMetadata({
187
+ params: params(["account"]),
188
+ });
189
+
190
+ expect(metadata).toMatchObject({ title: "Request Metadata" });
191
+ });
192
+
143
193
  it("generateMetadata returns {} without running the loader when the route has no meta", async () => {
144
194
  const loader = vi.fn();
145
195
  const page = createNextPage({
@@ -170,6 +220,35 @@ describe("createNextPage", () => {
170
220
  });
171
221
 
172
222
  describe("createReactRouterPage", () => {
223
+ it("creates an async request-aware loader without changing the render client", async () => {
224
+ const queryClient = makeQueryClient();
225
+ const page = createReactRouterPage({
226
+ getStackClient: makeStackClient({}),
227
+ getQueryClient: () => queryClient,
228
+ });
229
+ const loader = page.createLoader<{ role: string }>(
230
+ async (currentQueryClient, { request, context }) => {
231
+ const role = context.role;
232
+ const requestRole = request.headers.get("x-role");
233
+ return makeStackClient({
234
+ "/account": {
235
+ meta: () => [{ name: "viewer", content: `${role}:${requestRole}` }],
236
+ },
237
+ })(currentQueryClient);
238
+ },
239
+ );
240
+
241
+ const data = await loader({
242
+ params: { "*": "account" },
243
+ request: new Request("http://test.local/account", {
244
+ headers: { "x-role": "member" },
245
+ }),
246
+ context: { role: "admin" },
247
+ });
248
+
249
+ expect(data.meta).toEqual([{ name: "viewer", content: "admin:member" }]);
250
+ });
251
+
173
252
  it("loader prefetches and returns path, dehydratedState and meta", async () => {
174
253
  const queryClient = makeQueryClient();
175
254
  const page = createReactRouterPage({
@@ -187,7 +266,7 @@ describe("createReactRouterPage", () => {
187
266
  getQueryClient: () => queryClient,
188
267
  });
189
268
 
190
- const data = await page.loader({ params: { "*": "blog" } });
269
+ const data = await page.loader(reactRouterLoaderArgs("blog"));
191
270
  expect(data.path).toBe("/blog");
192
271
  expect(data.meta).toEqual([{ name: "title", content: "Blog" }]);
193
272
  expect(data.dehydratedState.queries).toHaveLength(1);
@@ -215,7 +294,7 @@ describe("createReactRouterPage", () => {
215
294
  getQueryClient: () => queryClient,
216
295
  });
217
296
 
218
- const data = await page.loader({ params: { "*": "broken" } });
297
+ const data = await page.loader(reactRouterLoaderArgs("broken"));
219
298
  const errored = data.dehydratedState.queries.find(
220
299
  (q) => q.state.status === "error",
221
300
  );
@@ -244,12 +323,33 @@ describe("createReactRouterPage", () => {
244
323
  });
245
324
 
246
325
  expect(page.ErrorBoundary).toBe(CustomErrorBoundary);
247
- const data = await page.loader({ params: { "*": "broken" } });
326
+ const data = await page.loader(reactRouterLoaderArgs("broken"));
248
327
  expect(data.dehydratedState.queries).toHaveLength(0);
249
328
  });
250
329
  });
251
330
 
252
331
  describe("createTanStackPageOptions", () => {
332
+ it("awaits a context-aware stack client for isomorphic loaders", async () => {
333
+ const queryClient = makeQueryClient();
334
+ const options = createTanStackPageOptions<{ role: string }>({
335
+ getStackClient: makeStackClient({}),
336
+ getQueryClient: () => queryClient,
337
+ getLoaderStackClient: async (currentQueryClient, { context }) =>
338
+ makeStackClient({
339
+ "/account": {
340
+ meta: () => [{ title: `Account for ${context.role}` }],
341
+ },
342
+ })(currentQueryClient),
343
+ });
344
+
345
+ const data = await options.loader({
346
+ params: { _splat: "account" },
347
+ context: { role: "admin" },
348
+ });
349
+
350
+ expect(data).toEqual({ meta: [{ title: "Account for admin" }] });
351
+ });
352
+
253
353
  it("loader throws notFound() when no route matches", async () => {
254
354
  const options = createTanStackPageOptions({
255
355
  getStackClient: makeStackClient({}),
@@ -257,7 +357,7 @@ describe("createTanStackPageOptions", () => {
257
357
  });
258
358
 
259
359
  await expect(
260
- options.loader({ params: { _splat: "missing" } }),
360
+ options.loader({ params: { _splat: "missing" }, context: undefined }),
261
361
  ).rejects.toMatchObject({ isNotFound: true });
262
362
  });
263
363
 
@@ -0,0 +1,21 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ describe("published dependency alignment", () => {
6
+ it("uses the Better DB release that shares the Better Auth 1.6.16 cohort", async () => {
7
+ const manifest = JSON.parse(
8
+ await readFile(resolve("package.json"), "utf8"),
9
+ ) as {
10
+ dependencies?: Record<string, string>;
11
+ devDependencies?: Record<string, string>;
12
+ peerDependencies?: Record<string, string>;
13
+ };
14
+
15
+ expect(manifest.dependencies?.["@btst/db"]).toBe("2.2.3");
16
+ expect(manifest.devDependencies?.["@btst/adapter-memory"]).toBe("2.2.3");
17
+ expect(manifest.devDependencies?.["@btst/yar"]).toBe("1.3.2");
18
+ expect(manifest.peerDependencies?.["@btst/yar"]).toBe(">=1.3.2");
19
+ expect(manifest.peerDependencies?.["better-call"]).toBe("1.3.6");
20
+ });
21
+ });
@@ -1,11 +1,13 @@
1
1
  export { toNextRouteHandlers } from "./handlers";
2
2
  export {
3
3
  type CreateNextPageOptions,
4
+ type GetNextStackClient,
4
5
  type NextPageProps,
5
6
  createNextPage,
6
7
  } from "./page";
7
8
  export { nextRouter } from "./router";
8
9
  export type {
9
10
  GetStackClient,
11
+ ResolveStackClient,
10
12
  StackRequestHandler,
11
13
  } from "../shared/entry-factories";
package/src/next/page.tsx CHANGED
@@ -6,7 +6,7 @@ import type { ReactNode } from "react";
6
6
  import { metaElementsToObject } from "../client/meta-utils";
7
7
  import { normalizePath } from "../client/path-utils";
8
8
  import {
9
- type GetStackClient,
9
+ type ResolveStackClient,
10
10
  stackDehydrateOptions,
11
11
  } from "../shared/entry-factories";
12
12
 
@@ -14,9 +14,15 @@ export interface NextPageProps {
14
14
  params: Promise<{ all?: string[] }>;
15
15
  }
16
16
 
17
+ /** Resolves the stack client for a Next.js page or metadata request. */
18
+ export type GetNextStackClient = ResolveStackClient<NextPageProps>;
19
+
17
20
  export interface CreateNextPageOptions {
18
- /** Returns the stack client for a given QueryClient (`lib/stack-client`). */
19
- getStackClient: GetStackClient;
21
+ /**
22
+ * Returns the stack client for the current request. May be async and receives
23
+ * the page props so request-aware clients can resolve request-local state.
24
+ */
25
+ getStackClient: GetNextStackClient;
20
26
  /** Returns the QueryClient for the current context (`lib/query-client`). */
21
27
  getQueryClient: () => QueryClient;
22
28
  /**
@@ -63,11 +69,13 @@ export function createNextPage(options: CreateNextPageOptions) {
63
69
  dehydrateOptions = stackDehydrateOptions,
64
70
  } = options;
65
71
 
66
- async function Page({ params }: NextPageProps) {
72
+ async function Page(pageProps: NextPageProps) {
73
+ const { params } = pageProps;
67
74
  const pathParams = await params;
68
75
  const path = normalizePath(pathParams?.all);
69
76
  const queryClient = getQueryClient();
70
- const route = getStackClient(queryClient).router.getRoute(path);
77
+ const stackClient = await getStackClient(queryClient, pageProps);
78
+ const route = stackClient.router.getRoute(path);
71
79
 
72
80
  if (route?.loader) {
73
81
  await route.loader();
@@ -82,13 +90,13 @@ export function createNextPage(options: CreateNextPageOptions) {
82
90
  );
83
91
  }
84
92
 
85
- async function generateMetadata({
86
- params,
87
- }: NextPageProps): Promise<Metadata> {
93
+ async function generateMetadata(pageProps: NextPageProps): Promise<Metadata> {
94
+ const { params } = pageProps;
88
95
  const pathParams = await params;
89
96
  const path = normalizePath(pathParams?.all);
90
97
  const queryClient = getQueryClient();
91
- const route = getStackClient(queryClient).router.getRoute(path);
98
+ const stackClient = await getStackClient(queryClient, pageProps);
99
+ const route = stackClient.router.getRoute(path);
92
100
 
93
101
  if (!route) {
94
102
  return notFound();
@@ -496,6 +496,29 @@ describe("CommentThread provider wiring", () => {
496
496
  ?.getAttribute("href"),
497
497
  ).toBe("/sign-in");
498
498
  });
499
+
500
+ it("prefers the per-thread login href when unauthenticated", async () => {
501
+ await render(
502
+ <StackProvider
503
+ basePath="/pages"
504
+ api={{ baseURL: "http://provider.local", basePath: "/api/stack" }}
505
+ auth={{ getIdentity: () => null, loginPath: "/sign-in" }}
506
+ >
507
+ <CommentThread
508
+ resourceId="post-1"
509
+ resourceType="blog-post"
510
+ loginHref="/sign-in?redirectTo=%2Fblog%2Fpost-1%23comments"
511
+ />
512
+ </StackProvider>,
513
+ );
514
+ await act(async () => {});
515
+
516
+ expect(
517
+ container
518
+ .querySelector<HTMLAnchorElement>('[data-testid="login-link"]')
519
+ ?.getAttribute("href"),
520
+ ).toBe("/sign-in?redirectTo=%2Fblog%2Fpost-1%23comments");
521
+ });
499
522
  });
500
523
 
501
524
  describe("CommentForm inline field errors (StackError)", () => {
@@ -69,6 +69,11 @@ export interface CommentThreadProps {
69
69
  className?: string;
70
70
  /** Localization strings — defaults to English */
71
71
  localization?: Partial<CommentsLocalization>;
72
+ /**
73
+ * Sign-in URL for unauthenticated users in this thread.
74
+ * Overrides the `loginPath` from the nearest `StackProvider`.
75
+ */
76
+ loginHref?: string;
72
77
  /**
73
78
  * Number of top-level comments to load per page.
74
79
  * Clicking "Load more" fetches the next page. Default: 10.
@@ -103,7 +108,6 @@ type ResolvedCommentThreadProps = CommentThreadProps & {
103
108
  apiBaseURL: string;
104
109
  apiBasePath: string;
105
110
  currentUserId?: string;
106
- loginHref?: string;
107
111
  };
108
112
 
109
113
  const DEFAULT_RENDERER: ComponentType<CommentRendererProps> = ({ body }) => (
@@ -842,7 +846,7 @@ export function CommentThread(props: CommentThreadProps) {
842
846
  apiBaseURL: api?.baseURL ?? "",
843
847
  apiBasePath: api?.basePath ?? "",
844
848
  currentUserId,
845
- loginHref: auth?.loginPath,
849
+ loginHref: props.loginHref ?? auth?.loginPath,
846
850
  };
847
851
 
848
852
  return (
@@ -7,5 +7,6 @@ export {
7
7
  export { reactRouter } from "./router";
8
8
  export type {
9
9
  GetStackClient,
10
+ ResolveStackClient,
10
11
  StackRequestHandler,
11
12
  } from "../shared/entry-factories";
@@ -10,6 +10,7 @@ import { useLoaderData, useRouteError } from "react-router";
10
10
  import { normalizePath } from "../client/path-utils";
11
11
  import {
12
12
  type GetStackClient,
13
+ type ResolveStackClient,
13
14
  stackDehydrateOptions,
14
15
  } from "../shared/entry-factories";
15
16
 
@@ -19,8 +20,13 @@ import {
19
20
  * are not available inside the library, so the factory types the subset it
20
21
  * uses; the results remain assignable to the generated route exports.
21
22
  */
22
- export interface ReactRouterPageLoaderArgs {
23
+ export interface ReactRouterPageLoaderArgs<TContext = unknown> {
24
+ /** The request that triggered the route loader. */
25
+ request: Request;
26
+ /** Parameters for the React Router catch-all route. */
23
27
  params: { "*"?: string } & Record<string, string | undefined>;
28
+ /** Request-local values supplied by React Router middleware. */
29
+ context: TContext;
24
30
  }
25
31
 
26
32
  export interface CreateReactRouterPageOptions {
@@ -75,22 +81,37 @@ export function createReactRouterPage(options: CreateReactRouterPageOptions) {
75
81
  dehydrateOptions = stackDehydrateOptions,
76
82
  } = options;
77
83
 
78
- async function loader({ params }: ReactRouterPageLoaderArgs) {
79
- const queryClient = getQueryClient();
80
- const path = normalizePath(params["*"]);
81
- const route = getStackClient(queryClient).router.getRoute(path);
84
+ /** Creates a loader with an optional async, request-aware stack client. */
85
+ function createLoader<TContext = unknown>(
86
+ getLoaderStackClient?: ResolveStackClient<
87
+ ReactRouterPageLoaderArgs<TContext>
88
+ >,
89
+ ) {
90
+ return async function loader(
91
+ loaderArgs: ReactRouterPageLoaderArgs<TContext>,
92
+ ) {
93
+ const { params } = loaderArgs;
94
+ const queryClient = getQueryClient();
95
+ const path = normalizePath(params["*"]);
96
+ const stackClient = getLoaderStackClient
97
+ ? await getLoaderStackClient(queryClient, loaderArgs)
98
+ : getStackClient(queryClient);
99
+ const route = stackClient.router.getRoute(path);
82
100
 
83
- if (route?.loader) {
84
- await route.loader();
85
- }
101
+ if (route?.loader) {
102
+ await route.loader();
103
+ }
86
104
 
87
- return {
88
- path,
89
- dehydratedState: dehydrate(queryClient, dehydrateOptions),
90
- meta: await route?.meta?.(),
105
+ return {
106
+ path,
107
+ dehydratedState: dehydrate(queryClient, dehydrateOptions),
108
+ meta: await route?.meta?.(),
109
+ };
91
110
  };
92
111
  }
93
112
 
113
+ const loader = createLoader();
114
+
94
115
  type LoaderData = Awaited<ReturnType<typeof loader>>;
95
116
 
96
117
  // Recent React Router versions pass `loaderData`; older 7.x used `data`.
@@ -132,6 +153,7 @@ export function createReactRouterPage(options: CreateReactRouterPageOptions) {
132
153
 
133
154
  return {
134
155
  loader,
156
+ createLoader,
135
157
  meta,
136
158
  Component,
137
159
  ErrorBoundary: CustomErrorBoundary ?? DefaultErrorBoundary,
@@ -35,6 +35,15 @@ export interface StackClientLike {
35
35
  */
36
36
  export type GetStackClient = (queryClient: QueryClient) => StackClientLike;
37
37
 
38
+ /**
39
+ * Consumer-provided resolver for lifecycle phases that can await request-aware
40
+ * stack client creation.
41
+ */
42
+ export type ResolveStackClient<TContext> = (
43
+ queryClient: QueryClient,
44
+ context: TContext,
45
+ ) => StackClientLike | PromiseLike<StackClientLike>;
46
+
38
47
  /**
39
48
  * A framework-agnostic BTST API handler, as returned by
40
49
  * `createBackendHandler(...).handler`.