@tanstack/router-core 1.171.24 → 1.171.26

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.
@@ -82,17 +82,51 @@ function normalize(value: unknown, rejected: boolean): LoaderOutcome {
82
82
  return rejected ? [ERROR, value] : [SUCCESS, value]
83
83
  }
84
84
 
85
- function normalizeError(route: AnyRoute, cause: unknown): LoaderOutcome {
85
+ function normalizeError(
86
+ router: AnyRouter,
87
+ lane: { location: ParsedLocation },
88
+ route: AnyRoute,
89
+ cause: unknown,
90
+ signal?: AbortSignal,
91
+ notify = true,
92
+ ): LoaderOutcome {
93
+ signal?.throwIfAborted()
86
94
  let outcome = normalize(cause, true)
87
95
  if (outcome[0] !== ERROR) {
88
- return outcome
96
+ return materializeRedirect(router, lane, route, outcome, signal, notify)
89
97
  }
90
98
  try {
91
99
  route.options.onError?.(outcome[1])
92
100
  } catch (onErrorCause) {
93
101
  outcome = normalize(onErrorCause, true)
94
102
  }
95
- return outcome
103
+ signal?.throwIfAborted()
104
+ return materializeRedirect(router, lane, route, outcome, signal, notify)
105
+ }
106
+
107
+ function materializeRedirect(
108
+ router: AnyRouter,
109
+ lane: { location: ParsedLocation },
110
+ route: AnyRoute,
111
+ outcome: LoaderOutcome,
112
+ signal?: AbortSignal,
113
+ notify = true,
114
+ ): LoaderOutcome {
115
+ if (outcome[0] !== REDIRECTED) {
116
+ return outcome
117
+ }
118
+ signal?.throwIfAborted()
119
+ try {
120
+ outcome[1].options._fromLocation = lane.location
121
+ router.resolveRedirect(outcome[1])
122
+ signal?.throwIfAborted()
123
+ return outcome
124
+ } catch (cause) {
125
+ signal?.throwIfAborted()
126
+ return notify
127
+ ? normalizeError(router, lane, route, cause, signal, false)
128
+ : [ERROR, cause]
129
+ }
96
130
  }
97
131
 
98
132
  function maybe<TValue>(
@@ -197,7 +231,13 @@ async function contextualize(
197
231
  match.ssr = await resolveSsr(router, lane, index)
198
232
  } catch (cause) {
199
233
  signal?.throwIfAborted()
200
- failure = [index, stampNotFound(match, normalizeError(route, cause))]
234
+ failure = [
235
+ index,
236
+ stampNotFound(
237
+ match,
238
+ normalizeError(router, lane, route, cause, signal),
239
+ ),
240
+ ]
201
241
  end = index
202
242
  }
203
243
  signal?.throwIfAborted()
@@ -239,7 +279,13 @@ async function contextualize(
239
279
  } catch (cause) {
240
280
  signal?.throwIfAborted()
241
281
  if (!failure) {
242
- failure = [index, stampNotFound(match, normalizeError(route, cause))]
282
+ failure = [
283
+ index,
284
+ stampNotFound(
285
+ match,
286
+ normalizeError(router, lane, route, cause, signal),
287
+ ),
288
+ ]
243
289
  }
244
290
  end = index
245
291
  break
@@ -252,7 +298,10 @@ async function contextualize(
252
298
  if (validationError !== undefined) {
253
299
  failure = [
254
300
  index,
255
- stampNotFound(match, normalizeError(route, validationError)),
301
+ stampNotFound(
302
+ match,
303
+ normalizeError(router, lane, route, validationError, signal),
304
+ ),
256
305
  ]
257
306
  end = index
258
307
  break
@@ -293,7 +342,16 @@ async function contextualize(
293
342
  try {
294
343
  const beforeLoadContext = await route.options.beforeLoad(options)
295
344
  signal?.throwIfAborted()
296
- const outcome = stampNotFound(match, normalize(beforeLoadContext, false))
345
+ const outcome = stampNotFound(
346
+ match,
347
+ materializeRedirect(
348
+ router,
349
+ lane,
350
+ route,
351
+ normalize(beforeLoadContext, false),
352
+ signal,
353
+ ),
354
+ )
297
355
  if (outcome[0] !== SUCCESS) {
298
356
  failure = [index, outcome]
299
357
  end = index
@@ -307,7 +365,13 @@ async function contextualize(
307
365
  parentContext = match.context
308
366
  } catch (cause) {
309
367
  signal?.throwIfAborted()
310
- failure = [index, stampNotFound(match, normalizeError(route, cause))]
368
+ failure = [
369
+ index,
370
+ stampNotFound(
371
+ match,
372
+ normalizeError(router, lane, route, cause, signal),
373
+ ),
374
+ ]
311
375
  end = index
312
376
  break
313
377
  }
@@ -373,14 +437,13 @@ function createLoaderTask(
373
437
  (cause) => normalize(cause, true),
374
438
  )
375
439
  .then((result): LoaderOutcome => {
376
- if (
377
- result[0] !== REDIRECTED &&
378
- (signal?.aborted || match.abortController.signal.reason === lane)
379
- ) {
440
+ if (signal?.aborted || match.abortController.signal.reason === lane) {
380
441
  return [SKIPPED]
381
442
  }
382
443
  if (result[0] === ERROR) {
383
- result = normalizeError(route, result[1])
444
+ result = normalizeError(router, lane, route, result[1], signal)
445
+ } else {
446
+ result = materializeRedirect(router, lane, route, result, signal)
384
447
  }
385
448
  return stampNotFound(match, result)
386
449
  })
@@ -424,13 +487,13 @@ async function getNotFoundBoundary(
424
487
  }
425
488
  for (let candidate = index; candidate >= 0; candidate--) {
426
489
  const route = getRoute(router, matches[candidate]!)
427
- const loading = loadRouteChunk(route, false)
428
- if (loading) {
429
- try {
490
+ try {
491
+ const loading = loadRouteChunk(route, false)
492
+ if (loading) {
430
493
  await loading
431
- } catch {
432
- signal?.throwIfAborted()
433
494
  }
495
+ } catch {
496
+ signal?.throwIfAborted()
434
497
  }
435
498
  signal?.throwIfAborted()
436
499
  if (route.options.notFoundComponent) {
@@ -450,15 +513,6 @@ function abortMatches(
450
513
  }
451
514
  }
452
515
 
453
- function resolveServerRedirect(
454
- router: AnyRouter,
455
- location: ParsedLocation,
456
- value: AnyRedirect,
457
- ): ServerLoadResult {
458
- value.options._fromLocation = location
459
- return { type: 'redirect', redirect: router.resolveRedirect(value) }
460
- }
461
-
462
516
  async function applyFailure(
463
517
  router: AnyRouter,
464
518
  lane: ContextualizedLane,
@@ -530,7 +584,10 @@ async function loadNormalChunks(
530
584
  signal?.throwIfAborted()
531
585
  return [
532
586
  index,
533
- stampNotFound(match, normalizeError(route, cause)),
587
+ stampNotFound(
588
+ match,
589
+ normalizeError(router, lane, route, cause, signal),
590
+ ),
534
591
  ] as IndexedOutcome
535
592
  },
536
593
  )
@@ -540,7 +597,13 @@ async function loadNormalChunks(
540
597
  }
541
598
  } catch (cause) {
542
599
  signal?.throwIfAborted()
543
- chunks.push([index, stampNotFound(match, normalizeError(route, cause))])
600
+ chunks.push([
601
+ index,
602
+ stampNotFound(
603
+ match,
604
+ normalizeError(router, lane, route, cause, signal),
605
+ ),
606
+ ])
544
607
  }
545
608
  }
546
609
  for (const chunk of chunks) {
@@ -698,7 +761,7 @@ async function executeServerLane(
698
761
 
699
762
  if (control?.[1][0] === REDIRECTED) {
700
763
  abortMatches(lane.matches, 0, lane)
701
- return resolveServerRedirect(router, location, control[1][1])
764
+ return { type: 'redirect', redirect: control[1][1] }
702
765
  }
703
766
 
704
767
  let failure = lane.failure ?? loaderFailure
@@ -742,7 +805,7 @@ async function executeServerLane(
742
805
  if (requiredFailure) {
743
806
  if (requiredFailure[1][0] === REDIRECTED) {
744
807
  abortMatches(lane.matches)
745
- return resolveServerRedirect(router, location, requiredFailure[1][1])
808
+ return { type: 'redirect', redirect: requiredFailure[1][1] }
746
809
  }
747
810
  failure = requiredFailure
748
811
  }
@@ -808,9 +871,7 @@ export async function loadServerRoute(
808
871
  })
809
872
  if (next.publicHref !== canonical.publicHref) {
810
873
  const href = canonical.publicHref || '/'
811
- throw canonical.external
812
- ? redirect({ href })
813
- : redirect({ href, _builtLocation: canonical })
874
+ throw redirect({ href })
814
875
  }
815
876
 
816
877
  const fromLocation = router.stores.resolvedLocation.get()
@@ -828,7 +889,8 @@ export async function loadServerRoute(
828
889
  if (!isRedirect(cause)) {
829
890
  throw cause
830
891
  }
831
- result = resolveServerRedirect(router, next, cause)
892
+ cause.options._fromLocation = next
893
+ result = { type: 'redirect', redirect: router.resolveRedirect(cause) }
832
894
  }
833
895
 
834
896
  router._serverResult = result
@@ -582,7 +582,7 @@ export function findFlatMatch<T extends Extract<RouteLike, { from: string }>>(
582
582
  ) {
583
583
  path ||= '/'
584
584
  const cached = processedTree.flatCache!.get(path)
585
- if (cached) return cached
585
+ if (cached !== undefined) return cached
586
586
  const result = findMatch(path, processedTree.masksTree!)
587
587
  processedTree.flatCache!.set(path, result)
588
588
  return result
package/src/path.ts CHANGED
@@ -112,13 +112,25 @@ export function resolvePath({
112
112
  trailingSlash = 'never',
113
113
  cache,
114
114
  }: ResolvePathOptions) {
115
- const isBase = to === '.'
116
- const isAbsolute = to.startsWith('/')
115
+ if (to.includes('//')) {
116
+ to = cleanPath(to)
117
+ }
118
+
119
+ if (to.startsWith('/')) {
120
+ if (to.length === 1 || trailingSlash === 'preserve') {
121
+ return to
122
+ }
123
+ if (trailingSlash === 'always') {
124
+ return to.endsWith('/') ? to : `${to}/`
125
+ }
126
+ return to.endsWith('/') ? to.slice(0, -1) : to
127
+ }
117
128
 
129
+ const isBase = to === '.'
118
130
  let key
119
131
  if (cache) {
120
132
  // `trailingSlash` is static per router, so it doesn't need to be part of the cache key
121
- key = isAbsolute ? to : isBase ? base : base + '\0' + to
133
+ key = isBase ? base : base + '\0' + to
122
134
  const cached = cache.get(key)
123
135
  if (cached) return cached
124
136
  }
@@ -126,9 +138,10 @@ export function resolvePath({
126
138
  let baseSegments: Array<string>
127
139
  if (isBase) {
128
140
  baseSegments = base.split('/')
129
- } else if (isAbsolute) {
130
- baseSegments = to.split('/')
131
141
  } else {
142
+ if (base.includes('//')) {
143
+ base = cleanPath(base)
144
+ }
132
145
  baseSegments = base.split('/')
133
146
  while (baseSegments.length > 1 && last(baseSegments) === '') {
134
147
  baseSegments.pop()
@@ -171,7 +184,8 @@ export function resolvePath({
171
184
  }
172
185
  }
173
186
 
174
- const result = cleanPath(baseSegments.join('/')) || '/'
187
+ const joined = baseSegments.join('/')
188
+ const result = (isBase ? cleanPath(joined) : joined) || '/'
175
189
  if (key && cache) cache.set(key, result)
176
190
  return result
177
191
  }
package/src/redirect.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { NavigateOptions } from './link'
2
2
  import type { AnyRouter, RegisteredRouter } from './router'
3
- import type { ParsedLocation } from './location'
4
3
 
5
4
  export type AnyRedirect = Redirect<any, any, any, any, any>
6
5
 
@@ -14,13 +13,7 @@ export type Redirect<
14
13
  TMaskFrom extends string = TFrom,
15
14
  TMaskTo extends string = '.',
16
15
  > = Response & {
17
- options: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
18
- /**
19
- * @internal
20
- * A **trusted** built location that can be used to redirect to.
21
- */
22
- _builtLocation?: ParsedLocation
23
- }
16
+ options: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
24
17
  }
25
18
 
26
19
  export type RedirectOptions<
@@ -50,11 +43,6 @@ export type RedirectOptions<
50
43
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RedirectType#headers-property)
51
44
  */
52
45
  headers?: HeadersInit
53
- /**
54
- * @internal
55
- * A **trusted** built location that can be used to redirect to.
56
- */
57
- _builtLocation?: ParsedLocation
58
46
  } & NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
59
47
 
60
48
  export type ResolvedRedirect<
@@ -122,11 +110,7 @@ export function redirect<
122
110
  ): Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> {
123
111
  opts.statusCode = opts.statusCode || opts.code || 307
124
112
 
125
- if (
126
- !opts._builtLocation &&
127
- !opts.reloadDocument &&
128
- typeof opts.href === 'string'
129
- ) {
113
+ if (!opts.reloadDocument && typeof opts.href === 'string') {
130
114
  try {
131
115
  new URL(opts.href)
132
116
  opts.reloadDocument = true
package/src/router.ts CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  processRouteTree,
23
23
  } from './new-process-route-tree'
24
24
  import {
25
- cleanPath,
26
25
  compileDecodeCharMap,
27
26
  interpolatePath,
28
27
  resolvePath,
@@ -692,13 +691,7 @@ export type PreloadRouteFn<
692
691
  TTo,
693
692
  TMaskFrom,
694
693
  TMaskTo
695
- > & {
696
- /**
697
- * @internal
698
- * A **trusted** built location that can be used to redirect to.
699
- */
700
- _builtLocation?: ParsedLocation
701
- },
694
+ >,
702
695
  ) => Promise<Array<AnyRouteMatch> | undefined>
703
696
 
704
697
  export type MatchRouteFn<
@@ -937,10 +930,10 @@ export function runRouteLifecycle(
937
930
  router: AnyRouter,
938
931
  previous: Array<AnyRouteMatch>,
939
932
  matches: Array<AnyRouteMatch>,
940
- isCurrent?: () => boolean,
933
+ owner?: LoadTransaction,
941
934
  ): void {
942
935
  for (const match of previous) {
943
- if (isCurrent?.() === false) {
936
+ if (owner && router._tx !== owner) {
944
937
  return
945
938
  }
946
939
  if (!matches.some((candidate) => candidate.routeId === match.routeId)) {
@@ -950,7 +943,7 @@ export function runRouteLifecycle(
950
943
  }
951
944
  }
952
945
  for (const match of matches) {
953
- if (isCurrent?.() === false) {
946
+ if (owner && router._tx !== owner) {
954
947
  return
955
948
  }
956
949
  const route = (router.routesById as Record<string, AnyRoute>)[
@@ -1475,7 +1468,7 @@ export class RouterCore<
1475
1468
  resolvePathWithBase = (from: string, path: string) => {
1476
1469
  return resolvePath({
1477
1470
  base: from,
1478
- to: path.includes('//') ? cleanPath(path) : path,
1471
+ to: path,
1479
1472
  trailingSlash: this.options.trailingSlash,
1480
1473
  cache: this.resolvePathCache,
1481
1474
  })
@@ -1846,6 +1839,19 @@ export class RouterCore<
1846
1839
  unmaskOnReload?: boolean
1847
1840
  } = {},
1848
1841
  ): ParsedLocation => {
1842
+ if (dest.href) {
1843
+ const parsed = parseHref(dest.href, {} as ParsedHistoryState)
1844
+ dest = {
1845
+ ...dest,
1846
+ to: executeRewriteInput(
1847
+ this.rewrite,
1848
+ new URL(parsed.pathname, this.origin),
1849
+ ).pathname,
1850
+ search: this.options.parseSearch(parsed.search),
1851
+ hash: parsed.hash.slice(1),
1852
+ }
1853
+ }
1854
+
1849
1855
  // We allow the caller to override the current location
1850
1856
  const currentLocation =
1851
1857
  dest._fromLocation || this._pendingLocation || this.latestLocation
@@ -1885,28 +1891,19 @@ export class RouterCore<
1885
1891
  dest.unsafeRelative === 'path'
1886
1892
  ? currentLocation.pathname
1887
1893
  : (dest.from ?? lightweightResult[1 /* fullPath */])
1888
- const destTo = dest.to ? `${dest.to}` : undefined
1889
1894
 
1890
1895
  // From search should always use the current location
1891
1896
  const fromSearch = lightweightResult[2 /* search */]
1892
1897
  // Same with params. It can't hurt to provide as many as possible
1893
- const fromParams = Object.assign(
1894
- Object.create(null),
1895
- lightweightResult[3 /* params */],
1896
- )
1897
-
1898
- const isAbsoluteTo = destTo?.charCodeAt(0) === 47
1899
- const sourcePath = isAbsoluteTo
1900
- ? '/'
1901
- : this.resolvePathWithBase(defaultedFromPath, '.')
1898
+ const fromParams = lightweightResult[3 /* params */]
1902
1899
 
1903
- // Resolve the destination. Absolute destinations don't need the source path.
1904
- const nextTo = destTo
1905
- ? this.resolvePathWithBase(sourcePath, destTo)
1906
- : sourcePath
1900
+ const nextTo = this.resolvePathWithBase(
1901
+ defaultedFromPath,
1902
+ dest.to ? `${dest.to}` : '.',
1903
+ )
1907
1904
 
1908
1905
  // Resolve the next params
1909
- const nextParams = resolveNextParams(dest.params, fromParams)
1906
+ let nextParams = resolveNextParams(dest.params, fromParams)
1910
1907
 
1911
1908
  const destRoute = this.routesByPath[
1912
1909
  trimPathRight(nextTo) as keyof typeof this.routesByPath
@@ -1938,6 +1935,9 @@ export class RouterCore<
1938
1935
  const fn =
1939
1936
  route.options.params?.stringify ?? route.options.stringifyParams
1940
1937
  if (fn) {
1938
+ if (nextParams === fromParams) {
1939
+ nextParams = Object.assign(Object.create(null), nextParams)
1940
+ }
1941
1941
  try {
1942
1942
  Object.assign(nextParams, fn(nextParams))
1943
1943
  } catch {
@@ -2036,7 +2036,9 @@ export class RouterCore<
2036
2036
  : {}
2037
2037
 
2038
2038
  // Replace the equal deep
2039
- nextState = replaceEqualDeep(currentLocation.state, nextState)
2039
+ if (dest.state) {
2040
+ nextState = replaceEqualDeep(currentLocation.state, nextState)
2041
+ }
2040
2042
 
2041
2043
  // Create the full path of the location
2042
2044
  const fullPath = `${nextPathname}${searchStr}${hashStr}`
@@ -2216,38 +2218,12 @@ export class RouterCore<
2216
2218
  hashScrollIntoView,
2217
2219
  viewTransition,
2218
2220
  ignoreBlocker,
2219
- _redirects,
2220
- href,
2221
2221
  ...rest
2222
- }: BuildNextOptions &
2223
- CommitLocationOptions & { _redirects?: number } = {}) => {
2224
- if (href) {
2225
- const currentIndex = this.history.location.state.__TSR_index
2226
-
2227
- const parsed = parseHref(href, {
2228
- __TSR_index: replace ? currentIndex : currentIndex + 1,
2229
- })
2230
-
2231
- // If the href contains the basepath, we need to strip it before setting `to`
2232
- // because `buildLocation` will add the basepath back when creating the final URL.
2233
- // Without this, hrefs like '/app/about' would become '/app/app/about'.
2234
- const hrefUrl = new URL(parsed.pathname, this.origin)
2235
- const rewrittenUrl = executeRewriteInput(this.rewrite, hrefUrl)
2236
-
2237
- rest.to = rewrittenUrl.pathname
2238
- rest.search = this.options.parseSearch(parsed.search)
2239
- // remove the leading `#` from the hash
2240
- rest.hash = parsed.hash.slice(1)
2241
- }
2242
-
2222
+ }: BuildNextOptions & CommitLocationOptions = {}) => {
2243
2223
  const location = this.buildLocation({
2244
2224
  ...(rest as any),
2245
2225
  _includeValidateSearch: true,
2246
2226
  })
2247
- if (_redirects) {
2248
- ;(location as typeof location & { _redirects?: number })._redirects =
2249
- _redirects
2250
- }
2251
2227
 
2252
2228
  this._pendingLocation = location as ParsedLocation<
2253
2229
  FullSearchSchema<TRouteTree>
@@ -2514,9 +2490,8 @@ export class RouterCore<
2514
2490
  resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
2515
2491
  const locationHeader = redirect.headers.get('Location')
2516
2492
 
2517
- if (!redirect.options.href || redirect.options._builtLocation) {
2518
- const location =
2519
- redirect.options._builtLocation ?? this.buildLocation(redirect.options)
2493
+ if (!redirect.options.href) {
2494
+ const location = this.buildLocation(redirect.options)
2520
2495
  const href = location.publicHref || '/'
2521
2496
  redirect.options.href = href
2522
2497
  redirect.headers.set('Location', href)
@@ -2535,7 +2510,6 @@ export class RouterCore<
2535
2510
 
2536
2511
  if (
2537
2512
  redirect.options.href &&
2538
- !redirect.options._builtLocation &&
2539
2513
  // Check for dangerous protocols before processing the redirect
2540
2514
  isDangerousProtocol(redirect.options.href, this.protocolAllowlist)
2541
2515
  ) {
@@ -2611,7 +2585,8 @@ export class RouterCore<
2611
2585
  TTrailingSlashOption,
2612
2586
  TDefaultStructuralSharingOption,
2613
2587
  TRouterHistory
2614
- > = (opts) => preloadClientRoute(this, opts)
2588
+ > = (opts: any, builtLocation?: ParsedLocation) =>
2589
+ preloadClientRoute(this, opts, 0, builtLocation)
2615
2590
 
2616
2591
  matchRoute: MatchRouteFn<
2617
2592
  TRouteTree,
@@ -2797,24 +2772,22 @@ function applySearchMiddleware(
2797
2772
  }
2798
2773
 
2799
2774
  const routeValidateSearch = routeOptions.validateSearch
2800
- if (routeValidateSearch) {
2775
+ if (includeValidateSearch && routeValidateSearch) {
2801
2776
  const validate: SearchMiddleware<any> = ({ search, next, meta }) => {
2802
2777
  const result = next(search)
2803
- if (includeValidateSearch) {
2804
- try {
2805
- const validated = validateSearch(routeValidateSearch, result) as any
2806
-
2807
- if (meta && validated) {
2808
- for (const key in validated) {
2809
- if (!(key in result)) {
2810
- ;(meta.defaulted ||= new Map()).set(key, validated[key])
2811
- }
2778
+ try {
2779
+ const validated = validateSearch(routeValidateSearch, result) as any
2780
+
2781
+ if (meta && validated) {
2782
+ for (const key in validated) {
2783
+ if (!(key in result)) {
2784
+ ;(meta.defaulted ||= new Map()).set(key, validated[key])
2812
2785
  }
2813
2786
  }
2814
- return { ...result, ...validated }
2815
- } catch {
2816
- // ignore errors here because they are already handled in matchRoutes
2817
2787
  }
2788
+ return { ...result, ...validated }
2789
+ } catch {
2790
+ // ignore errors here because they are already handled in matchRoutes
2818
2791
  }
2819
2792
  return result
2820
2793
  }
@@ -2884,11 +2857,14 @@ function resolveNextParams(
2884
2857
  spec: unknown,
2885
2858
  base: Record<string, unknown>,
2886
2859
  ): Record<string, unknown> {
2887
- return spec === false || spec === null
2888
- ? Object.create(null)
2889
- : (spec ?? true) === true
2890
- ? base
2891
- : Object.assign(base, functionalUpdate(spec as any, base))
2860
+ if (spec === false || spec === null) {
2861
+ return Object.create(null)
2862
+ }
2863
+ if ((spec ?? true) === true) {
2864
+ return base
2865
+ }
2866
+ const next = Object.assign(Object.create(null), base)
2867
+ return Object.assign(next, functionalUpdate(spec as any, next))
2892
2868
  }
2893
2869
 
2894
2870
  function extractStrictParams(