@tanstack/router-core 1.121.33 → 1.121.39

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 (94) hide show
  1. package/dist/cjs/index.d.cts +1 -1
  2. package/dist/cjs/router.cjs +5 -0
  3. package/dist/cjs/router.cjs.map +1 -1
  4. package/dist/cjs/router.d.cts +2 -2
  5. package/dist/cjs/scroll-restoration.cjs +1 -1
  6. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  7. package/dist/cjs/serializer.cjs +146 -0
  8. package/dist/cjs/serializer.cjs.map +1 -0
  9. package/dist/cjs/serializer.d.cts +7 -1
  10. package/dist/cjs/ssr/client.cjs +12 -0
  11. package/dist/cjs/ssr/client.cjs.map +1 -0
  12. package/dist/cjs/ssr/client.d.cts +6 -0
  13. package/dist/cjs/ssr/createRequestHandler.cjs +50 -0
  14. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -0
  15. package/dist/cjs/ssr/createRequestHandler.d.cts +9 -0
  16. package/dist/cjs/ssr/handlerCallback.cjs +7 -0
  17. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -0
  18. package/dist/cjs/ssr/handlerCallback.d.cts +9 -0
  19. package/dist/cjs/ssr/headers.cjs +39 -0
  20. package/dist/cjs/ssr/headers.cjs.map +1 -0
  21. package/dist/cjs/ssr/headers.d.cts +5 -0
  22. package/dist/cjs/ssr/json.cjs +14 -0
  23. package/dist/cjs/ssr/json.cjs.map +1 -0
  24. package/dist/cjs/ssr/json.d.cts +4 -0
  25. package/dist/cjs/ssr/server.cjs +17 -0
  26. package/dist/cjs/ssr/server.cjs.map +1 -0
  27. package/dist/cjs/ssr/server.d.cts +8 -0
  28. package/dist/cjs/ssr/ssr-client.cjs +131 -0
  29. package/dist/cjs/ssr/ssr-client.cjs.map +1 -0
  30. package/dist/cjs/ssr/ssr-client.d.cts +68 -0
  31. package/dist/cjs/ssr/ssr-server.cjs +248 -0
  32. package/dist/cjs/ssr/ssr-server.cjs.map +1 -0
  33. package/dist/cjs/ssr/ssr-server.d.cts +32 -0
  34. package/dist/cjs/ssr/transformStreamWithRouter.cjs +183 -0
  35. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -0
  36. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +6 -0
  37. package/dist/cjs/ssr/tsrScript.cjs +4 -0
  38. package/dist/cjs/ssr/tsrScript.cjs.map +1 -0
  39. package/dist/cjs/ssr/tsrScript.d.cts +1 -0
  40. package/dist/esm/index.d.ts +1 -1
  41. package/dist/esm/router.d.ts +2 -2
  42. package/dist/esm/router.js +5 -0
  43. package/dist/esm/router.js.map +1 -1
  44. package/dist/esm/scroll-restoration.js +1 -1
  45. package/dist/esm/scroll-restoration.js.map +1 -1
  46. package/dist/esm/serializer.d.ts +7 -1
  47. package/dist/esm/serializer.js +146 -0
  48. package/dist/esm/serializer.js.map +1 -0
  49. package/dist/esm/ssr/client.d.ts +6 -0
  50. package/dist/esm/ssr/client.js +12 -0
  51. package/dist/esm/ssr/client.js.map +1 -0
  52. package/dist/esm/ssr/createRequestHandler.d.ts +9 -0
  53. package/dist/esm/ssr/createRequestHandler.js +50 -0
  54. package/dist/esm/ssr/createRequestHandler.js.map +1 -0
  55. package/dist/esm/ssr/handlerCallback.d.ts +9 -0
  56. package/dist/esm/ssr/handlerCallback.js +7 -0
  57. package/dist/esm/ssr/handlerCallback.js.map +1 -0
  58. package/dist/esm/ssr/headers.d.ts +5 -0
  59. package/dist/esm/ssr/headers.js +39 -0
  60. package/dist/esm/ssr/headers.js.map +1 -0
  61. package/dist/esm/ssr/json.d.ts +4 -0
  62. package/dist/esm/ssr/json.js +14 -0
  63. package/dist/esm/ssr/json.js.map +1 -0
  64. package/dist/esm/ssr/server.d.ts +8 -0
  65. package/dist/esm/ssr/server.js +17 -0
  66. package/dist/esm/ssr/server.js.map +1 -0
  67. package/dist/esm/ssr/ssr-client.d.ts +68 -0
  68. package/dist/esm/ssr/ssr-client.js +131 -0
  69. package/dist/esm/ssr/ssr-client.js.map +1 -0
  70. package/dist/esm/ssr/ssr-server.d.ts +32 -0
  71. package/dist/esm/ssr/ssr-server.js +248 -0
  72. package/dist/esm/ssr/ssr-server.js.map +1 -0
  73. package/dist/esm/ssr/transformStreamWithRouter.d.ts +6 -0
  74. package/dist/esm/ssr/transformStreamWithRouter.js +183 -0
  75. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -0
  76. package/dist/esm/ssr/tsrScript.d.ts +1 -0
  77. package/dist/esm/ssr/tsrScript.js +5 -0
  78. package/dist/esm/ssr/tsrScript.js.map +1 -0
  79. package/package.json +29 -2
  80. package/src/index.ts +1 -0
  81. package/src/router.ts +8 -5
  82. package/src/scroll-restoration.ts +1 -1
  83. package/src/serializer.ts +174 -1
  84. package/src/ssr/client.ts +15 -0
  85. package/src/ssr/createRequestHandler.ts +74 -0
  86. package/src/ssr/handlerCallback.ts +15 -0
  87. package/src/ssr/headers.ts +51 -0
  88. package/src/ssr/json.ts +18 -0
  89. package/src/ssr/server.ts +23 -0
  90. package/src/ssr/ssr-client.ts +244 -0
  91. package/src/ssr/ssr-server.ts +345 -0
  92. package/src/ssr/transformStreamWithRouter.ts +258 -0
  93. package/src/ssr/tsrScript.ts +91 -0
  94. package/src/vite-env.d.ts +4 -0
