@tanstack/router-core 1.171.10 → 1.171.11

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.
@@ -1,4 +1,4 @@
1
- import { createNull, deepEqual } from "./utils.js";
1
+ import { deepEqual } from "./utils.js";
2
2
  //#region src/searchMiddleware.ts
3
3
  /**
4
4
  * Retain specified search params across navigations.
@@ -12,33 +12,26 @@ import { createNull, deepEqual } from "./utils.js";
12
12
  */
13
13
  function retainSearchParams(keys) {
14
14
  return ({ search, next }) => {
15
- const [resultSearch, validations] = next(search, true);
16
- const defaultKeys = validations.length ? getValidationDefaultKeys(search, resultSearch, validations) : void 0;
15
+ const { search: resultSearch, meta } = next(search, true);
17
16
  if (keys === true) {
18
17
  const copy = {
19
18
  ...search,
20
19
  ...resultSearch
21
20
  };
22
- if (defaultKeys) for (const key in defaultKeys) copy[key] = search[key];
21
+ const removed = meta.removed;
22
+ for (const key of removed?.keys() || []) if (deepEqual(search[key], removed.get(key))) delete copy[key];
23
+ for (const key of meta.removedAny || []) delete copy[key];
24
+ for (const key of meta.defaulted?.keys() || []) if (key in search && !meta.removedAny?.has(key) && !(meta.removed?.has(key) && deepEqual(search[key], meta.removed.get(key)))) copy[key] = search[key];
23
25
  return copy;
24
26
  }
25
27
  const copy = { ...resultSearch };
26
- for (const key of keys) if (!(key in copy) || (defaultKeys ? key in defaultKeys : false)) copy[key] = search[key];
28
+ for (const key of keys) {
29
+ const stringKey = key;
30
+ if (!(meta.removedAny?.has(stringKey) || meta.removed?.has(stringKey) && deepEqual(search[key], meta.removed.get(stringKey))) && (!(key in copy) || key in search && meta.defaulted?.has(stringKey) && deepEqual(copy[key], meta.defaulted.get(stringKey)))) copy[key] = search[key];
31
+ }
27
32
  return copy;
28
33
  };
29
34
  }
30
- function getValidationDefaultKeys(search, resultSearch, validations) {
31
- let defaultKeys;
32
- for (let i = 0; i < validations.length; i += 2) {
33
- const baseSearch = validations[i];
34
- const validatedSearch = validations[i + 1];
35
- for (const key in validatedSearch) if (key in search && !(key in baseSearch) && resultSearch[key] === validatedSearch[key]) {
36
- const target = defaultKeys || (defaultKeys = createNull());
37
- target[key] = true;
38
- }
39
- }
40
- return defaultKeys;
41
- }
42
35
  /**
43
36
  * Remove optional or default-valued search params from navigations.
44
37
  *
@@ -50,17 +43,26 @@ function getValidationDefaultKeys(search, resultSearch, validations) {
50
43
  * @link https://tanstack.com/router/latest/docs/framework/react/api/router/stripSearchParamsFunction
51
44
  */
52
45
  function stripSearchParams(input) {
53
- return ({ search, next }) => {
54
- if (input === true) return {};
46
+ return (({ search, next, meta }) => {
47
+ if (input === true) {
48
+ Object.keys(search).forEach((key) => {
49
+ if (meta) (meta.removedAny ||= /* @__PURE__ */ new Set()).add(key);
50
+ });
51
+ return {};
52
+ }
55
53
  const result = { ...next(search) };
56
54
  if (Array.isArray(input)) input.forEach((key) => {
57
55
  delete result[key];
56
+ if (meta) (meta.removedAny ||= /* @__PURE__ */ new Set()).add(key);
58
57
  });
59
58
  else Object.entries(input).forEach(([key, value]) => {
60
- if (deepEqual(result[key], value)) delete result[key];
59
+ if (deepEqual(result[key], value)) {
60
+ delete result[key];
61
+ if (meta) (meta.removed ||= /* @__PURE__ */ new Map()).set(key, value);
62
+ }
61
63
  });
62
64
  return result;
63
- };
65
+ });
64
66
  }
65
67
  //#endregion
