@trpc/next 11.0.0-next.91 → 11.0.0-rc.329

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 (58) hide show
  1. package/README.md +5 -5
  2. package/dist/app-dir/client.d.ts +3 -3
  3. package/dist/app-dir/client.d.ts.map +1 -1
  4. package/dist/app-dir/client.js +5 -105
  5. package/dist/app-dir/client.mjs +4 -102
  6. package/dist/app-dir/create-action-hook.d.ts +18 -6
  7. package/dist/app-dir/create-action-hook.d.ts.map +1 -1
  8. package/dist/app-dir/create-action-hook.js +108 -0
  9. package/dist/app-dir/create-action-hook.mjs +105 -0
  10. package/dist/app-dir/formDataToObject.js +34 -0
  11. package/dist/app-dir/formDataToObject.mjs +32 -0
  12. package/dist/app-dir/links/nextCache.d.ts +4 -3
  13. package/dist/app-dir/links/nextCache.d.ts.map +1 -1
  14. package/dist/app-dir/links/nextCache.js +9 -10
  15. package/dist/app-dir/links/nextCache.mjs +8 -7
  16. package/dist/app-dir/links/nextHttp.d.ts +11 -5
  17. package/dist/app-dir/links/nextHttp.d.ts.map +1 -1
  18. package/dist/app-dir/links/nextHttp.js +22 -23
  19. package/dist/app-dir/links/nextHttp.mjs +22 -21
  20. package/dist/app-dir/server.d.ts +19 -12
  21. package/dist/app-dir/server.d.ts.map +1 -1
  22. package/dist/app-dir/server.js +39 -55
  23. package/dist/app-dir/server.mjs +29 -43
  24. package/dist/app-dir/shared.d.ts +19 -13
  25. package/dist/app-dir/shared.d.ts.map +1 -1
  26. package/dist/{shared-e49b9cdc.js → app-dir/shared.js} +1 -1
  27. package/dist/{shared-f6996341.mjs → app-dir/shared.mjs} +2 -2
  28. package/dist/app-dir/types.d.ts +23 -11
  29. package/dist/app-dir/types.d.ts.map +1 -1
  30. package/dist/bundle-analysis.json +56 -44
  31. package/dist/createTRPCNext.d.ts +10 -8
  32. package/dist/createTRPCNext.d.ts.map +1 -1
  33. package/dist/createTRPCNext.js +38 -0
  34. package/dist/createTRPCNext.mjs +36 -0
  35. package/dist/index.js +4 -190
  36. package/dist/index.mjs +2 -185
  37. package/dist/ssrPrepass.d.ts +3 -0
  38. package/dist/ssrPrepass.d.ts.map +1 -0
  39. package/dist/ssrPrepass.js +139 -0
  40. package/dist/ssrPrepass.mjs +137 -0
  41. package/dist/withTRPC.d.ts +41 -13
  42. package/dist/withTRPC.d.ts.map +1 -1
  43. package/dist/withTRPC.js +86 -0
  44. package/dist/withTRPC.mjs +84 -0
  45. package/package.json +36 -25
  46. package/src/app-dir/client.ts +4 -4
  47. package/src/app-dir/create-action-hook.tsx +49 -19
  48. package/src/app-dir/links/nextCache.ts +20 -8
  49. package/src/app-dir/links/nextHttp.ts +50 -30
  50. package/src/app-dir/server.ts +86 -34
  51. package/src/app-dir/shared.ts +52 -25
  52. package/src/app-dir/types.ts +41 -29
  53. package/src/createTRPCNext.tsx +25 -16
  54. package/src/ssrPrepass.ts +185 -0
  55. package/src/withTRPC.tsx +102 -180
  56. package/ssrPrepass/index.d.ts +1 -0
  57. package/ssrPrepass/index.js +1 -0
  58. package/dist/shared-642894f4.js +0 -19
@@ -3,34 +3,37 @@ import {
3
3
  clientCallTypeToProcedureType,
4
4
  createTRPCUntypedClient,
5
5
  } from '@trpc/client';