@@ -0,0 +1,244 @@
1
+ import invariant from 'tiny-invariant'
2
+ import { isPlainObject } from '../utils'
3
+ import { tsrSerializer } from '../serializer'
4
+ import type { DeferredPromiseState } from '../defer'
5
+ import type { MakeRouteMatch } from '../Matches'
6
+ import type { AnyRouter, ControllablePromise } from '../router'
7
+ import type { Manifest } from '../manifest'
8
+ import type { RouteContextOptions } from '../route'
9
+
10
+ declare global {
11
+ interface Window {
12
+ __TSR_SSR__?: TsrSsrGlobal
13
+ }
14
+ }
15
+
16
+ export interface TsrSsrGlobal {
17
+ matches: Array<SsrMatch>
18
+ streamedValues: Record<
19
+ string,
20
+ {
21
+ value: any
22
+ parsed: any
23
+ }
24
+ >
25
+ cleanScripts: () => void
26
+ dehydrated?: any
27
+ initMatch: (match: SsrMatch) => void
28
+ resolvePromise: (opts: {
29
+ matchId: string
30
+ id: number
31
+ promiseState: DeferredPromiseState<any>
32
+ }) => void
33
+ injectChunk: (opts: { matchId: string; id: number; chunk: string }) => void
34
+ closeStream: (opts: { matchId: string; id: number }) => void
35
+ }
36
+
37
+ export interface SsrMatch {
38
+ id: string
39
+ __beforeLoadContext: string
40
+ loaderData?: string
41
+ error?: string
42
+ extracted?: Array<ClientExtractedEntry>
43
+ updatedAt: MakeRouteMatch['updatedAt']
44
+ status: MakeRouteMatch['status']
45
+ }
46
+
47
+ export type ClientExtractedEntry =
48
+ | ClientExtractedStream
49
+ | ClientExtractedPromise
50
+
51
+ export interface ClientExtractedPromise extends ClientExtractedBaseEntry {
52
+ type: 'promise'
53
+ value?: ControllablePromise<any>
54
+ }
55
+
56
+ export interface ClientExtractedStream extends ClientExtractedBaseEntry {
57
+ type: 'stream'
58
+ value?: ReadableStream & { controller?: ReadableStreamDefaultController }
59
+ }
60
+
61
+ export interface ClientExtractedBaseEntry {
62
+ type: string
63
+ path: Array<string>
64
+ }
65
+
66
+ export interface ResolvePromiseState {
67
+ matchId: string
68
+ id: number
69
+ promiseState: DeferredPromiseState<any>
70
+ }
71
+
72
+ export interface DehydratedRouter {
73
+ manifest: Manifest | undefined
74
+ dehydratedData: any
75
+ lastMatchId: string
76
+ }
77
+
78
+ export async function hydrate(router: AnyRouter): Promise<any> {
79
+ invariant(
80
+ window.__TSR_SSR__?.dehydrated,
81
+ 'Expected to find a dehydrated data on window.__TSR_SSR__.dehydrated... but we did not. Please file an issue!',
82
+ )
83
+
84
+ const { manifest, dehydratedData, lastMatchId } = tsrSerializer.parse(
85
+ window.__TSR_SSR__.dehydrated,
86
+ ) as DehydratedRouter
87
+
88
+ router.ssr = {
89
+ manifest,
90
+ serializer: tsrSerializer,
91
+ }
92
+
93
+ router.clientSsr = {
94
+ getStreamedValue: <T>(key: string): T | undefined => {
95
+ if (router.isServer) {
96
+ return undefined
97
+ }
98
+
99
+ const streamedValue = window.__TSR_SSR__?.streamedValues[key]
100
+
101
+ if (!streamedValue) {
102
+ return
103
+ }
104
+
105
+ if (!streamedValue.parsed) {
106
+ streamedValue.parsed = router.ssr!.serializer.parse(streamedValue.value)
107
+ }
108
+
109
+ return streamedValue.parsed
110
+ },
111
+ }
112
+
113
+ // Hydrate the router state
114
+ const matches = router.matchRoutes(router.state.location)
115
+
116
+ // kick off loading the route chunks
117
+ const routeChunkPromise = Promise.all(
118
+ matches.map((match) => {
119
+ const route = router.looseRoutesById[match.routeId]!
120
+ return router.loadRouteChunk(route)
121
+ }),
122
+ )
123
+
124
+ // Right after hydration and before the first render, we need to rehydrate each match
125
+ // First step is to reyhdrate loaderData and __beforeLoadContext
126
+ matches.forEach((match) => {
127
+ const dehydratedMatch = window.__TSR_SSR__!.matches.find(
128
+ (d) => d.id === match.id,
129
+ )
130
+
131
+ if (!dehydratedMatch) {
132
+ return
133
+ }
134
+
135
+ Object.assign(match, dehydratedMatch)
136
+
137
+ // Handle beforeLoadContext
138
+ if (dehydratedMatch.__beforeLoadContext) {
139
+ match.__beforeLoadContext = router.ssr!.serializer.parse(
140
+ dehydratedMatch.__beforeLoadContext,
141
+ ) as any
142
+ }
143
+
144
+ // Handle loaderData
145
+ if (dehydratedMatch.loaderData) {
146
+ match.loaderData = router.ssr!.serializer.parse(
147
+ dehydratedMatch.loaderData,
148
+ )
149
+ }
150
+
151
+ // Handle error
152
+ if (dehydratedMatch.error) {
153
+ match.error = router.ssr!.serializer.parse(dehydratedMatch.error)
154
+ }
155
+
156
+ // Handle extracted
157
+ ;(match as unknown as SsrMatch).extracted?.forEach((ex) => {
158
+ deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value)
159
+ })
160
+
161
+ return match
162
+ })
163
+
164
+ router.__store.setState((s) => {
165
+ return {
166
+ ...s,
167
+ matches,
168
+ }
169
+ })
170
+
171
+ // Allow the user to handle custom hydration data
172
+ await router.options.hydrate?.(dehydratedData)
173
+
174
+ // now that all necessary data is hydrated:
175
+ // 1) fully reconstruct the route context
176
+ // 2) execute `head()` and `scripts()` for each match
177
+ await Promise.all(
178
+ router.state.matches.map(async (match) => {
179
+ const route = router.looseRoutesById[match.routeId]!
180
+
181
+ const parentMatch = router.state.matches[match.index - 1]
182
+ const parentContext = parentMatch?.context ?? router.options.context ?? {}
183
+
184
+ // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed
185
+ // so run it again and merge route context
186
+ const contextFnContext: RouteContextOptions<any, any, any, any> = {
187
+ deps: match.loaderDeps,
188
+ params: match.params,
189
+ context: parentContext,
190
+ location: router.state.location,
191
+ navigate: (opts: any) =>
192
+ router.navigate({ ...opts, _fromLocation: router.state.location }),
193
+ buildLocation: router.buildLocation,
194
+ cause: match.cause,
195
+ abortController: match.abortController,
196
+ preload: false,
197
+ matches,
198
+ }
199
+ match.__routeContext = route.options.context?.(contextFnContext) ?? {}
200
+
201
+ match.context = {
202
+ ...parentContext,
203
+ ...match.__routeContext,
204
+ ...match.__beforeLoadContext,
205
+ }
206
+
207
+ const assetContext = {
208
+ matches: router.state.matches,
209
+ match,
210
+ params: match.params,
211
+ loaderData: match.loaderData,
212
+ }
213
+ const headFnContent = await route.options.head?.(assetContext)
214
+
215
+ const scripts = await route.options.scripts?.(assetContext)
216
+
217
+ match.meta = headFnContent?.meta
218
+ match.links = headFnContent?.links
219
+ match.headScripts = headFnContent?.scripts
220
+ match.scripts = scripts
221
+ }),
222
+ )
223
+
224
+ if (matches[matches.length - 1]!.id !== lastMatchId) {
225
+ return await Promise.all([routeChunkPromise, router.load()])
226
+ }
227
+
228
+ return routeChunkPromise
229
+ }
230
+
231
+ function deepMutableSetByPath<T>(obj: T, path: Array<string>, value: any) {
232
+ // mutable set by path retaining array and object references
233
+ if (path.length === 1) {
234
+ ;(obj as any)[path[0]!] = value
235
+ }
236
+
237
+ const [key, ...rest] = path
238
+
239
+ if (Array.isArray(obj)) {
240
+ deepMutableSetByPath(obj[Number(key)], rest, value)
241
+ } else if (isPlainObject(obj)) {
242
+ deepMutableSetByPath((obj as any)[key!], rest, value)
243
+ }
244
+ }
@@ -0,0 +1,345 @@
1
+ import { default as warning } from 'tiny-warning'
2
+ import jsesc from 'jsesc'
3
+ import { TSR_DEFERRED_PROMISE, defer } from '../defer'
4
+ import { isPlainArray, isPlainObject, pick } from '../utils'
5
+ import { tsrSerializer } from '../serializer'
6
+ import minifiedTsrBootStrapScript from './tsrScript?script-string'
7
+ import type { DeferredPromise } from '../defer'
8
+ import type {
9
+ ClientExtractedBaseEntry,
10
+ DehydratedRouter,
11
+ ResolvePromiseState,
12
+ SsrMatch,
13
+ } from './ssr-client'
14
+ import type { AnyRouter } from '../router'
15
+ import type { Manifest } from '../manifest'
16
+ import type { AnyRouteMatch } from '../Matches'
17
+
18
+ export type ServerExtractedEntry =
19
+ | ServerExtractedStream
20
+ | ServerExtractedPromise
21
+
22
+ export interface ServerExtractedBaseEntry extends ClientExtractedBaseEntry {
23
+ id: number
24
+ matchIndex: number
25
+ }
26
+
27
+ export interface ServerExtractedStream extends ServerExtractedBaseEntry {
28
+ type: 'stream'
29
+ stream: ReadableStream
30
+ }
31
+
32
+ export interface ServerExtractedPromise extends ServerExtractedBaseEntry {
33
+ type: 'promise'
34
+ promise: DeferredPromise<any>
35
+ }
36
+
37
+ export function attachRouterServerSsrUtils(
38
+ router: AnyRouter,
39
+ manifest: Manifest | undefined,
40
+ ) {
41
+ router.ssr = {
42
+ manifest,
43
+ serializer: tsrSerializer,
44
+ }
45
+
46
+ router.serverSsr = {
47
+ injectedHtml: [],
48
+ streamedKeys: new Set(),
49
+ injectHtml: (getHtml) => {
50
+ const promise = Promise.resolve().then(getHtml)
51
+ router.serverSsr!.injectedHtml.push(promise)
52
+ router.emit({
53
+ type: 'onInjectedHtml',
54
+ promise,
55
+ })
56
+
57
+ return promise.then(() => {})
58
+ },
59
+ injectScript: (getScript, opts) => {
60
+ return router.serverSsr!.injectHtml(async () => {
61
+ const script = await getScript()
62
+ return `<script class='tsr-once'>${script}${
63
+ process.env.NODE_ENV === 'development' && (opts?.logScript ?? true)
64
+ ? `; console.info(\`Injected From Server:
65
+ ${jsesc(script, { quotes: 'backtick' })}\`)`
66
+ : ''
67
+ }; if (typeof __TSR_SSR__ !== 'undefined') __TSR_SSR__.cleanScripts()</script>`
68
+ })
69
+ },
70
+ streamValue: (key, value) => {
71
+ warning(
72
+ !router.serverSsr!.streamedKeys.has(key),
73
+ 'Key has already been streamed: ' + key,
74
+ )
75
+
76
+ router.serverSsr!.streamedKeys.add(key)
77
+ router.serverSsr!.injectScript(
78
+ () =>
79
+ `__TSR_SSR__.streamedValues['${key}'] = { value: ${jsesc(
80
+ router.ssr!.serializer.stringify(value),
81
+ {
82
+ isScriptContext: true,
83
+ wrap: true,
84
+ json: true,
85
+ },
86
+ )}}`,
87
+ )
88
+ },
89
+ onMatchSettled,
90
+ }
91
+
92
+ router.serverSsr.injectScript(() => minifiedTsrBootStrapScript, {
93
+ logScript: false,
94
+ })
95
+ }
96
+
97
+ export function dehydrateRouter(router: AnyRouter) {
98
+ const dehydratedRouter: DehydratedRouter = {
99
+ manifest: router.ssr!.manifest,
100
+ dehydratedData: router.options.dehydrate?.(),
101
+ lastMatchId:
102
+ router.state.matches[router.state.matches.length - 1]?.id || '',
103
+ }
104
+
105
+ router.serverSsr!.injectScript(
106
+ () =>
107
+ `__TSR_SSR__.dehydrated = ${jsesc(
108
+ router.ssr!.serializer.stringify(dehydratedRouter),
109
+ {
110
+ isScriptContext: true,
111
+ wrap: true,
112
+ json: true,
113
+ },
114
+ )}`,
115
+ )
116
+ }
117
+
118
+ export function extractAsyncLoaderData(
119
+ loaderData: any,
120
+ ctx: {
121
+ match: AnyRouteMatch
122
+ router: AnyRouter
123
+ },
124
+ ) {
125
+ const extracted: Array<ServerExtractedEntry> = []
126
+
127
+ const replaced = replaceBy(loaderData, (value, path) => {
128
+ // If it's a stream, we need to tee it so we can read it multiple times
129
+ if (value instanceof ReadableStream) {
130
+ const [copy1, copy2] = value.tee()
131
+ const entry: ServerExtractedStream = {
132
+ type: 'stream',
133
+ path,
134
+ id: extracted.length,
135
+ matchIndex: ctx.match.index,
136
+ stream: copy2,
137
+ }
138
+
139
+ extracted.push(entry)
140
+ return copy1
141
+ } else if (value instanceof Promise) {
142
+ const deferredPromise = defer(value)
143
+ const entry: ServerExtractedPromise = {
144
+ type: 'promise',
145
+ path,
146
+ id: extracted.length,
147
+ matchIndex: ctx.match.index,
148
+ promise: deferredPromise,
149
+ }
150
+ extracted.push(entry)
151
+ }
152
+
153
+ return value
154
+ })
155
+
156
+ return { replaced, extracted }
157
+ }
158
+
159
+ export function onMatchSettled(opts: {
160
+ router: AnyRouter
161
+ match: AnyRouteMatch
162
+ }) {
163
+ const { router, match } = opts
164
+
165
+ let extracted: Array<ServerExtractedEntry> | undefined = undefined
166
+ let serializedLoaderData: any = undefined
167
+ if (match.loaderData !== undefined) {
168
+ const result = extractAsyncLoaderData(match.loaderData, {
169
+ router,
170
+ match,
171
+ })
172
+ match.loaderData = result.replaced
173
+ extracted = result.extracted
174
+ serializedLoaderData = extracted.reduce(
175
+ (acc: any, entry: ServerExtractedEntry) => {
176
+ return deepImmutableSetByPath(acc, ['temp', ...entry.path], undefined)
177
+ },
178
+ { temp: result.replaced },
179
+ ).temp
180
+ }
181
+
182
+ const initCode = `__TSR_SSR__.initMatch(${jsesc(
183
+ {
184
+ id: match.id,
185
+ __beforeLoadContext: router.ssr!.serializer.stringify(
186
+ match.__beforeLoadContext,
187
+ ),
188
+ loaderData: router.ssr!.serializer.stringify(serializedLoaderData),
189
+ error: router.ssr!.serializer.stringify(match.error),
190
+ extracted: extracted?.map((entry) => pick(entry, ['type', 'path'])),
191
+ updatedAt: match.updatedAt,
192
+ status: match.status,
193
+ } satisfies SsrMatch,
194
+ {
195
+ isScriptContext: true,
196
+ wrap: true,
197
+ json: true,
198
+ },
199
+ )})`
200
+
201
+ router.serverSsr!.injectScript(() => initCode)
202
+
203
+ if (extracted) {
204
+ extracted.forEach((entry) => {
205
+ if (entry.type === 'promise') return injectPromise(entry)
206
+ return injectStream(entry)
207
+ })
208
+ }
209
+
210
+ function injectPromise(entry: ServerExtractedPromise) {
211
+ router.serverSsr!.injectScript(async () => {
212
+ await entry.promise
213
+
214
+ return `__TSR_SSR__.resolvePromise(${jsesc(
215
+ {
216
+ matchId: match.id,
217
+ id: entry.id,
218
+ promiseState: entry.promise[TSR_DEFERRED_PROMISE],
219
+ } satisfies ResolvePromiseState,
220
+ {
221
+ isScriptContext: true,
222
+ wrap: true,
223
+ json: true,
224
+ },
225
+ )})`
226
+ })
227
+ }
228
+
229
+ function injectStream(entry: ServerExtractedStream) {
230
+ // Inject a promise that resolves when the stream is done
231
+ // We do this to keep the stream open until we're done
232
+ router.serverSsr!.injectHtml(async () => {
233
+ //
234
+ try {
235
+ const reader = entry.stream.getReader()
236
+ let chunk: ReadableStreamReadResult<any> | null = null
237
+ while (!(chunk = await reader.read()).done) {
238
+ if (chunk.value) {
239
+ const code = `__TSR_SSR__.injectChunk(${jsesc(
240
+ {
241
+ matchId: match.id,
242
+ id: entry.id,
243
+ chunk: chunk.value,
244
+ },
245
+ {
246
+ isScriptContext: true,
247
+ wrap: true,
248
+ json: true,
249
+ },
250
+ )})`
251
+
252
+ router.serverSsr!.injectScript(() => code)
253
+ }
254
+ }
255
+
256
+ router.serverSsr!.injectScript(
257
+ () =>
258
+ `__TSR_SSR__.closeStream(${jsesc(
259
+ {
260
+ matchId: match.id,
261
+ id: entry.id,
262
+ },
263
+ {
264
+ isScriptContext: true,
265
+ wrap: true,
266
+ json: true,
267
+ },
268
+ )})`,
269
+ )
270
+ } catch (err) {
271
+ console.error('stream read error', err)
272
+ }
273
+
274
+ return ''
275
+ })
276
+ }
277
+ }
278
+
279
+ function deepImmutableSetByPath<T>(obj: T, path: Array<string>, value: any): T {
280
+ // immutable set by path retaining array and object references
281
+ if (path.length === 0) {
282
+ return value
283
+ }
284
+
285
+ const [key, ...rest] = path
286
+
287
+ if (Array.isArray(obj)) {
288
+ return obj.map((item, i) => {
289
+ if (i === Number(key)) {
290
+ return deepImmutableSetByPath(item, rest, value)
291
+ }
292
+ return item
293
+ }) as T
294
+ }
295
+
296
+ if (isPlainObject(obj)) {
297
+ return {
298
+ ...obj,
299
+ [key!]: deepImmutableSetByPath((obj as any)[key!], rest, value),
300
+ }
301
+ }
302
+
303
+ return obj
304
+ }
305
+
306
+ export function replaceBy<T>(
307
+ obj: T,
308
+ cb: (value: any, path: Array<string>) => any,
309
+ path: Array<string> = [],
310
+ ): T {
311
+ if (isPlainArray(obj)) {
312
+ return obj.map((value, i) => replaceBy(value, cb, [...path, `${i}`])) as any
313
+ }
314
+
315
+ if (isPlainObject(obj)) {
316
+ // Do not allow objects with illegal
317
+ const newObj: any = {}
318
+
319
+ for (const key in obj) {
320
+ newObj[key] = replaceBy(obj[key], cb, [...path, key])
321
+ }
322
+
323
+ return newObj
324
+ }
325
+
326
+ // // Detect classes, functions, and other non-serializable objects
327
+ // // and return undefined. Exclude some known types that are serializable
328
+ // if (
329
+ // typeof obj === 'function' ||
330
+ // (typeof obj === 'object' &&
331
+ // ![Object, Promise, ReadableStream].includes((obj as any)?.constructor))
332
+ // ) {
333
+ // console.info(obj)
334
+ // warning(false, `Non-serializable value ☝️ found at ${path.join('.')}`)
335
+ // return undefined as any
336
+ // }
337
+
338
+ const newObj = cb(obj, path)
339
+
340
+ if (newObj !== obj) {
341
+ return newObj
342
+ }
343
+
344
+ return obj
345
+ }