66
68
  export { retainSearchParams, stripSearchParams };
@@ -1 +1 @@
1
- {"version":3,"file":"searchMiddleware.js","names":[],"sources":["../../src/searchMiddleware.ts"],"sourcesContent":["import { createNull, deepEqual } from './utils'\nimport type { NoInfer, PickOptional } from './utils'\nimport type { SearchMiddleware } from './route'\nimport type { IsRequiredParams } from './link'\n\n/**\n * Retain specified search params across navigations.\n *\n * If `keys` is `true`, retain all current params. Otherwise, copy only the\n * listed keys from the current search into the next search.\n *\n * @param keys `true` to retain all, or a list of keys to retain.\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/retainSearchParamsFunction\n */\nexport function retainSearchParams<TSearchSchema extends object>(\n keys: Array<keyof TSearchSchema> | true,\n): SearchMiddleware<TSearchSchema> {\n return ({ search, next }) => {\n const [resultSearch, validations] = (next as any)(search, true) as [\n TSearchSchema,\n Array<Record<PropertyKey, unknown>>,\n ]\n const defaultKeys = validations.length\n ? getValidationDefaultKeys(search, resultSearch, validations)\n : undefined\n\n if (keys === true) {\n const copy = { ...search, ...resultSearch }\n if (defaultKeys) {\n for (const key in defaultKeys) {\n copy[key as keyof TSearchSchema] = search[key as keyof TSearchSchema]\n }\n }\n return copy\n }\n\n const copy = { ...resultSearch }\n // add missing keys from search to copy\n for (const key of keys) {\n if (!(key in copy) || (defaultKeys ? key in defaultKeys : false)) {\n copy[key] = search[key]\n }\n }\n return copy\n }\n}\n\nfunction getValidationDefaultKeys(\n search: any,\n resultSearch: any,\n validations: Array<Record<PropertyKey, unknown>>,\n) {\n let defaultKeys: Record<PropertyKey, true> | undefined\n for (let i = 0; i < validations.length; i += 2) {\n const baseSearch = validations[i]!\n const validatedSearch = validations[i + 1]!\n for (const key in validatedSearch) {\n if (\n key in search &&\n !(key in baseSearch) &&\n resultSearch[key] === validatedSearch[key]\n ) {\n const target = defaultKeys || (defaultKeys = createNull())\n target[key] = true\n }\n }\n }\n return defaultKeys\n}\n\n/**\n * Remove optional or default-valued search params from navigations.\n *\n * - Pass `true` (only if there are no required search params) to strip all.\n * - Pass an array to always remove those optional keys.\n * - Pass an object of default values; keys equal (deeply) to the defaults are removed.\n *\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/stripSearchParamsFunction\n */\nexport function stripSearchParams<\n TSearchSchema,\n TOptionalProps = PickOptional<NoInfer<TSearchSchema>>,\n const TValues = Partial<NoInfer<TSearchSchema>> | Array<keyof TOptionalProps>,\n const TInput = IsRequiredParams<TSearchSchema> extends never\n ? TValues | true\n : TValues,\n>(input: NoInfer<TInput>): SearchMiddleware<TSearchSchema> {\n return ({ search, next }) => {\n if (input === true) {\n return {}\n }\n const result = { ...next(search) } as Record<string, unknown>\n if (Array.isArray(input)) {\n input.forEach((key) => {\n delete result[key]\n })\n } else {\n Object.entries(input as Record<string, unknown>).forEach(\n ([key, value]) => {\n if (deepEqual(result[key], value)) {\n delete result[key]\n }\n },\n )\n }\n return result as any\n }\n}\n"],"mappings":";;;;;;;;;;;;AAeA,SAAgB,mBACd,MACiC;CACjC,QAAQ,EAAE,QAAQ,WAAW;EAC3B,MAAM,CAAC,cAAc,eAAgB,KAAa,QAAQ,IAAI;EAI9D,MAAM,cAAc,YAAY,SAC5B,yBAAyB,QAAQ,cAAc,WAAW,IAC1D,KAAA;EAEJ,IAAI,SAAS,MAAM;GACjB,MAAM,OAAO;IAAE,GAAG;IAAQ,GAAG;GAAa;GAC1C,IAAI,aACF,KAAK,MAAM,OAAO,aAChB,KAAK,OAA8B,OAAO;GAG9C,OAAO;EACT;EAEA,MAAM,OAAO,EAAE,GAAG,aAAa;EAE/B,KAAK,MAAM,OAAO,MAChB,IAAI,EAAE,OAAO,UAAU,cAAc,OAAO,cAAc,QACxD,KAAK,OAAO,OAAO;EAGvB,OAAO;CACT;AACF;AAEA,SAAS,yBACP,QACA,cACA,aACA;CACA,IAAI;CACJ,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK,GAAG;EAC9C,MAAM,aAAa,YAAY;EAC/B,MAAM,kBAAkB,YAAY,IAAI;EACxC,KAAK,MAAM,OAAO,iBAChB,IACE,OAAO,UACP,EAAE,OAAO,eACT,aAAa,SAAS,gBAAgB,MACtC;GACA,MAAM,SAAS,gBAAgB,cAAc,WAAW;GACxD,OAAO,OAAO;EAChB;CAEJ;CACA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,kBAOd,OAAyD;CACzD,QAAQ,EAAE,QAAQ,WAAW;EAC3B,IAAI,UAAU,MACZ,OAAO,CAAC;EAEV,MAAM,SAAS,EAAE,GAAG,KAAK,MAAM,EAAE;EACjC,IAAI,MAAM,QAAQ,KAAK,GACrB,MAAM,SAAS,QAAQ;GACrB,OAAO,OAAO;EAChB,CAAC;OAED,OAAO,QAAQ,KAAgC,EAAE,SAC9C,CAAC,KAAK,WAAW;GAChB,IAAI,UAAU,OAAO,MAAM,KAAK,GAC9B,OAAO,OAAO;EAElB,CACF;EAEF,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"searchMiddleware.js","names":[],"sources":["../../src/searchMiddleware.ts"],"sourcesContent":["import { deepEqual } from './utils'\nimport type { NoInfer, PickOptional } from './utils'\nimport type {\n SearchMiddleware,\n SearchMiddlewareContext,\n SearchMiddlewareMeta,\n} from './route'\nimport type { IsRequiredParams } from './link'\n\ntype SearchMiddlewareNextWithMeta<TSearchSchema> = (\n newSearch: TSearchSchema,\n collectMeta: true,\n) => { search: TSearchSchema; meta: SearchMiddlewareMeta }\n\n/**\n * Retain specified search params across navigations.\n *\n * If `keys` is `true`, retain all current params. Otherwise, copy only the\n * listed keys from the current search into the next search.\n *\n * @param keys `true` to retain all, or a list of keys to retain.\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/retainSearchParamsFunction\n */\nexport function retainSearchParams<TSearchSchema extends object>(\n keys: Array<keyof TSearchSchema> | true,\n): SearchMiddleware<TSearchSchema> {\n return ({ search, next }) => {\n const { search: resultSearch, meta } = (\n next as unknown as SearchMiddlewareNextWithMeta<TSearchSchema>\n )(search, true)\n\n if (keys === true) {\n const copy = { ...search, ...resultSearch }\n const removed = meta.removed\n for (const key of removed?.keys() || []) {\n if (deepEqual(search[key as keyof TSearchSchema], removed!.get(key))) {\n delete copy[key as keyof TSearchSchema]\n }\n }\n for (const key of meta.removedAny || []) {\n delete copy[key as keyof TSearchSchema]\n }\n for (const key of meta.defaulted?.keys() || []) {\n if (\n key in search &&\n !meta.removedAny?.has(key) &&\n !(\n meta.removed?.has(key) &&\n deepEqual(search[key as keyof TSearchSchema], meta.removed.get(key))\n )\n ) {\n copy[key as keyof TSearchSchema] = search[key as keyof TSearchSchema]\n }\n }\n return copy\n }\n\n const copy = { ...resultSearch }\n // add missing keys from search to copy\n for (const key of keys) {\n const stringKey = key as string\n const removed =\n meta.removedAny?.has(stringKey) ||\n (meta.removed?.has(stringKey) &&\n deepEqual(search[key], meta.removed.get(stringKey)))\n if (\n !removed &&\n (!(key in copy) ||\n (key in search &&\n meta.defaulted?.has(stringKey) &&\n deepEqual(copy[key], meta.defaulted.get(stringKey))))\n ) {\n copy[key] = search[key]\n }\n }\n return copy\n }\n}\n\n/**\n * Remove optional or default-valued search params from navigations.\n *\n * - Pass `true` (only if there are no required search params) to strip all.\n * - Pass an array to always remove those optional keys.\n * - Pass an object of default values; keys equal (deeply) to the defaults are removed.\n *\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/stripSearchParamsFunction\n */\nexport function stripSearchParams<\n TSearchSchema,\n TOptionalProps = PickOptional<NoInfer<TSearchSchema>>,\n const TValues = Partial<NoInfer<TSearchSchema>> | Array<keyof TOptionalProps>,\n const TInput = IsRequiredParams<TSearchSchema> extends never\n ? TValues | true\n : TValues,\n>(input: NoInfer<TInput>): SearchMiddleware<TSearchSchema> {\n return (({ search, next, meta }: SearchMiddlewareContext<TSearchSchema>) => {\n if (input === true) {\n Object.keys(search as object).forEach((key) => {\n if (meta) {\n ;(meta.removedAny ||= new Set()).add(key)\n }\n })\n return {}\n }\n const nextResult = next(search)\n const result = { ...nextResult } as Record<string, unknown>\n if (Array.isArray(input)) {\n input.forEach((key) => {\n delete result[key as string]\n if (meta) {\n ;(meta.removedAny ||= new Set()).add(key as string)\n }\n })\n } else {\n Object.entries(input as Record<string, unknown>).forEach(\n ([key, value]) => {\n if (deepEqual(result[key], value)) {\n delete result[key]\n if (meta) {\n ;(meta.removed ||= new Map()).set(key, value)\n }\n }\n },\n )\n }\n return result as any\n }) as SearchMiddleware<TSearchSchema>\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,SAAgB,mBACd,MACiC;CACjC,QAAQ,EAAE,QAAQ,WAAW;EAC3B,MAAM,EAAE,QAAQ,cAAc,SAC5B,KACA,QAAQ,IAAI;EAEd,IAAI,SAAS,MAAM;GACjB,MAAM,OAAO;IAAE,GAAG;IAAQ,GAAG;GAAa;GAC1C,MAAM,UAAU,KAAK;GACrB,KAAK,MAAM,OAAO,SAAS,KAAK,KAAK,CAAC,GACpC,IAAI,UAAU,OAAO,MAA6B,QAAS,IAAI,GAAG,CAAC,GACjE,OAAO,KAAK;GAGhB,KAAK,MAAM,OAAO,KAAK,cAAc,CAAC,GACpC,OAAO,KAAK;GAEd,KAAK,MAAM,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,GAC3C,IACE,OAAO,UACP,CAAC,KAAK,YAAY,IAAI,GAAG,KACzB,EACE,KAAK,SAAS,IAAI,GAAG,KACrB,UAAU,OAAO,MAA6B,KAAK,QAAQ,IAAI,GAAG,CAAC,IAGrE,KAAK,OAA8B,OAAO;GAG9C,OAAO;EACT;EAEA,MAAM,OAAO,EAAE,GAAG,aAAa;EAE/B,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,YAAY;GAKlB,IACE,EAJA,KAAK,YAAY,IAAI,SAAS,KAC7B,KAAK,SAAS,IAAI,SAAS,KAC1B,UAAU,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAC,OAGnD,EAAE,OAAO,SACP,OAAO,UACN,KAAK,WAAW,IAAI,SAAS,KAC7B,UAAU,KAAK,MAAM,KAAK,UAAU,IAAI,SAAS,CAAC,IAEtD,KAAK,OAAO,OAAO;EAEvB;EACA,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,kBAOd,OAAyD;CACzD,SAAS,EAAE,QAAQ,MAAM,WAAmD;EAC1E,IAAI,UAAU,MAAM;GAClB,OAAO,KAAK,MAAgB,EAAE,SAAS,QAAQ;IAC7C,IAAI,MACD,CAAC,KAAK,+BAAe,IAAI,IAAI,GAAG,IAAI,GAAG;GAE5C,CAAC;GACD,OAAO,CAAC;EACV;EAEA,MAAM,SAAS,EAAE,GADE,KAAK,MACJ,EAAW;EAC/B,IAAI,MAAM,QAAQ,KAAK,GACrB,MAAM,SAAS,QAAQ;GACrB,OAAO,OAAO;GACd,IAAI,MACD,CAAC,KAAK,+BAAe,IAAI,IAAI,GAAG,IAAI,GAAa;EAEtD,CAAC;OAED,OAAO,QAAQ,KAAgC,EAAE,SAC9C,CAAC,KAAK,WAAW;GAChB,IAAI,UAAU,OAAO,MAAM,KAAK,GAAG;IACjC,OAAO,OAAO;IACd,IAAI,MACD,CAAC,KAAK,4BAAY,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK;GAEhD;EACF,CACF;EAEF,OAAO;CACT;AACF"}
package/dist/esm/utils.js CHANGED
@@ -321,6 +321,6 @@ function arraysEqual(a, b) {
321
321
  return true;
322
322
  }
323
323
  //#endregion
324
- export { DEFAULT_PROTOCOL_ALLOWLIST, arraysEqual, buildDevStylesUrl, createControlledPromise, createNull, decodePath, deepEqual, encodePathLikeUrl, escapeHtml, findLast, functionalUpdate, hasKeys, isDangerousProtocol, isModuleNotFoundError, isPlainArray, isPlainObject, isPromise, last, nullReplaceEqualDeep, replaceEqualDeep };
324
+ export { DEFAULT_PROTOCOL_ALLOWLIST, arraysEqual, buildDevStylesUrl, createControlledPromise, decodePath, deepEqual, encodePathLikeUrl, escapeHtml, findLast, functionalUpdate, hasKeys, isDangerousProtocol, isModuleNotFoundError, isPlainArray, isPlainObject, isPromise, last, nullReplaceEqualDeep, replaceEqualDeep };
325
325
 
326
326
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.171.10",
3
+ "version": "1.171.11",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
package/src/route.ts CHANGED
@@ -74,9 +74,16 @@ export type RoutePathOptionsIntersection<TCustomId, TPath> = {
74
74
 
75
75
  export type SearchFilter<TInput, TResult = TInput> = (prev: TInput) => TResult
76
76
 
77
+ export type SearchMiddlewareMeta = {
78
+ removed?: Map<string, unknown>
79
+ removedAny?: Set<string>
80
+ defaulted?: Map<string, unknown>
81
+ }
82
+
77
83
  export type SearchMiddlewareContext<TSearchSchema> = {
78
84
  search: TSearchSchema
79
85
  next: (newSearch: TSearchSchema) => TSearchSchema
86
+ meta?: SearchMiddlewareMeta
80
87
  }
81
88
 
82
89
  export type SearchMiddleware<TSearchSchema> = (
package/src/router.ts CHANGED
@@ -79,6 +79,7 @@ import type {
79
79
  RouteLike,
80
80
  RouteMask,
81
81
  SearchMiddleware,
82
+ SearchMiddlewareMeta,
82
83
  } from './route'
83
84
  import type {
84
85
  FullSearchSchema,
@@ -3123,8 +3124,6 @@ function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3123
3124
  let dest: BuildNextOptions
3124
3125
  let includeValidateSearch: boolean | undefined
3125
3126
  const middlewares = [] as Array<SearchMiddleware<any>>
3126
- // Flat pairs: [searchBeforeValidation, validatedSearch, ...]
3127
- type ValidatedSearches = Array<Record<PropertyKey, unknown>>
3128
3127
 
3129
3128
  for (const route of destRoutes) {
3130
3129
  const routeOptions = route.options
@@ -3157,17 +3156,18 @@ function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3157
3156
 
3158
3157
  const routeValidateSearch = routeOptions.validateSearch
3159
3158
  if (routeValidateSearch) {
3160
- const validate: SearchMiddleware<any> = (
3161
- { search, next },
3162
- validations?: ValidatedSearches,
3163
- ) => {
3159
+ const validate: SearchMiddleware<any> = ({ search, next, meta }) => {
3164
3160
  const result = next(search)
3165
3161
  if (includeValidateSearch) {
3166
3162
  try {
3167
3163
  const validated = validateSearch(routeValidateSearch, result) as any
3168
3164
 
3169
- if (validations && validated) {
3170
- validations.push(result, validated)
3165
+ if (meta && validated) {
3166
+ for (const key in validated) {
3167
+ if (!(key in result)) {
3168
+ ;(meta.defaulted ||= new Map()).set(key, validated[key])
3169
+ }
3170
+ }
3171
3171
  }
3172
3172
  return { ...result, ...validated }
3173
3173
  } catch {
@@ -3184,7 +3184,7 @@ function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3184
3184
  const applyNext = (
3185
3185
  index: number,
3186
3186
  currentSearch: any,
3187
- validations?: ValidatedSearches,
3187
+ meta?: SearchMiddlewareMeta,
3188
3188
  ): any => {
3189
3189
  // no more middlewares left, return the current search
3190
3190
  if (index >= middlewares.length) {
@@ -3197,21 +3197,18 @@ function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3197
3197
  return functionalUpdate(dest.search, currentSearch)
3198
3198
  }
3199
3199
 
3200
- const next = (newSearch: any, collectValidations?: true): any => {
3201
- if (collectValidations) {
3202
- const nextValidations: ValidatedSearches = []
3203
- return [
3204
- applyNext(index + 1, newSearch, nextValidations),
3205
- nextValidations,
3206
- ]
3200
+ const next = (newSearch: any, collectMeta?: true): any => {
3201
+ if (collectMeta) {
3202
+ const nextMeta = meta || ({} as SearchMiddlewareMeta)
3203
+ return {
3204
+ search: applyNext(index + 1, newSearch, nextMeta),
3205
+ meta: nextMeta,
3206
+ }
3207
3207
  }
3208
- return applyNext(index + 1, newSearch, validations)
3208
+ return applyNext(index + 1, newSearch, meta)
3209
3209
  }
3210
3210
 
3211
- return (middlewares[index]! as any)(
3212
- { search: currentSearch, next },
3213
- validations,
3214
- )
3211
+ return (middlewares[index]! as any)({ search: currentSearch, next, meta })
3215
3212
  }
3216
3213
 
3217
3214
  return function middleware(
@@ -1,8 +1,17 @@
1
- import { createNull, deepEqual } from './utils'
1
+ import { deepEqual } from './utils'
2
2
  import type { NoInfer, PickOptional } from './utils'
3
- import type { SearchMiddleware } from './route'
3
+ import type {
4
+ SearchMiddleware,
5
+ SearchMiddlewareContext,
6
+ SearchMiddlewareMeta,
7
+ } from './route'
4
8
  import type { IsRequiredParams } from './link'
5
9
 
10
+ type SearchMiddlewareNextWithMeta<TSearchSchema> = (
11
+ newSearch: TSearchSchema,
12
+ collectMeta: true,
13
+ ) => { search: TSearchSchema; meta: SearchMiddlewareMeta }
14
+
6
15
  /**
7
16
  * Retain specified search params across navigations.
8
17
  *
@@ -17,18 +26,30 @@ export function retainSearchParams<TSearchSchema extends object>(
17
26
  keys: Array<keyof TSearchSchema> | true,
18
27
  ): SearchMiddleware<TSearchSchema> {
19
28
  return ({ search, next }) => {
20
- const [resultSearch, validations] = (next as any)(search, true) as [
21
- TSearchSchema,
22
- Array<Record<PropertyKey, unknown>>,
23
- ]
24
- const defaultKeys = validations.length
25
- ? getValidationDefaultKeys(search, resultSearch, validations)
26
- : undefined
29
+ const { search: resultSearch, meta } = (
30
+ next as unknown as SearchMiddlewareNextWithMeta<TSearchSchema>
31
+ )(search, true)
27
32
 
28
33
  if (keys === true) {
29
34
  const copy = { ...search, ...resultSearch }
30
- if (defaultKeys) {
31
- for (const key in defaultKeys) {
35
+ const removed = meta.removed
36
+ for (const key of removed?.keys() || []) {
37
+ if (deepEqual(search[key as keyof TSearchSchema], removed!.get(key))) {
38
+ delete copy[key as keyof TSearchSchema]
39
+ }
40
+ }
41
+ for (const key of meta.removedAny || []) {
42
+ delete copy[key as keyof TSearchSchema]
43
+ }
44
+ for (const key of meta.defaulted?.keys() || []) {
45
+ if (
46
+ key in search &&
47
+ !meta.removedAny?.has(key) &&
48
+ !(
49
+ meta.removed?.has(key) &&
50
+ deepEqual(search[key as keyof TSearchSchema], meta.removed.get(key))
51
+ )
52
+ ) {
32
53
  copy[key as keyof TSearchSchema] = search[key as keyof TSearchSchema]
33
54
  }
34
55
  }
@@ -38,35 +59,23 @@ export function retainSearchParams<TSearchSchema extends object>(
38
59
  const copy = { ...resultSearch }
39
60
  // add missing keys from search to copy
40
61
  for (const key of keys) {
41
- if (!(key in copy) || (defaultKeys ? key in defaultKeys : false)) {
42
- copy[key] = search[key]
43
- }
44
- }
45
- return copy
46
- }
47
- }
48
-
49
- function getValidationDefaultKeys(
50
- search: any,
51
- resultSearch: any,
52
- validations: Array<Record<PropertyKey, unknown>>,
53
- ) {
54
- let defaultKeys: Record<PropertyKey, true> | undefined
55
- for (let i = 0; i < validations.length; i += 2) {
56
- const baseSearch = validations[i]!
57
- const validatedSearch = validations[i + 1]!
58
- for (const key in validatedSearch) {
62
+ const stringKey = key as string
63
+ const removed =
64
+ meta.removedAny?.has(stringKey) ||
65
+ (meta.removed?.has(stringKey) &&
66
+ deepEqual(search[key], meta.removed.get(stringKey)))
59
67
  if (
60
- key in search &&
61
- !(key in baseSearch) &&
62
- resultSearch[key] === validatedSearch[key]
68
+ !removed &&
69
+ (!(key in copy) ||
70
+ (key in search &&
71
+ meta.defaulted?.has(stringKey) &&
72
+ deepEqual(copy[key], meta.defaulted.get(stringKey))))
63
73
  ) {
64
- const target = defaultKeys || (defaultKeys = createNull())
65
- target[key] = true
74
+ copy[key] = search[key]
66
75
  }
67
76
  }
77
+ return copy
68
78
  }
69
- return defaultKeys
70
79
  }
71
80
 
72
81
  /**
@@ -87,24 +96,36 @@ export function stripSearchParams<
87
96
  ? TValues | true
88
97
  : TValues,
89
98
  >(input: NoInfer<TInput>): SearchMiddleware<TSearchSchema> {
90
- return ({ search, next }) => {
99
+ return (({ search, next, meta }: SearchMiddlewareContext<TSearchSchema>) => {
91
100
  if (input === true) {
101
+ Object.keys(search as object).forEach((key) => {
102
+ if (meta) {
103
+ ;(meta.removedAny ||= new Set()).add(key)
104
+ }
105
+ })
92
106
  return {}
93
107
  }
94
- const result = { ...next(search) } as Record<string, unknown>
108
+ const nextResult = next(search)
109
+ const result = { ...nextResult } as Record<string, unknown>
95
110
  if (Array.isArray(input)) {
96
111
  input.forEach((key) => {
97
- delete result[key]
112
+ delete result[key as string]
113
+ if (meta) {
114
+ ;(meta.removedAny ||= new Set()).add(key as string)
115
+ }
98
116
  })
99
117
  } else {
100
118
  Object.entries(input as Record<string, unknown>).forEach(
101
119
  ([key, value]) => {
102
120
  if (deepEqual(result[key], value)) {
103
121
  delete result[key]
122
+ if (meta) {
123
+ ;(meta.removed ||= new Map()).set(key, value)
124
+ }
104
125
  }
105
126
  },
106
127
  )
107
128
  }
108
129
  return result as any
109
- }
130
+ }) as SearchMiddleware<TSearchSchema>
110
131
  }