@tanstack/start-client-core 1.20.3-alpha.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/createIsomorphicFn.cjs +7 -0
  4. package/dist/cjs/createIsomorphicFn.cjs.map +1 -0
  5. package/dist/cjs/createIsomorphicFn.d.cts +12 -0
  6. package/dist/cjs/createMiddleware.cjs +37 -0
  7. package/dist/cjs/createMiddleware.cjs.map +1 -0
  8. package/dist/cjs/createMiddleware.d.cts +175 -0
  9. package/dist/cjs/createServerFn.cjs +378 -0
  10. package/dist/cjs/createServerFn.cjs.map +1 -0
  11. package/dist/cjs/createServerFn.d.cts +159 -0
  12. package/dist/cjs/envOnly.cjs +7 -0
  13. package/dist/cjs/envOnly.cjs.map +1 -0
  14. package/dist/cjs/envOnly.d.cts +4 -0
  15. package/dist/cjs/headers.cjs +30 -0
  16. package/dist/cjs/headers.cjs.map +1 -0
  17. package/dist/cjs/headers.d.cts +5 -0
  18. package/dist/cjs/index.cjs +33 -0
  19. package/dist/cjs/index.cjs.map +1 -0
  20. package/dist/cjs/index.d.cts +11 -0
  21. package/dist/cjs/json.cjs +14 -0
  22. package/dist/cjs/json.cjs.map +1 -0
  23. package/dist/cjs/json.d.cts +2 -0
  24. package/dist/cjs/registerGlobalMiddleware.cjs +9 -0
  25. package/dist/cjs/registerGlobalMiddleware.cjs.map +1 -0
  26. package/dist/cjs/registerGlobalMiddleware.d.cts +5 -0
  27. package/dist/cjs/serializer.cjs +152 -0
  28. package/dist/cjs/serializer.cjs.map +1 -0
  29. package/dist/cjs/serializer.d.cts +2 -0
  30. package/dist/cjs/ssr-client.cjs +130 -0
  31. package/dist/cjs/ssr-client.cjs.map +1 -0
  32. package/dist/cjs/ssr-client.d.cts +64 -0
  33. package/dist/cjs/tests/createIsomorphicFn.test-d.d.cts +1 -0
  34. package/dist/cjs/tests/createServerFn.test-d.d.cts +1 -0
  35. package/dist/cjs/tests/createServerMiddleware.test-d.d.cts +1 -0
  36. package/dist/cjs/tests/envOnly.test-d.d.cts +1 -0
  37. package/dist/cjs/tests/json.test.d.cts +1 -0
  38. package/dist/cjs/tests/transformer.test.d.cts +1 -0
  39. package/dist/esm/createIsomorphicFn.d.ts +12 -0
  40. package/dist/esm/createIsomorphicFn.js +7 -0
  41. package/dist/esm/createIsomorphicFn.js.map +1 -0
  42. package/dist/esm/createMiddleware.d.ts +175 -0
  43. package/dist/esm/createMiddleware.js +37 -0
  44. package/dist/esm/createMiddleware.js.map +1 -0
  45. package/dist/esm/createServerFn.d.ts +159 -0
  46. package/dist/esm/createServerFn.js +356 -0
  47. package/dist/esm/createServerFn.js.map +1 -0
  48. package/dist/esm/envOnly.d.ts +4 -0
  49. package/dist/esm/envOnly.js +7 -0
  50. package/dist/esm/envOnly.js.map +1 -0
  51. package/dist/esm/headers.d.ts +5 -0
  52. package/dist/esm/headers.js +30 -0
  53. package/dist/esm/headers.js.map +1 -0
  54. package/dist/esm/index.d.ts +11 -0
  55. package/dist/esm/index.js +30 -0
  56. package/dist/esm/index.js.map +1 -0
  57. package/dist/esm/json.d.ts +2 -0
  58. package/dist/esm/json.js +14 -0
  59. package/dist/esm/json.js.map +1 -0
  60. package/dist/esm/registerGlobalMiddleware.d.ts +5 -0
  61. package/dist/esm/registerGlobalMiddleware.js +9 -0
  62. package/dist/esm/registerGlobalMiddleware.js.map +1 -0
  63. package/dist/esm/serializer.d.ts +2 -0
  64. package/dist/esm/serializer.js +152 -0
  65. package/dist/esm/serializer.js.map +1 -0
  66. package/dist/esm/ssr-client.d.ts +64 -0
  67. package/dist/esm/ssr-client.js +130 -0
  68. package/dist/esm/ssr-client.js.map +1 -0
  69. package/dist/esm/tests/createIsomorphicFn.test-d.d.ts +1 -0
  70. package/dist/esm/tests/createServerFn.test-d.d.ts +1 -0
  71. package/dist/esm/tests/createServerMiddleware.test-d.d.ts +1 -0
  72. package/dist/esm/tests/envOnly.test-d.d.ts +1 -0
  73. package/dist/esm/tests/json.test.d.ts +1 -0
  74. package/dist/esm/tests/transformer.test.d.ts +1 -0
  75. package/package.json +56 -0
  76. package/src/createIsomorphicFn.ts +36 -0
  77. package/src/createMiddleware.ts +706 -0
  78. package/src/createServerFn.ts +1004 -0
  79. package/src/envOnly.ts +9 -0
  80. package/src/headers.ts +50 -0
  81. package/src/index.tsx +88 -0
  82. package/src/json.ts +15 -0
  83. package/src/registerGlobalMiddleware.ts +9 -0
  84. package/src/serializer.ts +177 -0
  85. package/src/ssr-client.tsx +243 -0
  86. package/src/tests/createIsomorphicFn.test-d.ts +72 -0
  87. package/src/tests/createServerFn.test-d.ts +519 -0
  88. package/src/tests/createServerMiddleware.test-d.ts +736 -0
  89. package/src/tests/envOnly.test-d.ts +34 -0
  90. package/src/tests/json.test.ts +37 -0
  91. package/src/tests/transformer.test.tsx +147 -0
