@tanstack/router-core 1.168.15 → 1.168.17

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.
@@ -1023,6 +1023,7 @@ type MatchStackFrame<T extends RouteLike> = {
1023
1023
  * If we really really need to support more than 32 segments we can switch to using a `BigInt` here. It's about 2x slower in worst case scenarios.
1024
1024
  */
1025
1025
  skipped: number
1026
+ /** Positional bitmasks tracking which consumed URL segments matched each segment kind. */
1026
1027
  statics: number
1027
1028
  dynamics: number
1028
1029
  optionals: number
@@ -1066,13 +1067,12 @@ function getNodeMatch<T extends RouteLike>(
1066
1067
  index: 1,
1067
1068
  skipped: 0,
1068
1069
  depth: 1,
1069
- statics: 1,
1070
+ statics: 0,
1070
1071
  dynamics: 0,
1071
1072
  optionals: 0,
1072
1073
  },
1073
1074
  ]
1074
1075
 
1075
- let wildcardMatch: Frame | null = null
1076
1076
  let bestFuzzy: Frame | null = null
1077
1077
  let bestMatch: Frame | null = null
1078
1078
 
@@ -1081,6 +1081,18 @@ function getNodeMatch<T extends RouteLike>(
1081
1081
  const { node, index, skipped, depth, statics, dynamics, optionals } = frame
1082
1082
  let { extract, rawParams, parsedParams } = frame
1083
1083
 
1084
+ // Wildcard candidates are pushed speculatively as fallbacks in case a
1085
+ // higher-priority wildcard later fails params.parse. If a better wildcard
1086
+ // has already validated and become bestMatch, lower-priority wildcard
1087
+ // fallbacks cannot win anymore and should not run params.parse.
1088
+ if (
1089
+ node.kind === SEGMENT_TYPE_WILDCARD &&
1090
+ node.route &&
1091
+ !isFrameMoreSpecific(bestMatch, frame)
1092
+ ) {
1093
+ continue
1094
+ }
1095
+
1084
1096
  if (node.skipOnParamError) {
1085
1097
  const result = validateMatchParams(path, parts, frame)
1086
1098
  if (!result) continue
@@ -1101,7 +1113,13 @@ function getNodeMatch<T extends RouteLike>(
1101
1113
 
1102
1114
  const isBeyondPath = index === partsLength
1103
1115
  if (isBeyondPath) {
1104
- if (node.route && !pathIsIndex && isFrameMoreSpecific(bestMatch, frame)) {
1116
+ if (
1117
+ node.route &&
1118
+ (!pathIsIndex ||
1119
+ node.kind === SEGMENT_TYPE_INDEX ||
1120
+ node.kind === SEGMENT_TYPE_WILDCARD) &&
1121
+ isFrameMoreSpecific(bestMatch, frame)
1122
+ ) {
1105
1123
  bestMatch = frame
1106
1124
  }
1107
1125
  // beyond the length of the path parts, only some segment types can match
@@ -1134,7 +1152,12 @@ function getNodeMatch<T extends RouteLike>(
1134
1152
  if (indexValid) {
1135
1153
  // perfect match, no need to continue
1136
1154
  // this is an optimization, algorithm should work correctly without this block
1137
- if (statics === partsLength && !dynamics && !optionals && !skipped) {
1155
+ if (
1156
+ !dynamics &&
1157
+ !optionals &&
1158
+ !skipped &&
1159
+ isPerfectStaticMatch(statics, partsLength)
1160
+ ) {
1138
1161
  return indexFrame
1139
1162
  }
1140
1163
  if (isFrameMoreSpecific(bestMatch, indexFrame)) {
@@ -1145,8 +1168,9 @@ function getNodeMatch<T extends RouteLike>(
1145
1168
  }
1146
1169
 
1147
1170
  // 5. Try wildcard match
1148
- if (node.wildcard && isFrameMoreSpecific(wildcardMatch, frame)) {
1149
- for (const segment of node.wildcard) {
1171
+ if (node.wildcard) {
1172
+ for (let i = node.wildcard.length - 1; i >= 0; i--) {
1173
+ const segment = node.wildcard[i]!
1150
1174
  const { prefix, suffix } = segment
1151
1175
  if (prefix) {
1152
1176
  if (isBeyondPath) continue
@@ -1161,26 +1185,19 @@ function getNodeMatch<T extends RouteLike>(
1161
1185
  const casePart = segment.caseSensitive ? end : end.toLowerCase()
1162
1186
  if (casePart !== suffix) continue
1163
1187
  }
1164
- // the first wildcard match is the highest priority one
1165
- // wildcard matches skip the stack because they cannot have children
1166
- const frame = {
1188
+ // wildcard matches consume the rest of the URL and cannot have children
1189
+ stack.push({
1167
1190
  node: segment,
1168
1191
  index: partsLength,
1169
1192
  skipped,
1170
- depth,
1193
+ depth: depth + 1,
1171
1194
  statics,
1172
1195
  dynamics,
1173
1196
  optionals,
1174
1197
  extract,
1175
1198
  rawParams,
1176
1199
  parsedParams,
1177
- }
1178
- if (segment.skipOnParamError) {
1179
- const result = validateMatchParams(path, parts, frame)
1180
- if (!result) continue
1181
- }
1182
- wildcardMatch = frame
1183
- break
1200
+ })
1184
1201
  }
1185
1202
  }
1186
1203
 
@@ -1222,7 +1239,7 @@ function getNodeMatch<T extends RouteLike>(
1222
1239
  depth: nextDepth,
1223
1240
  statics,
1224
1241
  dynamics,
1225
- optionals: optionals + 1,
1242
+ optionals: optionals + segmentScore(partsLength, index),
1226
1243
  extract,
1227
1244
  rawParams,
1228
1245
  parsedParams,
@@ -1249,7 +1266,7 @@ function getNodeMatch<T extends RouteLike>(
1249
1266
  skipped,
1250
1267
  depth: depth + 1,
1251
1268
  statics,
1252
- dynamics: dynamics + 1,
1269
+ dynamics: dynamics + segmentScore(partsLength, index),
1253
1270
  optionals,
1254
1271
  extract,
1255
1272
  rawParams,
@@ -1269,7 +1286,7 @@ function getNodeMatch<T extends RouteLike>(
1269
1286
  index: index + 1,
1270
1287
  skipped,
1271
1288
  depth: depth + 1,
1272
- statics: statics + 1,
1289
+ statics: statics + segmentScore(partsLength, index),
1273
1290
  dynamics,
1274
1291
  optionals,
1275
1292
  extract,
@@ -1288,7 +1305,7 @@ function getNodeMatch<T extends RouteLike>(
1288
1305
  index: index + 1,
1289
1306
  skipped,
1290
1307
  depth: depth + 1,
1291
- statics: statics + 1,
1308
+ statics: statics + segmentScore(partsLength, index),
1292
1309
  dynamics,
1293
1310
  optionals,
1294
1311
  extract,
@@ -1319,16 +1336,8 @@ function getNodeMatch<T extends RouteLike>(
1319
1336
  }
1320
1337
  }
1321
1338
 
1322
- if (bestMatch && wildcardMatch) {
1323
- return isFrameMoreSpecific(wildcardMatch, bestMatch)
1324
- ? bestMatch
1325
- : wildcardMatch
1326
- }
1327
-
1328
1339
  if (bestMatch) return bestMatch
1329
1340
 
1330
- if (wildcardMatch) return wildcardMatch
1331
-
1332
1341
  if (fuzzy && bestFuzzy) {
1333
1342
  let sliceIndex = bestFuzzy.index
1334
1343
  for (let i = 0; i < bestFuzzy.index; i++) {
@@ -1343,6 +1352,19 @@ function getNodeMatch<T extends RouteLike>(
1343
1352
  return null
1344
1353
  }
1345
1354
 
1355
+ function segmentScore(partsLength: number, index: number): number {
1356
+ // The specificity scores are bitmasks over consumed URL segments. Earlier
1357
+ // URL segments should dominate later ones when comparing scores, so the
1358
+ // first real segment gets the highest bit and the last gets bit 0. Since
1359
+ // `parts[0]` is the empty string before the leading slash, real URL segments
1360
+ // are [1, partsLength), making this segment's bit `partsLength - index - 1`.
1361
+ return 2 ** (partsLength - index - 1)
1362
+ }
1363
+
1364
+ function isPerfectStaticMatch(statics: number, partsLength: number): boolean {
1365
+ return statics === 2 ** (partsLength - 1) - 1
1366
+ }
1367
+
1346
1368
  function validateMatchParams<T extends RouteLike>(
1347
1369
  path: string,
1348
1370
  parts: Array<string>,
@@ -1,5 +1,11 @@
1
1
  import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
2
2
  import { invariant } from '../invariant'
3
+ import {
4
+ createInlineCssPlaceholderAsset,
5
+ createInlineCssStyleAsset,
6
+ getStylesheetHref,
7
+ isInlinableStylesheet,
8
+ } from '../manifest'
3
9
  import { decodePath } from '../utils'
4
10
  import { createLRUCache } from '../lru-cache'
5
11
  import { rootRouteId } from '../root'
@@ -157,9 +163,11 @@ const isProd = process.env.NODE_ENV === 'production'
157
163
  type FilteredRoutes = Manifest['routes']
158
164
 
159
165
  type ManifestLRU = LRUCache<string, FilteredRoutes>
166
+ type InlineCssLRU = LRUCache<string, string>
160
167
 
161
168
  const MANIFEST_CACHE_SIZE = 100
162
169
  const manifestCaches = new WeakMap<Manifest, ManifestLRU>()
170
+ const inlineCssCaches = new WeakMap<Manifest, InlineCssLRU>()
163
171
 
164
172
  function getManifestCache(manifest: Manifest): ManifestLRU {
165
173
  const cache = manifestCaches.get(manifest)
@@ -169,6 +177,108 @@ function getManifestCache(manifest: Manifest): ManifestLRU {
169
177
  return newCache
170
178
  }
171
179
 
180
+ function getInlineCssCache(manifest: Manifest): InlineCssLRU {
181
+ const cache = inlineCssCaches.get(manifest)
182
+ if (cache) return cache
183
+ const newCache = createLRUCache<string, string>(MANIFEST_CACHE_SIZE)
184
+ inlineCssCaches.set(manifest, newCache)
185
+ return newCache
186
+ }
187
+
188
+ function getInlineCssHrefsForMatches(
189
+ manifest: Manifest | undefined,
190
+ matches: Array<AnyRouteMatch>,
191
+ ) {
192
+ const styles = manifest?.inlineCss?.styles
193
+ if (!styles) return []
194
+
195
+ const seen = new Set<string>()
196
+ const hrefs: Array<string> = []
197
+
198
+ for (const match of matches) {
199
+ const assets = manifest?.routes[match.routeId]?.assets ?? []
200
+ for (const asset of assets) {
201
+ const href = getStylesheetHref(asset)
202
+ if (!href || seen.has(href) || styles[href] === undefined) {
203
+ continue
204
+ }
205
+ seen.add(href)
206
+ hrefs.push(href)
207
+ }
208
+ }
209
+
210
+ return hrefs
211
+ }
212
+
213
+ function getInlineCssForHrefs(manifest: Manifest, hrefs: Array<string>) {
214
+ const styles = manifest.inlineCss?.styles
215
+ if (!styles || hrefs.length === 0) return undefined
216
+
217
+ const cacheKey = hrefs.join('\0')
218
+ if (isProd) {
219
+ const cached = getInlineCssCache(manifest).get(cacheKey)
220
+ if (cached !== undefined) return cached
221
+ }
222
+
223
+ const css = hrefs.map((href) => styles[href]!).join('')
224
+
225
+ if (isProd) {
226
+ getInlineCssCache(manifest).set(cacheKey, css)
227
+ }
228
+
229
+ return css
230
+ }
231
+
232
+ function getInlineCssAssetForMatches(
233
+ manifest: Manifest | undefined,
234
+ matches: Array<AnyRouteMatch>,
235
+ ) {
236
+ if (!manifest?.inlineCss) return undefined
237
+
238
+ const hrefs = getInlineCssHrefsForMatches(manifest, matches)
239
+ const css = getInlineCssForHrefs(manifest, hrefs)
240
+
241
+ return css === undefined ? undefined : createInlineCssStyleAsset(css)
242
+ }
243
+
244
+ function stripInlinedStylesheetAssets(
245
+ manifest: Manifest,
246
+ routes: FilteredRoutes,
247
+ matches: Array<AnyRouteMatch>,
248
+ ): FilteredRoutes {
249
+ if (!manifest.inlineCss) {
250
+ return routes
251
+ }
252
+
253
+ const nextRoutes: FilteredRoutes = {}
254
+
255
+ for (const [routeId, route] of Object.entries(routes)) {
256
+ const assets = route.assets?.filter(
257
+ (asset) => !isInlinableStylesheet(manifest, asset),
258
+ )
259
+
260
+ const nextRoute = { ...route }
261
+ if (assets) {
262
+ if (assets.length > 0) {
263
+ nextRoute.assets = assets
264
+ } else {
265
+ delete nextRoute.assets
266
+ }
267
+ }
268
+ nextRoutes[routeId] = nextRoute
269
+ }
270
+
271
+ if (getInlineCssAssetForMatches(manifest, matches)) {
272
+ const rootRoute = nextRoutes[rootRouteId] ?? {}
273
+ nextRoutes[rootRouteId] = {
274
+ ...rootRoute,
275
+ assets: [createInlineCssPlaceholderAsset(), ...(rootRoute.assets ?? [])],
276
+ }
277
+ }
278
+
279
+ return nextRoutes
280
+ }
281
+
172
282
  export function attachRouterServerSsrUtils({
173
283
  router,
174
284
  manifest,
@@ -183,7 +293,11 @@ export function attachRouterServerSsrUtils({
183
293
  router.ssr = {
184
294
  get manifest() {
185
295
  const requestAssets = getRequestAssets?.()
186
- if (!requestAssets?.length) return manifest
296
+ const inlineCssAsset = getInlineCssAssetForMatches(
297
+ manifest,
298
+ router.stores.matches.get(),
299
+ )
300
+ if (!requestAssets?.length && !inlineCssAsset) return manifest
187
301
  // Merge request-scoped assets into root route without mutating cached manifest
188
302
  return {
189
303
  ...manifest,
@@ -192,7 +306,8 @@ export function attachRouterServerSsrUtils({
192
306
  [rootRouteId]: {
193
307
  ...manifest?.routes?.[rootRouteId],
194
308
  assets: [
195
- ...requestAssets,
309
+ ...(requestAssets ?? []),
310
+ ...(inlineCssAsset ? [inlineCssAsset] : []),
196
311
  ...(manifest?.routes?.[rootRouteId]?.assets ?? []),
197
312
  ],
198
313
  },
@@ -272,15 +387,19 @@ export function attachRouterServerSsrUtils({
272
387
  }
273
388
  }
274
389
 
390
+ filteredRoutes = stripInlinedStylesheetAssets(
391
+ manifest,
392
+ nextFilteredRoutes,
393
+ matchesToDehydrate,
394
+ )
395
+
275
396
  if (isProd) {
276
- getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)
397
+ getManifestCache(manifest).set(manifestCacheKey, filteredRoutes)
277
398
  }
278
-
279
- filteredRoutes = nextFilteredRoutes
280
399
  }
281
400
 
282
401
  manifestToDehydrate = {
283
- routes: filteredRoutes,
402
+ routes: { ...filteredRoutes },
284
403
  }
285
404
 
286
405
  // Merge request-scoped assets into root route (without mutating cached manifest)