6
- import {
6
+ import type {
7
7
  AnyProcedure,
8
- AnyRootConfig,
8
+ AnyRootTypes,
9
9
  AnyRouter,
10
- CombinedDataTransformer,
11
- getTRPCErrorFromUnknown,
10
+ ErrorHandlerOptions,
11
+ inferClientTypes,
12
12
  inferProcedureInput,
13
13
  MaybePromise,
14
+ RootConfig,
14
15
  Simplify,
15
- TRPCError,
16
- } from '@trpc/server';
17
- import { TRPCResponse } from '@trpc/server/rpc';
16
+ TRPCResponse,
17
+ } from '@trpc/server/unstable-core-do-not-import';
18
18
  import {
19
19
  createRecursiveProxy,
20
20
  getErrorShape,
21
+ getTRPCErrorFromUnknown,
21
22
  transformTRPCResponse,
22
- } from '@trpc/server/shared';
23
+ TRPCError,
24
+ } from '@trpc/server/unstable-core-do-not-import';
23
25
  import { revalidateTag } from 'next/cache';
26
+ import { isNotFoundError } from 'next/dist/client/components/not-found';
27
+ import { isRedirectError } from 'next/dist/client/components/redirect';
24
28
  import { cache } from 'react';
25
29
  import { formDataToObject } from './formDataToObject';
26
- import {
30
+ import type {
27
31
  ActionHandlerDef,
28
32
  CreateTRPCNextAppRouterOptions,
29
- generateCacheTag,
30
33
  inferActionDef,
31
- isFormData,
32
34
  } from './shared';
33
- import { NextAppDirDecoratedProcedureRecord } from './types';
35
+ import { generateCacheTag, isFormData } from './shared';
36
+ import type { NextAppDirDecorateRouterRecord } from './types';
34
37
 
35
38
  // ts-prune-ignore-next
36
39
  export function experimental_createTRPCNextAppDirServer<
@@ -58,12 +61,21 @@ export function experimental_createTRPCNextAppDirServer<
58
61
  }
59
62
 
60
63
  return (client[procedureType] as any)(procedurePath, ...callOpts.args);
61
- }) as NextAppDirDecoratedProcedureRecord<
62
- TRouter['_def']['_config'],
64
+ }) as NextAppDirDecorateRouterRecord<
65
+ TRouter['_def']['_config']['$types'],
63
66
  TRouter['_def']['record']
64
67
  >;
65
68
  }
66
69
 
70
+ /**
71
+ * Rethrow errors that should be handled by Next.js
72
+ */
73
+ const throwNextErrors = (error: TRPCError) => {
74
+ const { cause } = error;
75
+ if (isRedirectError(cause) || isNotFoundError(cause)) {
76
+ throw error.cause;
77
+ }
78
+ };
67
79
  /**
68
80
  * @internal
69
81
  */