package/src/envOnly.ts ADDED
@@ -0,0 +1,9 @@
1
+ type EnvOnlyFn = <TFn extends (...args: Array<any>) => any>(fn: TFn) => TFn
2
+
3
+ // A function that will only be available in the server build
4
+ // If called on the client, it will throw an error
5
+ export const serverOnly: EnvOnlyFn = (fn) => fn
6
+
7
+ // A function that will only be available in the client build
8
+ // If called on the server, it will throw an error
9
+ export const clientOnly: EnvOnlyFn = (fn) => fn
package/src/headers.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { splitSetCookieString } from 'cookie-es'
2
+ import type { OutgoingHttpHeaders } from 'node:http2'
3
+ // A utility function to turn HeadersInit into an object
4
+ export function headersInitToObject(
5
+ headers: HeadersInit,
6
+ ): Record<keyof OutgoingHttpHeaders, string> {
7
+ const obj: Record<keyof OutgoingHttpHeaders, string> = {}
8
+ const headersInstance = new Headers(headers)
9
+ for (const [key, value] of headersInstance.entries()) {
10
+ obj[key] = value
11
+ }
12
+ return obj
13
+ }
14
+
15
+ type AnyHeaders =
16
+ | Headers
17
+ | HeadersInit
18
+ | Record<string, string>
19
+ | Array<[string, string]>
20
+ | OutgoingHttpHeaders
21
+ | undefined
22
+
23
+ // Helper function to convert various HeaderInit types to a Headers instance
24
+ function toHeadersInstance(init: AnyHeaders) {
25
+ if (init instanceof Headers) {
26
+ return new Headers(init)
27
+ } else if (Array.isArray(init)) {
28
+ return new Headers(init)
29
+ } else if (typeof init === 'object') {
30
+ return new Headers(init as HeadersInit)
31
+ } else {
32
+ return new Headers()
33
+ }
34
+ }
35
+
36
+ // Function to merge headers with proper overrides
37
+ export function mergeHeaders(...headers: Array<AnyHeaders>) {
38
+ return headers.reduce((acc: Headers, header) => {
39
+ const headersInstance = toHeadersInstance(header)
40
+ for (const [key, value] of headersInstance.entries()) {
41
+ if (key === 'set-cookie') {
42
+ const splitCookies = splitSetCookieString(value)
43
+ splitCookies.forEach((cookie) => acc.append('set-cookie', cookie))
44
+ } else {
45
+ acc.set(key, value)
46
+ }
47
+ }
48
+ return acc
49
+ }, new Headers())
50
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,88 @@
1
+ export { mergeHeaders } from './headers'
2
+ export { startSerializer } from './serializer'
3
+ export {
4
+ type DehydratedRouter,
5
+ type ClientExtractedBaseEntry,
6
+ type StartSsrGlobal,
7
+ type ClientExtractedEntry,
8
+ type SsrMatch,
9
+ type ClientExtractedPromise,
10
+ type ClientExtractedStream,
11
+ type ResolvePromiseState,
12
+ hydrate,
13
+ } from './ssr-client'
14
+ export {
15
+ createIsomorphicFn,
16
+ type IsomorphicFn,
17
+ type ServerOnlyFn,
18
+ type ClientOnlyFn,
19
+ type IsomorphicFnBase,
20
+ } from './createIsomorphicFn'
21
+ export { serverOnly, clientOnly } from './envOnly'
22
+ export { type JsonResponse, createServerFn } from './createServerFn'
23
+ export { json } from './json'
24
+ export {
25
+ createMiddleware,
26
+ type IntersectAllValidatorInputs,
27
+ type IntersectAllValidatorOutputs,
28
+ type FunctionMiddlewareServerFn,
29
+ type AnyFunctionMiddleware,
30
+ type FunctionMiddlewareOptions,
31
+ type FunctionMiddlewareWithTypes,
32
+ type FunctionMiddlewareValidator,
33
+ type FunctionMiddlewareServer,
34
+ type FunctionMiddlewareAfterClient,
35
+ type FunctionMiddlewareAfterServer,
36
+ type FunctionMiddleware,
37
+ type FunctionMiddlewareClientFnOptions,
38
+ type FunctionMiddlewareClientFnResult,
39
+ type FunctionMiddlewareClientNextFn,
40
+ type FunctionClientResultWithContext,
41
+ type AssignAllClientContextBeforeNext,
42
+ type AssignAllMiddleware,
43
+ type AssignAllServerContext,
44
+ type FunctionMiddlewareAfterValidator,
45
+ type FunctionMiddlewareClientFn,
46
+ type FunctionMiddlewareServerFnResult,
47
+ type FunctionMiddlewareClient,
48
+ type FunctionMiddlewareServerFnOptions,
49
+ type FunctionMiddlewareServerNextFn,
50
+ type FunctionServerResultWithContext,
51
+ type AnyRequestMiddleware,
52
+ } from './createMiddleware'
53
+ export {
54
+ registerGlobalMiddleware,
55
+ globalMiddleware,
56
+ } from './registerGlobalMiddleware'
57
+ export type {
58
+ ServerFn as FetchFn,
59
+ ServerFnCtx as FetchFnCtx,
60
+ CompiledFetcherFnOptions,
61
+ CompiledFetcherFn,
62
+ Fetcher,
63
+ RscStream,
64
+ FetcherData,
65
+ FetcherBaseOptions,
66
+ ServerFn,
67
+ ServerFnCtx,
68
+ ServerFnResponseType,
69
+ MiddlewareFn,
70
+ ServerFnMiddlewareOptions,
71
+ ServerFnMiddlewareResult,
72
+ ServerFnBuilder,
73
+ ServerFnType,
74
+ ServerFnBaseOptions,
75
+ NextFn,
76
+ Method,
77
+ StaticCachedResult,
78
+ OptionalFetcher,
79
+ } from './createServerFn'
80
+ export {
81
+ applyMiddleware,
82
+ execValidator,
83
+ serverFnBaseToMiddleware,
84
+ extractFormDataContext,
85
+ flattenMiddlewares,
86
+ serverFnStaticCache,
87
+ executeMiddleware,
88
+ } from './createServerFn'
package/src/json.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { mergeHeaders } from './headers'
2
+ import type { JsonResponse } from './createServerFn'
3
+
4
+ export function json<TData>(
5
+ payload: TData,
6
+ init?: ResponseInit,
7
+ ): JsonResponse<TData> {
8
+ return new Response(JSON.stringify(payload), {
9
+ ...init,
10
+ headers: mergeHeaders(
11
+ { 'content-type': 'application/json' },
12
+ init?.headers,
13
+ ),
14
+ })
15
+ }
@@ -0,0 +1,9 @@
1
+ import type { AnyFunctionMiddleware } from './createMiddleware'
2
+
3
+ export const globalMiddleware: Array<AnyFunctionMiddleware> = []
4
+
5
+ export function registerGlobalMiddleware(options: {
6
+ middleware: Array<AnyFunctionMiddleware>
7
+ }) {
8
+ globalMiddleware.push(...options.middleware)
9
+ }
@@ -0,0 +1,177 @@
1
+ import { isPlainObject } from '@tanstack/router-core'
2
+ import type { StartSerializer } from '@tanstack/router-core'
3
+
4
+ export const startSerializer: StartSerializer = {
5
+ stringify: (value: any) =>
6
+ JSON.stringify(value, function replacer(key, val) {
7
+ const ogVal = this[key]
8
+ const serializer = serializers.find((t) => t.stringifyCondition(ogVal))
9
+
10
+ if (serializer) {
11
+ return serializer.stringify(ogVal)
12
+ }
13
+
14
+ return val
15
+ }),
16
+ parse: (value: string) =>
17
+ JSON.parse(value, function parser(key, val) {
18
+ const ogVal = this[key]
19
+ if (isPlainObject(ogVal)) {
20
+ const serializer = serializers.find((t) => t.parseCondition(ogVal))
21
+
22
+ if (serializer) {
23
+ return serializer.parse(ogVal)
24
+ }
25
+ }
26
+
27
+ return val
28
+ }),
29
+ encode: (value: any) => {
30
+ // When encoding, dive first
31
+ if (Array.isArray(value)) {
32
+ return value.map((v) => startSerializer.encode(v))
33
+ }
34
+
35
+ if (isPlainObject(value)) {
36
+ return Object.fromEntries(
37
+ Object.entries(value).map(([key, v]) => [
38
+ key,
39
+ startSerializer.encode(v),
40
+ ]),
41
+ )
42
+ }
43
+
44
+ const serializer = serializers.find((t) => t.stringifyCondition(value))
45
+ if (serializer) {
46
+ return serializer.stringify(value)
47
+ }
48
+
49
+ return value
50
+ },
51
+ decode: (value: any) => {
52
+ // Attempt transform first
53
+ if (isPlainObject(value)) {
54
+ const serializer = serializers.find((t) => t.parseCondition(value))
55
+ if (serializer) {
56
+ return serializer.parse(value)
57
+ }
58
+ }
59
+
60
+ if (Array.isArray(value)) {
61
+ return value.map((v) => startSerializer.decode(v))
62
+ }
63
+
64
+ if (isPlainObject(value)) {
65
+ return Object.fromEntries(
66
+ Object.entries(value).map(([key, v]) => [
67
+ key,
68
+ startSerializer.decode(v),
69
+ ]),
70
+ )
71
+ }
72
+
73
+ return value
74
+ },
75
+ }
76
+
77
+ const createSerializer = <TKey extends string, TInput, TSerialized>(
78
+ key: TKey,
79
+ check: (value: any) => value is TInput,
80
+ toValue: (value: TInput) => TSerialized,
81
+ fromValue: (value: TSerialized) => TInput,
82
+ ) => ({
83
+ key,
84
+ stringifyCondition: check,
85
+ stringify: (value: any) => ({ [`$${key}`]: toValue(value) }),
86
+ parseCondition: (value: any) => Object.hasOwn(value, `$${key}`),
87
+ parse: (value: any) => fromValue(value[`$${key}`]),
88
+ })
89
+
90
+ // Keep these ordered by predicted frequency
91
+ // Make sure to keep DefaultSerializable in sync with these serializers
92
+ // Also, make sure that they are unit tested in serializer.test.tsx
93
+ const serializers = [
94
+ createSerializer(
95
+ // Key
96
+ 'undefined',
97
+ // Check
98
+ (v): v is undefined => v === undefined,
99
+ // To
100
+ () => 0,
101
+ // From
102
+ () => undefined,
103
+ ),
104
+ createSerializer(
105
+ // Key
106
+ 'date',
107
+ // Check
108
+ (v): v is Date => v instanceof Date,
109
+ // To
110
+ (v) => v.toISOString(),
111
+ // From
112
+ (v) => new Date(v),
113
+ ),
114
+ createSerializer(
115
+ // Key
116
+ 'error',
117
+ // Check
118
+ (v): v is Error => v instanceof Error,
119
+ // To
120
+ (v) => ({
121
+ ...v,
122
+ message: v.message,
123
+ stack: process.env.NODE_ENV === 'development' ? v.stack : undefined,
124
+ cause: v.cause,
125
+ }),
126
+ // From
127
+ (v) => Object.assign(new Error(v.message), v),
128
+ ),
129
+ createSerializer(
130
+ // Key
131
+ 'formData',
132
+ // Check
133
+ (v): v is FormData => v instanceof FormData,
134
+ // To
135
+ (v) => {
136
+ const entries: Record<
137
+ string,
138
+ Array<FormDataEntryValue> | FormDataEntryValue
139
+ > = {}
140
+ v.forEach((value, key) => {
141
+ const entry = entries[key]
142
+ if (entry !== undefined) {
143
+ if (Array.isArray(entry)) {
144
+ entry.push(value)
145
+ } else {
146
+ entries[key] = [entry, value]
147
+ }
148
+ } else {
149
+ entries[key] = value
150
+ }
151
+ })
152
+ return entries
153
+ },
154
+ // From
155
+ (v) => {
156
+ const formData = new FormData()
157
+ Object.entries(v).forEach(([key, value]) => {
158
+ if (Array.isArray(value)) {
159
+ value.forEach((val) => formData.append(key, val))
160
+ } else {
161
+ formData.append(key, value)
162
+ }
163
+ })
164
+ return formData
165
+ },
166
+ ),
167
+ createSerializer(
168
+ // Key
169
+ 'bigint',
170
+ // Check
171
+ (v): v is bigint => typeof v === 'bigint',
172
+ // To
173
+ (v) => v.toString(),
174
+ // From
175
+ (v) => BigInt(v),
176
+ ),
177
+ ] as const
@@ -0,0 +1,243 @@
1
+ import { isPlainObject } from '@tanstack/router-core'
2
+
3
+ import invariant from 'tiny-invariant'
4
+
5
+ import { startSerializer } from './serializer'
6
+ import type {
7
+ AnyRouter,
8
+ ControllablePromise,
9
+ DeferredPromiseState,
10
+ MakeRouteMatch,
11
+ Manifest,
12
+ RouteContextOptions,
13
+ } from '@tanstack/router-core'
14
+
15
+ declare global {
16
+ interface Window {
17
+ __TSR_SSR__?: StartSsrGlobal
18
+ }
19
+ }
20
+
21
+ export interface StartSsrGlobal {
22
+ matches: Array<SsrMatch>
23
+ streamedValues: Record<
24
+ string,
25
+ {
26
+ value: any
27
+ parsed: any
28
+ }
29
+ >
30
+ cleanScripts: () => void
31
+ dehydrated?: any
32
+ initMatch: (match: SsrMatch) => void
33
+ resolvePromise: (opts: {
34
+ matchId: string
35
+ id: number
36
+ promiseState: DeferredPromiseState<any>
37
+ }) => void
38
+ injectChunk: (opts: { matchId: string; id: number; chunk: string }) => void
39
+ closeStream: (opts: { matchId: string; id: number }) => void
40
+ }
41
+
42
+ export interface SsrMatch {
43
+ id: string
44
+ __beforeLoadContext: string
45
+ loaderData?: string
46
+ error?: string
47
+ extracted?: Array<ClientExtractedEntry>
48
+ updatedAt: MakeRouteMatch['updatedAt']
49
+ status: MakeRouteMatch['status']
50
+ }
51
+
52
+ export type ClientExtractedEntry =
53
+ | ClientExtractedStream
54
+ | ClientExtractedPromise
55
+
56
+ export interface ClientExtractedPromise extends ClientExtractedBaseEntry {
57
+ type: 'promise'
58
+ value?: ControllablePromise<any>
59
+ }
60
+
61
+ export interface ClientExtractedStream extends ClientExtractedBaseEntry {
62
+ type: 'stream'
63
+ value?: ReadableStream & { controller?: ReadableStreamDefaultController }
64
+ }
65
+
66
+ export interface ClientExtractedBaseEntry {
67
+ type: string
68
+ path: Array<string>
69
+ }
70
+
71
+ export interface ResolvePromiseState {
72
+ matchId: string
73
+ id: number
74
+ promiseState: DeferredPromiseState<any>
75
+ }
76
+
77
+ export interface DehydratedRouter {
78
+ manifest: Manifest | undefined
79
+ dehydratedData: any
80
+ }
81
+
82
+ export function hydrate(router: AnyRouter) {
83
+ invariant(
84
+ window.__TSR_SSR__?.dehydrated,
85
+ 'Expected to find a dehydrated data on window.__TSR_SSR__.dehydrated... but we did not. Please file an issue!',
86
+ )
87
+
88
+ const { manifest, dehydratedData } = startSerializer.parse(
89
+ window.__TSR_SSR__.dehydrated,
90
+ ) as DehydratedRouter
91
+
92
+ router.ssr = {
93
+ manifest,
94
+ serializer: startSerializer,
95
+ }
96
+
97
+ router.clientSsr = {
98
+ getStreamedValue: <T,>(key: string): T | undefined => {
99
+ if (router.isServer) {
100
+ return undefined
101
+ }
102
+
103
+ const streamedValue = window.__TSR_SSR__?.streamedValues[key]
104
+
105
+ if (!streamedValue) {
106
+ return
107
+ }
108
+
109
+ if (!streamedValue.parsed) {
110
+ streamedValue.parsed = router.ssr!.serializer.parse(streamedValue.value)
111
+ }
112
+
113
+ return streamedValue.parsed
114
+ },
115
+ }
116
+
117
+ // Hydrate the router state
118
+ const matches = router.matchRoutes(router.state.location)
119
+ // kick off loading the route chunks
120
+ const routeChunkPromise = Promise.all(
121
+ matches.map((match) => {
122
+ const route = router.looseRoutesById[match.routeId]!
123
+ return router.loadRouteChunk(route)
124
+ }),
125
+ )
126
+ // Right after hydration and before the first render, we need to rehydrate each match
127
+ // First step is to reyhdrate loaderData and __beforeLoadContext
128
+ matches.forEach((match) => {
129
+ const dehydratedMatch = window.__TSR_SSR__!.matches.find(
130
+ (d) => d.id === match.id,
131
+ )
132
+
133
+ if (dehydratedMatch) {
134
+ Object.assign(match, dehydratedMatch)
135
+
136
+ // Handle beforeLoadContext
137
+ if (dehydratedMatch.__beforeLoadContext) {
138
+ match.__beforeLoadContext = router.ssr!.serializer.parse(
139
+ dehydratedMatch.__beforeLoadContext,
140
+ ) as any
141
+ }
142
+
143
+ // Handle loaderData
144
+ if (dehydratedMatch.loaderData) {
145
+ match.loaderData = router.ssr!.serializer.parse(
146
+ dehydratedMatch.loaderData,
147
+ )
148
+ }
149
+
150
+ // Handle error
151
+ if (dehydratedMatch.error) {
152
+ match.error = router.ssr!.serializer.parse(dehydratedMatch.error)
153
+ }
154
+
155
+ // Handle extracted
156
+ ;(match as unknown as SsrMatch).extracted?.forEach((ex) => {
157
+ deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value)
158
+ })
159
+ } else {
160
+ Object.assign(match, {
161
+ status: 'success',
162
+ updatedAt: Date.now(),
163
+ })
164
+ }
165
+
166
+ return match
167
+ })
168
+
169
+ router.__store.setState((s) => {
170
+ return {
171
+ ...s,
172
+ matches,
173
+ }
174
+ })
175
+
176
+ // Allow the user to handle custom hydration data
177
+ router.options.hydrate?.(dehydratedData)
178
+
179
+ // now that all necessary data is hydrated:
180
+ // 1) fully reconstruct the route context
181
+ // 2) execute `head()` and `scripts()` for each match
182
+ router.state.matches.forEach((match) => {
183
+ const route = router.looseRoutesById[match.routeId]!
184
+
185
+ const parentMatch = router.state.matches[match.index - 1]
186
+ const parentContext = parentMatch?.context ?? router.options.context ?? {}
187
+
188
+ // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
189
+ // so run it again and merge route context
190
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
191
+ deps: match.loaderDeps,
192
+ params: match.params,
193
+ context: parentContext,
194
+ location: router.state.location,
195
+ navigate: (opts: any) =>
196
+ router.navigate({ ...opts, _fromLocation: router.state.location }),
197
+ buildLocation: router.buildLocation,
198
+ cause: match.cause,
199
+ abortController: match.abortController,
200
+ preload: false,
201
+ matches,
202
+ }
203
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
204
+
205
+ match.context = {
206
+ ...parentContext,
207
+ ...match.__routeContext,
208
+ ...match.__beforeLoadContext,
209
+ }
210
+
211
+ const assetContext = {
212
+ matches: router.state.matches,
213
+ match,
214
+ params: match.params,
215
+ loaderData: match.loaderData,
216
+ }
217
+ const headFnContent = route.options.head?.(assetContext)
218
+
219
+ const scripts = route.options.scripts?.(assetContext)
220
+
221
+ match.meta = headFnContent?.meta
222
+ match.links = headFnContent?.links
223
+ match.headScripts = headFnContent?.scripts
224
+ match.scripts = scripts
225
+ })
226
+
227
+ return routeChunkPromise
228
+ }
229
+
230
+ function deepMutableSetByPath<T>(obj: T, path: Array<string>, value: any) {
231
+ // mutable set by path retaining array and object references
232
+ if (path.length === 1) {
233
+ ;(obj as any)[path[0]!] = value
234
+ }
235
+
236
+ const [key, ...rest] = path
237
+
238
+ if (Array.isArray(obj)) {
239
+ deepMutableSetByPath(obj[Number(key)], rest, value)
240
+ } else if (isPlainObject(obj)) {
241
+ deepMutableSetByPath((obj as any)[key!], rest, value)
242
+ }
243
+ }
@@ -0,0 +1,72 @@
1
+ import { expectTypeOf, test } from 'vitest'
2
+ import { createIsomorphicFn } from '../createIsomorphicFn'
3
+
4
+ test('createIsomorphicFn with no implementations', () => {
5
+ const fn = createIsomorphicFn()
6
+
7
+ expectTypeOf(fn).toBeCallableWith()
8
+ expectTypeOf(fn).returns.toBeUndefined()
9
+
10
+ expectTypeOf(fn).toHaveProperty('server')
11
+ expectTypeOf(fn).toHaveProperty('client')
12
+ })
13
+
14
+ test('createIsomorphicFn with server implementation', () => {
15
+ const fn = createIsomorphicFn().server(() => 'data')
16
+
17
+ expectTypeOf(fn).toBeCallableWith()
18
+ expectTypeOf(fn).returns.toEqualTypeOf<string | undefined>()
19
+
20
+ expectTypeOf(fn).toHaveProperty('client')
21
+ expectTypeOf(fn).not.toHaveProperty('server')
22
+ })
23
+
24
+ test('createIsomorphicFn with client implementation', () => {
25
+ const fn = createIsomorphicFn().client(() => 'data')
26
+
27
+ expectTypeOf(fn).toBeCallableWith()
28
+ expectTypeOf(fn).returns.toEqualTypeOf<string | undefined>()
29
+
30
+ expectTypeOf(fn).toHaveProperty('server')
31
+ expectTypeOf(fn).not.toHaveProperty('client')
32
+ })
33
+
34
+ test('createIsomorphicFn with server and client implementation', () => {
35
+ const fn = createIsomorphicFn()
36
+ .server(() => 'data')
37
+ .client(() => 'data')
38
+
39
+ expectTypeOf(fn).toBeCallableWith()
40
+ expectTypeOf(fn).returns.toEqualTypeOf<string>()
41
+
42
+ expectTypeOf(fn).not.toHaveProperty('server')
43
+ expectTypeOf(fn).not.toHaveProperty('client')
44
+ })
45
+
46
+ test('createIsomorphicFn with varying returns', () => {
47
+ const fn = createIsomorphicFn()
48
+ .server(() => 'data')
49
+ .client(() => 1)
50
+ expectTypeOf(fn).toBeCallableWith()
51
+ expectTypeOf(fn).returns.toEqualTypeOf<string | number>()
52
+ })
53
+
54
+ test('createIsomorphicFn with arguments', () => {
55
+ const fn = createIsomorphicFn()
56
+ .server((a: number, b: string) => 'data')
57
+ .client((...args) => {
58
+ expectTypeOf(args).toEqualTypeOf<[number, string]>()
59
+ return 1
60
+ })
61
+ expectTypeOf(fn).toBeCallableWith(1, 'a')
62
+ expectTypeOf(fn).returns.toEqualTypeOf<string | number>()
63
+
64
+ const fn2 = createIsomorphicFn()
65
+ .client((a: number, b: string) => 'data')
66
+ .server((...args) => {
67
+ expectTypeOf(args).toEqualTypeOf<[number, string]>()
68
+ return 1
69
+ })
70
+ expectTypeOf(fn2).toBeCallableWith(1, 'a')
71
+ expectTypeOf(fn2).returns.toEqualTypeOf<string | number>()
72
+ })