@tanstack/start-plugin-core 1.166.7 → 1.166.8

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.
@@ -0,0 +1,555 @@
1
+ /* eslint-disable @typescript-eslint/prefer-for-of */
2
+ import { joinURL } from 'ufo'
3
+ import { rootRouteId } from '@tanstack/router-core'
4
+ import { tsrSplit } from '@tanstack/router-plugin'
5
+ import type { RouterManagedTag } from '@tanstack/router-core'
6
+ import type { Rollup } from 'vite'
7
+
8
+ const ROUTER_MANAGED_MODE = 1
9
+ const NON_ROUTE_DYNAMIC_MODE = 2
10
+ const VISITING_CHUNK = 1
11
+
12
+ type RouteTreeRoute = {
13
+ filePath?: string
14
+ preloads?: Array<string>
15
+ assets?: Array<RouterManagedTag>
16
+ children?: Array<string>
17
+ }
18
+
19
+ type RouteTreeRoutes = Record<string, RouteTreeRoute>
20
+
21
+ interface ScannedClientChunks {
22
+ entryChunk: Rollup.OutputChunk
23
+ chunksByFileName: Map<string, Rollup.OutputChunk>
24
+ routeChunksByFilePath: Map<string, Array<Rollup.OutputChunk>>
25
+ routeEntryChunks: Set<Rollup.OutputChunk>
26
+ }
27
+
28
+ interface ManifestAssetResolvers {
29
+ getAssetPath: (fileName: string) => string
30
+ getChunkPreloads: (chunk: Rollup.OutputChunk) => Array<string>
31
+ getStylesheetAsset: (cssFile: string) => RouterManagedTag
32
+ }
33
+
34
+ export function appendUniqueStrings(
35
+ target: Array<string> | undefined,
36
+ source: Array<string>,
37
+ ) {
38
+ // Similar to Set.prototype.union, but for ordered arrays.
39
+ // It preserves first-seen order and returns the original target array when
40
+ // source contributes no new values, which avoids extra allocations.
41
+ if (source.length === 0) {
42
+ return target
43
+ }
44
+
45
+ if (!target || target.length === 0) {
46
+ return source
47
+ }
48
+
49
+ const seen = new Set(target)
50
+ let result: Array<string> | undefined
51
+
52
+ for (const value of source) {
53
+ if (seen.has(value)) {
54
+ continue
55
+ }
56
+
57
+ seen.add(value)
58
+ if (!result) {
59
+ result = target.slice()
60
+ }
61
+ result.push(value)
62
+ }
63
+
64
+ return result ?? target
65
+ }
66
+
67
+ export function appendUniqueAssets(
68
+ target: Array<RouterManagedTag> | undefined,
69
+ source: Array<RouterManagedTag>,
70
+ ) {
71
+ // Same semantics as appendUniqueStrings, but uniqueness is based on the
72
+ // serialized asset identity instead of object reference.
73
+ if (source.length === 0) {
74
+ return target
75
+ }
76
+
77
+ if (!target || target.length === 0) {
78
+ return source
79
+ }
80
+
81
+ const seen = new Set(target.map(getAssetIdentity))
82
+ let result: Array<RouterManagedTag> | undefined
83
+
84
+ for (const asset of source) {
85
+ const identity = getAssetIdentity(asset)
86
+ if (seen.has(identity)) {
87
+ continue
88
+ }
89
+
90
+ seen.add(identity)
91
+ if (!result) {
92
+ result = target.slice()
93
+ }
94
+ result.push(asset)
95
+ }
96
+
97
+ return result ?? target
98
+ }
99
+
100
+ function getAssetIdentity(asset: RouterManagedTag) {
101
+ if (asset.tag === 'link' || asset.tag === 'script') {
102
+ const attrs = asset.attrs ?? {}
103
+ return [
104
+ asset.tag,
105
+ 'href' in attrs ? String(attrs.href) : '',
106
+ 'src' in attrs ? String(attrs.src) : '',
107
+ 'rel' in attrs ? String(attrs.rel) : '',
108
+ 'type' in attrs ? String(attrs.type) : '',
109
+ asset.children ?? '',
110
+ ].join('|')
111
+ }
112
+
113
+ return JSON.stringify(asset)
114
+ }
115
+
116
+ function mergeRouteChunkData(options: {
117
+ route: RouteTreeRoute
118
+ chunk: Rollup.OutputChunk
119
+ getChunkCssAssets: (chunk: Rollup.OutputChunk) => Array<RouterManagedTag>
120
+ getChunkPreloads: (chunk: Rollup.OutputChunk) => Array<string>
121
+ }) {
122
+ const chunkAssets = options.getChunkCssAssets(options.chunk)
123
+ const chunkPreloads = options.getChunkPreloads(options.chunk)
124
+
125
+ options.route.assets = appendUniqueAssets(options.route.assets, chunkAssets)
126
+ options.route.preloads = appendUniqueStrings(
127
+ options.route.preloads,
128
+ chunkPreloads,
129
+ )
130
+ }
131
+
132
+ export function buildStartManifest(options: {
133
+ clientBundle: Rollup.OutputBundle
134
+ routeTreeRoutes: RouteTreeRoutes
135
+ basePath: string
136
+ }) {
137
+ const scannedChunks = scanClientChunks(options.clientBundle)
138
+ const hashedCssFiles = collectDynamicImportCss(
139
+ scannedChunks.routeEntryChunks,
140
+ scannedChunks.chunksByFileName,
141
+ scannedChunks.entryChunk,
142
+ )
143
+ const assetResolvers = createManifestAssetResolvers({
144
+ basePath: options.basePath,
145
+ hashedCssFiles,
146
+ })
147
+
148
+ const routes = buildRouteManifestRoutes({
149
+ routeTreeRoutes: options.routeTreeRoutes,
150
+ routeChunksByFilePath: scannedChunks.routeChunksByFilePath,
151
+ chunksByFileName: scannedChunks.chunksByFileName,
152
+ entryChunk: scannedChunks.entryChunk,
153
+ assetResolvers,
154
+ })
155
+
156
+ dedupeNestedRoutePreloads(routes[rootRouteId]!, routes)
157
+
158
+ // Prune routes with no assets or preloads from the manifest
159
+ for (const routeId of Object.keys(routes)) {
160
+ const route = routes[routeId]!
161
+ const hasAssets = route.assets && route.assets.length > 0
162
+ const hasPreloads = route.preloads && route.preloads.length > 0
163
+ if (!hasAssets && !hasPreloads) {
164
+ delete routes[routeId]
165
+ }
166
+ }
167
+
168
+ return {
169
+ routes,
170
+ clientEntry: assetResolvers.getAssetPath(scannedChunks.entryChunk.fileName),
171
+ }
172
+ }
173
+
174
+ export function scanClientChunks(
175
+ clientBundle: Rollup.OutputBundle,
176
+ ): ScannedClientChunks {
177
+ let entryChunk: Rollup.OutputChunk | undefined
178
+ const chunksByFileName = new Map<string, Rollup.OutputChunk>()
179
+ const routeChunksByFilePath = new Map<string, Array<Rollup.OutputChunk>>()
180
+ const routeEntryChunks = new Set<Rollup.OutputChunk>()
181
+
182
+ for (const fileName in clientBundle) {
183
+ const bundleEntry = clientBundle[fileName]!
184
+ if (bundleEntry.type !== 'chunk') {
185
+ continue
186
+ }
187
+
188
+ chunksByFileName.set(bundleEntry.fileName, bundleEntry)
189
+
190
+ if (bundleEntry.isEntry) {
191
+ if (entryChunk) {
192
+ throw new Error(
193
+ `multiple entries detected: ${entryChunk.fileName} ${bundleEntry.fileName}`,
194
+ )
195
+ }
196
+ entryChunk = bundleEntry
197
+ }
198
+
199
+ const routeFilePaths = getRouteFilePathsFromModuleIds(bundleEntry.moduleIds)
200
+ if (routeFilePaths.length === 0) {
201
+ continue
202
+ }
203
+
204
+ routeEntryChunks.add(bundleEntry)
205
+
206
+ for (let i = 0; i < routeFilePaths.length; i++) {
207
+ const routeFilePath = routeFilePaths[i]!
208
+ let chunks = routeChunksByFilePath.get(routeFilePath)
209
+ if (chunks === undefined) {
210
+ chunks = []
211
+ routeChunksByFilePath.set(routeFilePath, chunks)
212
+ }
213
+ chunks.push(bundleEntry)
214
+ }
215
+ }
216
+
217
+ if (!entryChunk) {
218
+ throw new Error('No entry file found')
219
+ }
220
+
221
+ return {
222
+ entryChunk,
223
+ chunksByFileName,
224
+ routeChunksByFilePath,
225
+ routeEntryChunks,
226
+ }
227
+ }
228
+
229
+ export function getRouteFilePathsFromModuleIds(moduleIds: Array<string>) {
230
+ let routeFilePaths: Array<string> | undefined
231
+ let seenRouteFilePaths: Set<string> | undefined
232
+
233
+ for (const moduleId of moduleIds) {
234
+ const queryIndex = moduleId.indexOf('?')
235
+
236
+ if (queryIndex < 0) {
237
+ continue
238
+ }
239
+
240
+ const query = moduleId.slice(queryIndex + 1)
241
+
242
+ // Fast check before allocating URLSearchParams
243
+ if (!query.includes(tsrSplit)) {
244
+ continue
245
+ }
246
+
247
+ if (!new URLSearchParams(query).has(tsrSplit)) {
248
+ continue
249
+ }
250
+
251
+ const routeFilePath = moduleId.slice(0, queryIndex)
252
+
253
+ if (seenRouteFilePaths?.has(routeFilePath)) {
254
+ continue
255
+ }
256
+
257
+ if (routeFilePaths === undefined) {
258
+ routeFilePaths = []
259
+ seenRouteFilePaths = new Set<string>()
260
+ }
261
+
262
+ routeFilePaths.push(routeFilePath)
263
+ seenRouteFilePaths!.add(routeFilePath)
264
+ }
265
+
266
+ return routeFilePaths ?? []
267
+ }
268
+
269
+ export function collectDynamicImportCss(
270
+ routeEntryChunks: Set<Rollup.OutputChunk>,
271
+ chunksByFileName: Map<string, Rollup.OutputChunk>,
272
+ entryChunk?: Rollup.OutputChunk,
273
+ ) {
274
+ const routerManagedCssFiles = new Set<string>()
275
+ const nonRouteDynamicCssFiles = new Set<string>()
276
+ const hashedCssFiles = new Set<string>()
277
+ const visitedByChunk = new Map<Rollup.OutputChunk, number>()
278
+ const chunkStack: Array<Rollup.OutputChunk> = []
279
+ const modeStack: Array<number> = []
280
+
281
+ for (const routeEntryChunk of routeEntryChunks) {
282
+ chunkStack.push(routeEntryChunk)
283
+ modeStack.push(ROUTER_MANAGED_MODE)
284
+ }
285
+
286
+ if (entryChunk) {
287
+ chunkStack.push(entryChunk)
288
+ modeStack.push(ROUTER_MANAGED_MODE)
289
+ }
290
+
291
+ while (chunkStack.length > 0) {
292
+ const chunk = chunkStack.pop()!
293
+ const mode = modeStack.pop()!
294
+ const previousMode = visitedByChunk.get(chunk) ?? 0
295
+
296
+ if ((previousMode & mode) === mode) {
297
+ continue
298
+ }
299
+
300
+ visitedByChunk.set(chunk, previousMode | mode)
301
+
302
+ if ((mode & ROUTER_MANAGED_MODE) !== 0) {
303
+ for (const cssFile of chunk.viteMetadata?.importedCss ?? []) {
304
+ routerManagedCssFiles.add(cssFile)
305
+ }
306
+ }
307
+
308
+ if ((mode & NON_ROUTE_DYNAMIC_MODE) !== 0) {
309
+ for (const cssFile of chunk.viteMetadata?.importedCss ?? []) {
310
+ nonRouteDynamicCssFiles.add(cssFile)
311
+ }
312
+ }
313
+
314
+ for (let i = 0; i < chunk.imports.length; i++) {
315
+ const importedChunk = chunksByFileName.get(chunk.imports[i]!)
316
+ if (importedChunk) {
317
+ chunkStack.push(importedChunk)
318
+ modeStack.push(mode)
319
+ }
320
+ }
321
+
322
+ for (let i = 0; i < chunk.dynamicImports.length; i++) {
323
+ const dynamicImportedChunk = chunksByFileName.get(
324
+ chunk.dynamicImports[i]!,
325
+ )
326
+ if (dynamicImportedChunk) {
327
+ chunkStack.push(dynamicImportedChunk)
328
+ modeStack.push(
329
+ (mode & NON_ROUTE_DYNAMIC_MODE) !== 0 ||
330
+ !routeEntryChunks.has(dynamicImportedChunk)
331
+ ? NON_ROUTE_DYNAMIC_MODE
332
+ : ROUTER_MANAGED_MODE,
333
+ )
334
+ }
335
+ }
336
+ }
337
+
338
+ for (const cssFile of routerManagedCssFiles) {
339
+ if (nonRouteDynamicCssFiles.has(cssFile)) {
340
+ hashedCssFiles.add(cssFile)
341
+ }
342
+ }
343
+
344
+ return hashedCssFiles
345
+ }
346
+
347
+ export function createManifestAssetResolvers(options: {
348
+ basePath: string
349
+ hashedCssFiles?: Set<string>
350
+ }): ManifestAssetResolvers {
351
+ const assetPathByFileName = new Map<string, string>()
352
+ const stylesheetAssetByFileName = new Map<string, RouterManagedTag>()
353
+ const preloadsByChunk = new Map<Rollup.OutputChunk, Array<string>>()
354
+
355
+ const getAssetPath = (fileName: string) => {
356
+ const cachedPath = assetPathByFileName.get(fileName)
357
+ if (cachedPath) {
358
+ return cachedPath
359
+ }
360
+
361
+ const assetPath = joinURL(options.basePath, fileName)
362
+ assetPathByFileName.set(fileName, assetPath)
363
+ return assetPath
364
+ }
365
+
366
+ const getStylesheetAsset = (cssFile: string) => {
367
+ const cachedAsset = stylesheetAssetByFileName.get(cssFile)
368
+ if (cachedAsset) {
369
+ return cachedAsset
370
+ }
371
+
372
+ const href = getAssetPath(cssFile)
373
+ const asset = {
374
+ tag: 'link',
375
+ attrs: {
376
+ rel: 'stylesheet',
377
+ href: options.hashedCssFiles?.has(cssFile) ? `${href}#` : href,
378
+ type: 'text/css',
379
+ },
380
+ } satisfies RouterManagedTag
381
+
382
+ stylesheetAssetByFileName.set(cssFile, asset)
383
+ return asset
384
+ }
385
+
386
+ const getChunkPreloads = (chunk: Rollup.OutputChunk) => {
387
+ const cachedPreloads = preloadsByChunk.get(chunk)
388
+ if (cachedPreloads) {
389
+ return cachedPreloads
390
+ }
391
+
392
+ const preloads = [getAssetPath(chunk.fileName)]
393
+
394
+ for (let i = 0; i < chunk.imports.length; i++) {
395
+ preloads.push(getAssetPath(chunk.imports[i]!))
396
+ }
397
+
398
+ preloadsByChunk.set(chunk, preloads)
399
+ return preloads
400
+ }
401
+
402
+ return {
403
+ getAssetPath,
404
+ getChunkPreloads,
405
+ getStylesheetAsset,
406
+ }
407
+ }
408
+
409
+ export function createChunkCssAssetCollector(options: {
410
+ chunksByFileName: Map<string, Rollup.OutputChunk>
411
+ getStylesheetAsset: (cssFile: string) => RouterManagedTag
412
+ }) {
413
+ const assetsByChunk = new Map<Rollup.OutputChunk, Array<RouterManagedTag>>()
414
+ const stateByChunk = new Map<Rollup.OutputChunk, number>()
415
+
416
+ const getChunkCssAssets = (
417
+ chunk: Rollup.OutputChunk,
418
+ ): Array<RouterManagedTag> => {
419
+ const cachedAssets = assetsByChunk.get(chunk)
420
+ if (cachedAssets) {
421
+ return cachedAssets
422
+ }
423
+
424
+ if (stateByChunk.get(chunk) === VISITING_CHUNK) {
425
+ return []
426
+ }
427
+ stateByChunk.set(chunk, VISITING_CHUNK)
428
+
429
+ const assets: Array<RouterManagedTag> = []
430
+
431
+ for (const cssFile of chunk.viteMetadata?.importedCss ?? []) {
432
+ assets.push(options.getStylesheetAsset(cssFile))
433
+ }
434
+
435
+ for (let i = 0; i < chunk.imports.length; i++) {
436
+ const importedChunk = options.chunksByFileName.get(chunk.imports[i]!)
437
+ if (!importedChunk) {
438
+ continue
439
+ }
440
+
441
+ const importedAssets = getChunkCssAssets(importedChunk)
442
+ for (let j = 0; j < importedAssets.length; j++) {
443
+ assets.push(importedAssets[j]!)
444
+ }
445
+ }
446
+
447
+ stateByChunk.delete(chunk)
448
+ assetsByChunk.set(chunk, assets)
449
+ return assets
450
+ }
451
+
452
+ return { getChunkCssAssets }
453
+ }
454
+
455
+ export function buildRouteManifestRoutes(options: {
456
+ routeTreeRoutes: RouteTreeRoutes
457
+ routeChunksByFilePath: Map<string, Array<Rollup.OutputChunk>>
458
+ chunksByFileName: Map<string, Rollup.OutputChunk>
459
+ entryChunk: Rollup.OutputChunk
460
+ assetResolvers: ManifestAssetResolvers
461
+ }) {
462
+ const routes: Record<string, RouteTreeRoute> = {}
463
+ const getChunkCssAssets = createChunkCssAssetCollector({
464
+ chunksByFileName: options.chunksByFileName,
465
+ getStylesheetAsset: options.assetResolvers.getStylesheetAsset,
466
+ }).getChunkCssAssets
467
+
468
+ for (const [routeId, route] of Object.entries(options.routeTreeRoutes)) {
469
+ if (!route.filePath) {
470
+ if (routeId === rootRouteId) {
471
+ routes[routeId] = route
472
+ continue
473
+ }
474
+
475
+ throw new Error(`expected filePath to be set for ${routeId}`)
476
+ }
477
+
478
+ const chunks = options.routeChunksByFilePath.get(route.filePath)
479
+ if (!chunks) {
480
+ routes[routeId] = route
481
+ continue
482
+ }
483
+
484
+ const existing = routes[routeId]
485
+ const targetRoute = (routes[routeId] = existing ? existing : { ...route })
486
+
487
+ for (const chunk of chunks) {
488
+ mergeRouteChunkData({
489
+ route: targetRoute,
490
+ chunk,
491
+ getChunkCssAssets,
492
+ getChunkPreloads: options.assetResolvers.getChunkPreloads,
493
+ })
494
+ }
495
+ }
496
+
497
+ const rootRoute = (routes[rootRouteId] = routes[rootRouteId] || {})
498
+ mergeRouteChunkData({
499
+ route: rootRoute,
500
+ chunk: options.entryChunk,
501
+ getChunkCssAssets,
502
+ getChunkPreloads: options.assetResolvers.getChunkPreloads,
503
+ })
504
+
505
+ return routes
506
+ }
507
+
508
+ export function dedupeNestedRoutePreloads(
509
+ route: { preloads?: Array<string>; children?: Array<string> },
510
+ routesById: Record<string, RouteTreeRoute>,
511
+ seenPreloads = new Set<string>(),
512
+ ) {
513
+ let routePreloads = route.preloads
514
+
515
+ if (routePreloads && routePreloads.length > 0) {
516
+ let dedupedPreloads: Array<string> | undefined
517
+
518
+ for (let i = 0; i < routePreloads.length; i++) {
519
+ const preload = routePreloads[i]!
520
+ if (seenPreloads.has(preload)) {
521
+ if (dedupedPreloads === undefined) {
522
+ dedupedPreloads = routePreloads.slice(0, i)
523
+ }
524
+ continue
525
+ }
526
+
527
+ seenPreloads.add(preload)
528
+
529
+ if (dedupedPreloads) {
530
+ dedupedPreloads.push(preload)
531
+ }
532
+ }
533
+
534
+ if (dedupedPreloads) {
535
+ routePreloads = dedupedPreloads
536
+ route.preloads = dedupedPreloads
537
+ }
538
+ }
539
+
540
+ if (route.children) {
541
+ for (const childRouteId of route.children) {
542
+ dedupeNestedRoutePreloads(
543
+ routesById[childRouteId]!,
544
+ routesById,
545
+ seenPreloads,
546
+ )
547
+ }
548
+ }
549
+
550
+ if (routePreloads) {
551
+ for (let i = routePreloads.length - 1; i >= 0; i--) {
552
+ seenPreloads.delete(routePreloads[i]!)
553
+ }
554
+ }
555
+ }