@@ -73,36 +85,63 @@ export type TRPCActionHandler<TDef extends ActionHandlerDef> = (
73
85
 
74
86
  export function experimental_createServerActionHandler<
75
87
  TInstance extends {
76
- _config: AnyRootConfig;
88
+ _config: RootConfig<AnyRootTypes>;
77
89
  },
78
90
  >(
79
91
  t: TInstance,
80
- opts: {
81
- createContext: () => MaybePromise<TInstance['_config']['$types']['ctx']>;
92
+ opts: (object extends TInstance['_config']['$types']['ctx']
93
+ ? {
94
+ createContext?: () => MaybePromise<
95
+ TInstance['_config']['$types']['ctx']
96
+ >;
97
+ }
98
+ : {
99
+ createContext: () => MaybePromise<
100
+ TInstance['_config']['$types']['ctx']
101
+ >;
102
+ }) & {
82
103
  /**
83
104
  * Transform form data to a `Record` before passing it to the procedure
84
105
  * @default true
85
106
  */
86
107
  normalizeFormData?: boolean;
108
+ /**
109
+ * Called when an error occurs in the handler
110
+ */
111
+ onError?: (
112
+ opts: ErrorHandlerOptions<TInstance['_config']['$types']['ctx']>,
113
+ ) => void;
114
+
115
+ /**
116
+ * Rethrow errors that should be handled by Next.js
117
+ * @default true
118
+ */
119
+ rethrowNextErrors?: boolean;
87
120
  },
88
121
  ) {
89
122
  const config = t._config;
90
- const { normalizeFormData = true, createContext } = opts;
123
+ const {
124
+ normalizeFormData = true,
125
+ createContext,
126
+ rethrowNextErrors = true,
127
+ } = opts;
91
128
 
92
- const transformer = config.transformer as CombinedDataTransformer;
129
+ const transformer = config.transformer;
93
130
 
94
131
  // TODO allow this to take a `TRouter` in addition to a `AnyProcedure`
95
132
  return function createServerAction<TProc extends AnyProcedure>(
96
133
  proc: TProc,
97
- ): TRPCActionHandler<Simplify<inferActionDef<TInstance['_config'], TProc>>> {
134
+ ): TRPCActionHandler<
135
+ Simplify<inferActionDef<inferClientTypes<TInstance>, TProc>>
136
+ > {
98
137
  return async function actionHandler(
99
138
  rawInput: FormData | inferProcedureInput<TProc>,
100
139
  ) {
101
- const ctx: TInstance['_config']['$types']['ctx'] | undefined = undefined;
140
+ let ctx: TInstance['_config']['$types']['ctx'] | undefined = undefined;
102
141
  try {
103
- const ctx = await createContext();
142
+ ctx = (await createContext?.()) ?? {};
104
143
  if (normalizeFormData && isFormData(rawInput)) {
105
- // Normalizes formdata so we can use `z.object({})` etc on the server
144
+ // Normalizes FormData so we can use `z.object({})` etc on the server
106
145
  try {
107
146
  rawInput = formDataToObject(rawInput);
108
147
  } catch {
@@ -115,13 +154,15 @@ export function experimental_createServerActionHandler<
115
154
  rawInput = transformer.input.deserialize(rawInput);
116
155
  }
117
156
 
118
- const data = await proc({
119
- input: undefined,
120
- ctx,
121
- path: 'serverAction',
122
- getRawInput: async () => rawInput,
123
- type: proc._def.type,
124
- });
157
+ const data = proc._def.experimental_caller
158
+ ? await proc(rawInput as any)
159
+ : await proc({
160
+ input: undefined,
161
+ ctx,
162
+ path: '',
163
+ getRawInput: async () => rawInput,
164
+ type: proc._def.type,
165
+ });
125
166
 
126
167
  const transformedJSON = transformTRPCResponse(config, {
127
168
  result: {
@@ -131,22 +172,33 @@ export function experimental_createServerActionHandler<
131
172
  return transformedJSON;
132
173
  } catch (cause) {
133
174
  const error = getTRPCErrorFromUnknown(cause);
175
+
176
+ opts.onError?.({
177
+ ctx,
178
+ error,
179
+ input: rawInput,
180
+ path: '',
181
+ type: proc._def.type,
182
+ });
183
+
184
+ rethrowNextErrors && throwNextErrors(error);
185
+
134
186
  const shape = getErrorShape({
135
187
  config,
136
188
  ctx,
137
189
  error,
138
190
  input: rawInput,
139
- path: 'serverAction',
191
+ path: '',
140
192
  type: proc._def.type,
141
193
  });
142
194
 
143
- // TODO: send the right HTTP header?!
144
-
145
195
  return transformTRPCResponse(t._config, {
146
196
  error: shape,
147
197
  });
148
198
  }
149
- } as TRPCActionHandler<inferActionDef<TInstance['_config'], TProc>>;
199
+ } as TRPCActionHandler<
200
+ inferActionDef<TInstance['_config']['$types'], TProc>
201
+ >;
150
202
  };
151
203
  }
152
204
 
@@ -1,30 +1,41 @@
1
- import {
1
+ import type {
2
2
  CreateTRPCClientOptions,
3
3
  Resolver,
4
4
  TRPCUntypedClient,
5
5
  } from '@trpc/client';
6
- import {
6
+ import type { inferProcedureOutput } from '@trpc/server';
7
+ import type {
8
+ AnyClientTypes,
7
9
  AnyProcedure,
8
10
  AnyQueryProcedure,
9
- AnyRootConfig,
11
+ AnyRootTypes,
10
12
  AnyRouter,
11
- Filter,
12
- inferHandlerInput,
13
+ inferProcedureInput,
14
+ inferTransformedProcedureOutput,
13
15
  ProtectedIntersection,
14
- ThenArg,
15
- } from '@trpc/server';
16
- import { createRecursiveProxy } from '@trpc/server/shared';
16
+ RouterRecord,
17
+ } from '@trpc/server/unstable-core-do-not-import';
18
+ import { createRecursiveProxy } from '@trpc/server/unstable-core-do-not-import';
17
19
 
18
20
  /**
19
21
  * @internal
20
22
  */
21
- export type UseProcedureRecord<TRouter extends AnyRouter> = {
22
- [TKey in keyof Filter<
23
- TRouter['_def']['record'],
24
- AnyQueryProcedure | AnyRouter
25
- >]: TRouter['_def']['record'][TKey] extends AnyRouter
26
- ? UseProcedureRecord<TRouter['_def']['record'][TKey]>
27
- : Resolver<TRouter['_def']['_config'], TRouter['_def']['record'][TKey]>;
23
+ export type UseProcedureRecord<
24
+ TRoot extends AnyRootTypes,
25
+ TRecord extends RouterRecord,
26
+ > = {
27
+ [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value
28
+ ? $Value extends RouterRecord
29
+ ? UseProcedureRecord<TRoot, $Value>
30
+ : $Value extends AnyQueryProcedure
31
+ ? Resolver<{
32
+ input: inferProcedureInput<$Value>;
33
+ output: inferTransformedProcedureOutput<TRoot, $Value>;
34
+ errorShape: TRoot['errorShape'];
35
+ transformer: TRoot['transformer'];
36
+ }>
37
+ : never
38
+ : never;
28
39
  };
29
40
 
30
41
  export function createUseProxy<TRouter extends AnyRouter>(
@@ -34,18 +45,31 @@ export function createUseProxy<TRouter extends AnyRouter>(
34
45
  const path = opts.path.join('.');
35
46
 
36
47
  return client.query(path, ...opts.args);
37
- }) as UseProcedureRecord<TRouter>;
48
+ }) as UseProcedureRecord<
49
+ TRouter['_def']['_config']['$types'],
50
+ TRouter['_def']['record']
51
+ >;
38
52
  }
39
53
 
40
54
  type NextAppRouterUse<TRouter extends AnyRouter> = {
41
55
  <TData extends Promise<unknown>[]>(
42
- cb: (t: UseProcedureRecord<TRouter>) => [...TData],
56
+ cb: (
57
+ t: UseProcedureRecord<
58
+ TRouter['_def']['_config']['$types'],
59
+ TRouter['_def']['record']
60
+ >,
61
+ ) => [...TData],
43
62
  ): {
44
- [TKey in keyof TData]: ThenArg<TData[TKey]>;
63
+ [TKey in keyof TData]: Awaited<TData[TKey]>;
45
64
  };
46
65
  <TData extends Promise<unknown>>(
47
- cb: (t: UseProcedureRecord<TRouter>) => TData,
48
- ): ThenArg<TData>;
66
+ cb: (
67
+ t: UseProcedureRecord<
68
+ TRouter['_def']['_config']['$types'],
69
+ TRouter['_def']['record']
70
+ >,
71
+ ) => TData,
72
+ ): Awaited<TData>;
49
73
  };
50
74
  type CreateTRPCNextAppRouterBase<TRouter extends AnyRouter> = {
51
75
  use: NextAppRouterUse<TRouter>;
@@ -53,7 +77,10 @@ type CreateTRPCNextAppRouterBase<TRouter extends AnyRouter> = {
53
77
  export type CreateTRPCNextAppRouter<TRouter extends AnyRouter> =
54
78
  ProtectedIntersection<
55
79
  CreateTRPCNextAppRouterBase<TRouter>,
56
- UseProcedureRecord<TRouter>
80
+ UseProcedureRecord<
81
+ TRouter['_def']['_config']['$types'],
82
+ TRouter['_def']['record']
83
+ >
57
84
  >;
58
85
 
59
86
  /**
@@ -94,10 +121,10 @@ export interface ActionHandlerDef {
94
121
  * @internal
95
122
  */
96
123
  export type inferActionDef<
97
- TConfig extends AnyRootConfig,
124
+ TRoot extends AnyClientTypes,
98
125
  TProc extends AnyProcedure,
99
126
  > = {
100
- input: inferHandlerInput<TProc>[0];
101
- output: TProc['_def']['_output_out'];
102
- errorShape: TConfig['$types']['errorShape'];
127
+ input: inferProcedureInput<TProc>;
128
+ output: inferProcedureOutput<TProc>;
129
+ errorShape: TRoot['errorShape'];
103
130
  };
@@ -1,47 +1,59 @@
1
- import { Resolver } from '@trpc/client';
2
- import {
3
- AnyMutationProcedure,
1
+ import type { Resolver } from '@trpc/client';
2
+ import type {
4
3
  AnyProcedure,
5
- AnyQueryProcedure,
6
- AnyRootConfig,
7
- AnyRouter,
8
- AnySubscriptionProcedure,
9
- ProcedureArgs,
10
- ProcedureRouterRecord,
11
- } from '@trpc/server';
4
+ AnyRootTypes,
5
+ inferProcedureInput,
6
+ inferTransformedProcedureOutput,
7
+ ProcedureType,
8
+ RouterRecord,
9
+ } from '@trpc/server/unstable-core-do-not-import';
10
+
11
+ type ResolverDef = {
12
+ input: any;
13
+ output: any;
14
+ transformer: boolean;
15
+ errorShape: any;
16
+ };
12
17
 
13
18
  export type DecorateProcedureServer<
14
- TConfig extends AnyRootConfig,
15
- TProcedure extends AnyProcedure,
16
- > = TProcedure extends AnyQueryProcedure
19
+ TType extends ProcedureType,
20
+ TDef extends ResolverDef,
21
+ > = TType extends 'query'
17
22
  ? {
18
- query: Resolver<TConfig, TProcedure>;
23
+ query: Resolver<TDef>;
19
24
  revalidate: (
20
- input?: ProcedureArgs<TProcedure['_def']>[0],
25
+ input?: TDef['input'],
21
26
  ) => Promise<
22
27
  { revalidated: false; error: string } | { revalidated: true }
23
28
  >;
24
29
  }
25
- : TProcedure extends AnyMutationProcedure
30
+ : TType extends 'mutation'
26
31
  ? {
27
- mutate: Resolver<TConfig, TProcedure>;
32
+ mutate: Resolver<TDef>;
28
33
  }
29
- : TProcedure extends AnySubscriptionProcedure
34
+ : TType extends 'subscription'
30
35
  ? {
31
- subscribe: Resolver<TConfig, TProcedure>;
36
+ subscribe: Resolver<TDef>;
32
37
  }
33
38
  : never;
34
39
 
35
- export type NextAppDirDecoratedProcedureRecord<
36
- TConfig extends AnyRootConfig,
37
- TProcedures extends ProcedureRouterRecord,
40
+ export type NextAppDirDecorateRouterRecord<
41
+ TRoot extends AnyRootTypes,
42
+ TRecord extends RouterRecord,
38
43
  > = {
39
- [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
40
- ? NextAppDirDecoratedProcedureRecord<
41
- TConfig,
42
- TProcedures[TKey]['_def']['record']
43
- >
44
- : TProcedures[TKey] extends AnyProcedure
45
- ? DecorateProcedureServer<TConfig, TProcedures[TKey]>
44
+ [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value
45
+ ? $Value extends RouterRecord
46
+ ? NextAppDirDecorateRouterRecord<TRoot, $Value>
47
+ : $Value extends AnyProcedure
48
+ ? DecorateProcedureServer<
49
+ $Value['_def']['type'],
50
+ {
51
+ input: inferProcedureInput<$Value>;
52
+ output: inferTransformedProcedureOutput<TRoot, $Value>;
53
+ errorShape: TRoot['errorShape'];
54
+ transformer: TRoot['transformer'];
55
+ }
56
+ >
57
+ : never
46
58
  : never;
47
59
  };
@@ -1,18 +1,25 @@
1
1
  /* istanbul ignore file -- @preserve */
2
2
  // We're testing this through E2E-testing
3
+ import type {
4
+ CreateReactUtils,
5
+ DecorateRouterRecord,
6
+ TRPCUseQueries,
7
+ TRPCUseSuspenseQueries,
8
+ } from '@trpc/react-query/shared';
3
9
  import {
4
10
  createReactDecoration,
5
11
  createReactQueryUtils,
6
- CreateReactUtils,
7
12
  createRootHooks,
8
- DecoratedProcedureRecord,
9
- TRPCUseQueries,
10
13
  } from '@trpc/react-query/shared';
11
- import { AnyRouter, ProtectedIntersection } from '@trpc/server';
12
- import { createFlatProxy } from '@trpc/server/shared';
13
- import { NextPageContext } from 'next/types';
14
+ import type {
15
+ AnyRouter,
16
+ ProtectedIntersection,
17
+ } from '@trpc/server/unstable-core-do-not-import';
18
+ import { createFlatProxy } from '@trpc/server/unstable-core-do-not-import';
19
+ import type { NextPageContext } from 'next/types';
14
20
  import { useMemo } from 'react';
15
- import { withTRPC, WithTRPCNoSSROptions, WithTRPCSSROptions } from './withTRPC';
21
+ import type { WithTRPCNoSSROptions, WithTRPCSSROptions } from './withTRPC';
22
+ import { withTRPC } from './withTRPC';
16
23
 
17
24
  /**
18
25
  * @internal
@@ -24,15 +31,16 @@ export interface CreateTRPCNextBase<
24
31
  /**
25
32
  * @deprecated renamed to `useUtils` and will be removed in a future tRPC version
26
33
  *
27
- * @see https://trpc.io/docs/client/react/useUtils
34
+ * @link https://trpc.io/docs/v11/client/react/useUtils
28
35
  */
29
36
  useContext(): CreateReactUtils<TRouter, TSSRContext>;
30
37
  /**
31
- * @see https://trpc.io/docs/client/react/useUtils
38
+ * @link https://trpc.io/docs/v11/client/react/useUtils
32
39
  */
33
40
  useUtils(): CreateReactUtils<TRouter, TSSRContext>;
34
41
  withTRPC: ReturnType<typeof withTRPC<TRouter, TSSRContext>>;
35
42
  useQueries: TRPCUseQueries<TRouter>;
43
+ useSuspenseQueries: TRPCUseSuspenseQueries<TRouter>;
36
44
  }
37
45
 
38
46
  /**
@@ -41,23 +49,20 @@ export interface CreateTRPCNextBase<
41
49
  export type CreateTRPCNext<
42
50
  TRouter extends AnyRouter,
43
51
  TSSRContext extends NextPageContext,
44
- TFlags,
45
52
  > = ProtectedIntersection<
46
53
  CreateTRPCNextBase<TRouter, TSSRContext>,
47
- DecoratedProcedureRecord<
48
- TRouter['_def']['_config'],
49
- TRouter['_def']['record'],
50
- TFlags
54
+ DecorateRouterRecord<
55
+ TRouter['_def']['_config']['$types'],
56
+ TRouter['_def']['record']
51
57
  >
52
58
  >;
53
59
 
54
60
  export function createTRPCNext<
55
61
  TRouter extends AnyRouter,
56
62
  TSSRContext extends NextPageContext = NextPageContext,
57
- TFlags = null,
58
63
  >(
59
64
  opts: WithTRPCNoSSROptions<TRouter> | WithTRPCSSROptions<TRouter>,
60
- ): CreateTRPCNext<TRouter, TSSRContext, TFlags> {
65
+ ): CreateTRPCNext<TRouter, TSSRContext> {
61
66
  const hooks = createRootHooks<TRouter, TSSRContext>(opts);
62
67
 
63
68
  // TODO: maybe set TSSRContext to `never` when using `WithTRPCNoSSROptions`
@@ -78,6 +83,10 @@ export function createTRPCNext<
78
83
  return hooks.useQueries;
79
84
  }
80
85
 
86
+ if (key === 'useSuspenseQueries') {
87
+ return hooks.useSuspenseQueries;
88
+ }
89
+
81
90
  if (key === 'withTRPC') {
82
91
  return _withTRPC;
83
92
  }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Heavily based on urql's ssr
3
+ * https://github.com/FormidableLabs/urql/blob/main/packages/next-urql/src/with-urql-client.ts
4
+ */
5
+ import type { DehydratedState } from '@tanstack/react-query';
6
+ import { dehydrate } from '@tanstack/react-query';
7
+ import { createTRPCUntypedClient } from '@trpc/client';
8
+ import type { CoercedTransformerParameters } from '@trpc/client/unstable-internals';
9
+ import { getTransformer } from '@trpc/client/unstable-internals';
10
+ import type { TRPCClientError, TRPCClientErrorLike } from '@trpc/react-query';
11
+ import { getQueryClient } from '@trpc/react-query/shared';
12
+ import type {
13
+ AnyRouter,
14
+ Dict,
15
+ Maybe,
16
+ } from '@trpc/server/unstable-core-do-not-import';
17
+ import type {
18
+ AppContextType,
19
+ NextPageContext,
20
+ } from 'next/dist/shared/lib/utils';
21
+ import { createElement } from 'react';
22
+ import type { TRPCPrepassHelper, TRPCPrepassProps } from './withTRPC';
23
+
24
+ function transformQueryOrMutationCacheErrors<
25
+ TState extends
26
+ | DehydratedState['mutations'][0]
27
+ | DehydratedState['queries'][0],
28
+ >(result: TState): TState {
29
+ const error = result.state.error as Maybe<TRPCClientError<any>>;
30
+ if (error instanceof Error && error.name === 'TRPCClientError') {
31
+ const newError: TRPCClientErrorLike<any> = {
32
+ message: error.message,
33
+ data: error.data,
34
+ shape: error.shape,
35
+ };
36
+ return {
37
+ ...result,
38
+ state: {
39
+ ...result.state,
40
+ error: newError,
41
+ },
42
+ };
43
+ }
44
+ return result;
45
+ }
46
+
47
+ export const ssrPrepass: TRPCPrepassHelper = (opts) => {
48
+ const { parent, WithTRPC, AppOrPage } = opts;
49
+ type $PrepassProps = TRPCPrepassProps<AnyRouter, any>;
50
+
51
+ const transformer = getTransformer(
52
+ (parent as CoercedTransformerParameters).transformer,
53
+ );
54
+ WithTRPC.getInitialProps = async (appOrPageCtx: AppContextType) => {
55
+ const shouldSsr = async () => {
56
+ if (typeof window !== 'undefined') {
57
+ return false;
58
+ }
59
+ if (typeof parent.ssr === 'function') {
60
+ try {
61
+ return await parent.ssr({ ctx: appOrPageCtx.ctx });
62
+ } catch (e) {
63
+ return false;
64
+ }
65
+ }
66
+ return parent.ssr;
67
+ };
68
+ const ssrEnabled = await shouldSsr();
69
+ const AppTree = appOrPageCtx.AppTree;
70
+
71
+ // Determine if we are wrapping an App component or a Page component.
72
+ const isApp = !!appOrPageCtx.Component;
73
+ const ctx: NextPageContext = isApp
74
+ ? appOrPageCtx.ctx
75
+ : (appOrPageCtx as any as NextPageContext);
76
+
77
+ // Run the wrapped component's getInitialProps function.
78
+ let pageProps: Dict<unknown> = {};
79
+ if (AppOrPage.getInitialProps) {
80
+ const originalProps = await AppOrPage.getInitialProps(
81
+ appOrPageCtx as any,
82
+ );
83
+ const originalPageProps = isApp
84
+ ? originalProps.pageProps ?? {}
85
+ : originalProps;
86
+
87
+ pageProps = {
88
+ ...originalPageProps,
89
+ ...pageProps,
90
+ };
91
+ }
92
+ const getAppTreeProps = (props: Record<string, unknown>) =>
93
+ isApp ? { pageProps: props } : props;
94
+
95
+ if (typeof window !== 'undefined' || !ssrEnabled) {
96
+ return getAppTreeProps(pageProps);
97
+ }
98
+
99
+ const config = parent.config({ ctx });
100
+ const trpcClient = createTRPCUntypedClient(config);
101
+ const queryClient = getQueryClient(config);
102
+
103
+ const trpcProp: $PrepassProps = {
104
+ config,
105
+ trpcClient,
106
+ queryClient,
107
+ ssrState: 'prepass',
108
+ ssrContext: ctx,
109
+ };
110
+ const prepassProps = {
111
+ pageProps,
112
+ trpc: trpcProp,
113
+ };
114
+
115
+ const reactDomServer = await import('react-dom/server');
116
+
117
+ // Run the prepass step on AppTree. This will run all trpc queries on the server.
118
+ // multiple prepass ensures that we can do batching on the server
119
+ while (true) {
120
+ // render full tree
121
+ reactDomServer.renderToString(createElement(AppTree, prepassProps));
122
+ if (!queryClient.isFetching()) {
123
+ // the render didn't cause the queryClient to fetch anything
124
+ break;
125
+ }
126
+
127
+ // wait until the query cache has settled it's promises
128
+ await new Promise<void>((resolve) => {
129
+ const unsub = queryClient.getQueryCache().subscribe((event) => {
130
+ if (event?.query.getObserversCount() === 0) {
131
+ resolve();
132
+ unsub();
133
+ }
134
+ });
135
+ });
136
+ }
137
+ const dehydratedCache = dehydrate(queryClient, {
138
+ shouldDehydrateQuery(query) {
139
+ // filter out queries that are marked as trpc: { ssr: false } or are not enabled, but make sure errors are dehydrated
140
+ const isExcludedFromSSr =
141
+ query.state.fetchStatus === 'idle' &&
142
+ query.state.status === 'pending';
143
+ return !isExcludedFromSSr;
144
+ },
145
+ });
146
+ // since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects
147
+ const dehydratedCacheWithErrors = {
148
+ ...dehydratedCache,
149
+ queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors),
150
+ mutations: dehydratedCache.mutations.map(
151
+ transformQueryOrMutationCacheErrors,
152
+ ),
153
+ };
154
+
155
+ // dehydrate query client's state and add it to the props
156
+ pageProps['trpcState'] = transformer.input.serialize(
157
+ dehydratedCacheWithErrors,
158
+ );
159
+
160
+ const appTreeProps = getAppTreeProps(pageProps);
161
+
162
+ const meta =
163
+ parent.responseMeta?.({
164
+ ctx,
165
+ clientErrors: [...dehydratedCache.queries, ...dehydratedCache.mutations]
166
+ .map((v) => v.state.error)
167
+ .flatMap((err) =>
168
+ err instanceof Error && err.name === 'TRPCClientError'
169
+ ? [err as TRPCClientError<AnyRouter>]
170
+ : [],
171
+ ),
172
+ }) ?? {};
173
+
174
+ for (const [key, value] of Object.entries(meta.headers ?? {})) {
175
+ if (typeof value === 'string') {
176
+ ctx.res?.setHeader(key, value);
177
+ }
178
+ }
179
+ if (meta.status && ctx.res) {
180
+ ctx.res.statusCode = meta.status;
181
+ }
182
+
183
+ return appTreeProps;
184
+ };
185
+ };