@tanstack/start-server-core 1.169.2 → 1.169.3

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,16 +1,15 @@
1
- import { buildDevStylesUrl, rootRouteId } from '@tanstack/router-core'
2
- import type {
3
- AnyRoute,
4
- ManifestAssetLink,
5
- RouterManagedTag,
1
+ import {
2
+ DEV_STYLES_ATTR,
3
+ buildDevStylesUrl,
4
+ rootRouteId,
6
5
  } from '@tanstack/router-core'
6
+ import type { AnyRoute, ServerManifestRoute } from '@tanstack/router-core'
7
7
  import type { StartManifestWithClientEntry } from './transformAssetUrls'
8
8
 
9
9
  // Pre-computed constant for dev styles URL basepath.
10
10
  // Defaults to vite `base` (set via TSS_DEV_SSR_STYLES_BASEPATH in the plugin),
11
11
  // aligning dev styles with how other CSS/JS assets are served.
12
12
  const DEV_SSR_STYLES_BASEPATH = process.env.TSS_DEV_SSR_STYLES_BASEPATH || '/'
13
-
14
13
  /**
15
14
  * @description Returns the router manifest data that should be sent to the client.
16
15
  * This includes only the assets and preloads for the current route and any
@@ -28,11 +27,16 @@ export async function getStartManifest(
28
27
  ): Promise<StartManifestWithClientEntry> {
29
28
  const { tsrStartManifest } = await import('tanstack-start-manifest:v')
30
29
  const startManifest = tsrStartManifest()
30
+ let routes = startManifest.routes
31
+ let rootRoute = routes[rootRouteId]
31
32
 
32
- const rootRoute = (startManifest.routes[rootRouteId] =
33
- startManifest.routes[rootRouteId] || {})
34
-
35
- rootRoute.assets = rootRoute.assets || []
33
+ const updateRootRoute = (nextRootRoute: ServerManifestRoute) => {
34
+ rootRoute = nextRootRoute
35
+ routes = {
36
+ ...routes,
37
+ [rootRouteId]: rootRoute,
38
+ }
39
+ }
36
40
 
37
41
  // Inject dev styles link in dev mode (when SSR styles are enabled)
38
42
  if (
@@ -41,54 +45,66 @@ export async function getStartManifest(
41
45
  matchedRoutes
42
46
  ) {
43
47
  const matchedRouteIds = matchedRoutes.map((route) => route.id)
44
- rootRoute.assets.push({
45
- tag: 'link',
46
- attrs: {
47
- rel: 'stylesheet',
48
- href: buildDevStylesUrl(DEV_SSR_STYLES_BASEPATH, matchedRouteIds),
49
- 'data-tanstack-router-dev-styles': 'true',
50
- },
48
+ updateRootRoute({
49
+ ...rootRoute,
50
+ css: [
51
+ ...(rootRoute?.css ?? []),
52
+ {
53
+ href: buildDevStylesUrl(DEV_SSR_STYLES_BASEPATH, matchedRouteIds),
54
+ [DEV_STYLES_ATTR]: true,
55
+ },
56
+ ],
51
57
  })
52
58
  }
53
59
 
54
- // Collect injected head scripts in dev mode (returned separately so we can
55
- // build the client entry script tag after URL transforms are applied)
56
- let injectedHeadScripts: string | undefined
57
60
  if (process.env.TSS_DEV_SERVER === 'true') {
58
61
  const mod = await import('tanstack-start-injected-head-scripts:v')
59
62
  if (mod.injectedHeadScripts) {
60
- injectedHeadScripts = mod.injectedHeadScripts
63
+ updateRootRoute({
64
+ ...rootRoute,
65
+ scripts: [
66
+ ...(rootRoute?.scripts ?? []),
67
+ {
68
+ attrs: {
69
+ type: 'module',
70
+ },
71
+ children: mod.injectedHeadScripts,
72
+ },
73
+ ],
74
+ })
75
+ }
76
+ }
77
+
78
+ const manifestRoutes: Record<string, ServerManifestRoute> = {}
79
+
80
+ for (const k in routes) {
81
+ const v = routes[k]!
82
+ const result = {} as ServerManifestRoute
83
+
84
+ if (v.preloads && v.preloads.length > 0) {
85
+ result.preloads = v.preloads
86
+ }
87
+ if (v.scripts && v.scripts.length > 0) {
88
+ result.scripts = v.scripts
89
+ }
90
+ if (v.css?.length) {
91
+ result.css = v.css
92
+ }
93
+ if (result.preloads || result.scripts || result.css) {
94
+ manifestRoutes[k] = result
61
95
  }
62
96
  }
63
97
 
64
98
  const manifest = {
65
- inlineCss: startManifest.inlineCss,
66
- routes: Object.fromEntries(
67
- Object.entries(startManifest.routes).flatMap(([k, v]) => {
68
- const result = {} as {
69
- preloads?: Array<ManifestAssetLink>
70
- assets?: Array<RouterManagedTag>
71
- }
72
- let hasData = false
73
- if (v.preloads && v.preloads.length > 0) {
74
- result['preloads'] = v.preloads
75
- hasData = true
76
- }
77
- if (v.assets && v.assets.length > 0) {
78
- result['assets'] = v.assets
79
- hasData = true
80
- }
81
- if (!hasData) {
82
- return []
83
- }
84
- return [[k, result]]
85
- }),
86
- ),
99
+ ...(startManifest.scriptFormat
100
+ ? { scriptFormat: startManifest.scriptFormat }
101
+ : {}),
102
+ ...(startManifest.inlineCss ? { inlineCss: startManifest.inlineCss } : {}),
103
+ routes: manifestRoutes,
87
104
  }
88
105
 
89
106
  return {
90
107
  manifest: manifest as StartManifestWithClientEntry['manifest'],
91
108
  clientEntry: startManifest.clientEntry,
92
- injectedHeadScripts,
93
109
  }
94
110
  }
@@ -1,7 +1,7 @@
1
1
  declare module 'tanstack-start-manifest:v' {
2
- import type { Manifest } from '@tanstack/router-core'
2
+ import type { ServerManifest } from '@tanstack/router-core'
3
3
 
4
- export const tsrStartManifest: () => Manifest & { clientEntry: string }
4
+ export const tsrStartManifest: () => ServerManifest & { clientEntry: string }
5
5
  }
6
6
 
7
7
  declare module 'tanstack-start-route-tree:v' {
@@ -1,11 +1,17 @@
1
- import { resolveManifestAssetLink, rootRouteId } from '@tanstack/router-core'
1
+ import {
2
+ getManifestScriptFormat,
3
+ resolveManifestAssetLink,
4
+ resolveManifestCssLink,
5
+ } from '@tanstack/router-core'
2
6
 
3
7
  import type {
4
8
  AssetCrossOrigin,
5
9
  Awaitable,
6
- Manifest,
7
10
  ManifestAssetLink,
8
- RouterManagedTag,
11
+ ManifestCssLink,
12
+ ManifestScript,
13
+ ScriptFormat,
14
+ ServerManifest,
9
15
  } from '@tanstack/router-core'
10
16
 
11
17
  export type { AssetCrossOrigin }
@@ -13,16 +19,12 @@ export type { AssetCrossOrigin }
13
19
  export type TransformAssetsContext =
14
20
  | {
15
21
  url: string
16
- kind: 'modulepreload'
22
+ kind: 'script'
17
23
  }
18
24
  | {
19
25
  url: string
20
26
  kind: 'stylesheet'
21
27
  }
22
- | {
23
- url: string
24
- kind: 'clientEntry'
25
- }
26
28
  | {
27
29
  url: string
28
30
  kind: 'css-url'
@@ -33,7 +35,7 @@ export type TransformAssetKind = TransformAssetsContext['kind']
33
35
 
34
36
  type TransformAssetsShorthandCrossOriginKind = Exclude<
35
37
  TransformAssetKind,
36
- 'clientEntry' | 'css-url'
38
+ 'css-url'
37
39
  >
38
40
 
39
41
  export type TransformAssetResult =
@@ -114,7 +116,7 @@ export type TransformAssetsOptions =
114
116
  * crossOrigin: 'anonymous'
115
117
  *
116
118
  * // Different values per kind
117
- * crossOrigin: { modulepreload: 'anonymous', stylesheet: 'use-credentials' }
119
+ * crossOrigin: { script: 'anonymous', stylesheet: 'use-credentials' }
118
120
  * ```
119
121
  */
120
122
  export type TransformAssetsCrossOriginConfig =
@@ -136,7 +138,7 @@ export interface TransformAssetsObjectShorthand {
136
138
  /** URL prefix prepended to every asset URL. */
137
139
  prefix: string
138
140
  /**
139
- * Optional crossOrigin attribute applied to manifest-managed `<link>` assets.
141
+ * Optional crossOrigin attribute applied to transformed script and stylesheet assets.
140
142
  *
141
143
  * Accepts a single value or a per-kind record.
142
144
  */
@@ -211,7 +213,7 @@ async function transformInlineCssTemplate(options: {
211
213
  }
212
214
 
213
215
  async function transformInlineCssStyles(
214
- inlineCss: NonNullable<Manifest['inlineCss']>,
216
+ inlineCss: NonNullable<ServerManifest['inlineCss']>,
215
217
  transformFn: TransformAssetsFn,
216
218
  ) {
217
219
  const transformedStyles: Record<string, string> = {}
@@ -287,7 +289,7 @@ export function resolveTransformAssetsConfig(
287
289
  transformFn: ({ url, kind }) => {
288
290
  const href = `${prefix}${url}`
289
291
 
290
- if (kind === 'clientEntry' || kind === 'css-url') {
292
+ if (kind === 'css-url') {
291
293
  return { href }
292
294
  }
293
295
 
@@ -321,44 +323,104 @@ export function resolveTransformAssetsConfig(
321
323
  }
322
324
 
323
325
  export interface StartManifestWithClientEntry {
324
- manifest: Manifest
326
+ manifest: ServerManifest
325
327
  clientEntry: string
326
- /** Script content prepended before the client entry import (dev only) */
327
- injectedHeadScripts?: string
328
328
  }
329
329
 
330
330
  /**
331
331
  * Builds the client entry `<script>` tag from a (possibly transformed) client
332
- * entry URL and optional injected head scripts.
332
+ * entry URL.
333
333
  */
334
334
  export function buildClientEntryScriptTag(
335
335
  clientEntry: string,
336
- injectedHeadScripts?: string,
337
- ): RouterManagedTag {
338
- const clientEntryLiteral = JSON.stringify(clientEntry)
339
- let script = `import(${clientEntryLiteral})`
340
- if (injectedHeadScripts) {
341
- script = `${injectedHeadScripts};${script}`
342
- }
336
+ scriptFormat: ScriptFormat = 'module',
337
+ crossOrigin?: AssetCrossOrigin,
338
+ ): ManifestScript {
343
339
  return {
344
- tag: 'script',
345
340
  attrs: {
346
- type: 'module',
341
+ ...(scriptFormat === 'module' ? { type: 'module' } : {}),
347
342
  async: true,
343
+ src: clientEntry,
344
+ ...(crossOrigin ? { crossOrigin } : {}),
348
345
  },
349
- children: script,
350
346
  }
351
347
  }
352
348
 
353
- function assignManifestAssetLink(
349
+ type AssignableManifestLink = ManifestAssetLink | ManifestCssLink
350
+
351
+ function assignManifestLink(
354
352
  link: ManifestAssetLink,
355
353
  next: { href: string; crossOrigin?: AssetCrossOrigin },
356
- ): ManifestAssetLink {
354
+ ): ManifestAssetLink
355
+ function assignManifestLink(
356
+ link: ManifestCssLink,
357
+ next: { href: string; crossOrigin?: AssetCrossOrigin },
358
+ ): ManifestCssLink
359
+ function assignManifestLink(
360
+ link: AssignableManifestLink,
361
+ next: { href: string; crossOrigin?: AssetCrossOrigin },
362
+ ): AssignableManifestLink {
357
363
  if (typeof link === 'string') {
358
364
  return next.crossOrigin ? next : next.href
359
365
  }
360
366
 
361
- return next.crossOrigin ? next : { href: next.href }
367
+ const nextLink: Exclude<ManifestCssLink, string> = {
368
+ ...link,
369
+ href: next.href,
370
+ }
371
+
372
+ if (next.crossOrigin) {
373
+ nextLink.crossOrigin = next.crossOrigin
374
+ } else {
375
+ delete nextLink.crossOrigin
376
+ }
377
+
378
+ return nextLink
379
+ }
380
+
381
+ function appendUniqueManifestAssetLink(
382
+ target: Array<ManifestAssetLink> | undefined,
383
+ link: ManifestAssetLink,
384
+ ) {
385
+ const href = typeof link === 'string' ? link : link.href
386
+
387
+ if (target) {
388
+ for (const item of target) {
389
+ if ((typeof item === 'string' ? item : item.href) === href) {
390
+ return target
391
+ }
392
+ }
393
+ }
394
+
395
+ return [...(target ?? []), link]
396
+ }
397
+
398
+ function addClientEntryToManifest(
399
+ manifest: ServerManifest,
400
+ clientEntry: string,
401
+ ) {
402
+ const rootRoute = manifest.routes.__root__ ?? {}
403
+ const rootScripts = rootRoute.scripts ?? []
404
+ const scripts = rootScripts.some(
405
+ (script) => script.attrs?.src === clientEntry,
406
+ )
407
+ ? rootScripts
408
+ : [
409
+ ...rootScripts,
410
+ buildClientEntryScriptTag(
411
+ clientEntry,
412
+ getManifestScriptFormat(manifest),
413
+ ),
414
+ ]
415
+
416
+ manifest.routes = {
417
+ ...manifest.routes,
418
+ __root__: {
419
+ ...rootRoute,
420
+ preloads: appendUniqueManifestAssetLink(rootRoute.preloads, clientEntry),
421
+ scripts,
422
+ },
423
+ }
362
424
  }
363
425
 
364
426
  export async function transformManifestAssets(
@@ -368,9 +430,28 @@ export async function transformManifestAssets(
368
430
  clone?: boolean
369
431
  inlineCss?: boolean
370
432
  },
371
- ): Promise<Manifest> {
433
+ ): Promise<ServerManifest> {
372
434
  const manifest = structuredClone(source.manifest)
373
435
  const inlineCssEnabled = _opts?.inlineCss !== false
436
+ const scriptTransforms = new Map<
437
+ string,
438
+ Promise<Exclude<TransformAssetResult, string>>
439
+ >()
440
+ const transformScript = (url: string) => {
441
+ const cached = scriptTransforms.get(url)
442
+ if (cached) {
443
+ return cached
444
+ }
445
+
446
+ const transformed = Promise.resolve(
447
+ transformFn({
448
+ url,
449
+ kind: 'script',
450
+ }),
451
+ ).then(normalizeTransformAssetResult)
452
+ scriptTransforms.set(url, transformed)
453
+ return transformed
454
+ }
374
455
 
375
456
  if (!inlineCssEnabled) {
376
457
  delete manifest.inlineCss
@@ -381,19 +462,35 @@ export async function transformManifestAssets(
381
462
  )
382
463
  }
383
464
 
465
+ addClientEntryToManifest(manifest, source.clientEntry)
466
+
384
467
  for (const route of Object.values(manifest.routes)) {
385
- if (route.preloads) {
468
+ if (route.preloads?.length) {
386
469
  route.preloads = await Promise.all(
387
470
  route.preloads.map(async (link) => {
388
471
  const resolved = resolveManifestAssetLink(link)
472
+ const result = await transformScript(resolved.href)
473
+
474
+ return assignManifestLink(link, {
475
+ href: result.href,
476
+ crossOrigin: result.crossOrigin,
477
+ })
478
+ }),
479
+ )
480
+ }
481
+
482
+ if (route.css?.length && !manifest.inlineCss) {
483
+ route.css = await Promise.all(
484
+ route.css.map(async (link) => {
485
+ const resolved = resolveManifestCssLink(link)
389
486
  const result = normalizeTransformAssetResult(
390
487
  await transformFn({
391
488
  url: resolved.href,
392
- kind: 'modulepreload',
489
+ kind: 'stylesheet',
393
490
  }),
394
491
  )
395
492
 
396
- return assignManifestAssetLink(link, {
493
+ return assignManifestLink(link, {
397
494
  href: result.href,
398
495
  crossOrigin: result.crossOrigin,
399
496
  })
@@ -401,56 +498,33 @@ export async function transformManifestAssets(
401
498
  )
402
499
  }
403
500
 
404
- if (route.assets && !manifest.inlineCss) {
405
- for (const asset of route.assets) {
406
- if (asset.tag === 'link' && asset.attrs?.href) {
407
- const rel = asset.attrs.rel
408
- const relTokens = typeof rel === 'string' ? rel.split(/\s+/) : []
409
-
410
- if (!relTokens.includes('stylesheet')) {
411
- continue
412
- }
501
+ if (route.scripts?.length) {
502
+ for (const script of route.scripts) {
503
+ const src = script.attrs?.src
504
+ if (typeof src !== 'string') {
505
+ continue
506
+ }
413
507
 
414
- const result = normalizeTransformAssetResult(
415
- await transformFn({
416
- url: asset.attrs.href,
417
- kind: 'stylesheet',
418
- }),
419
- )
508
+ const result = await transformScript(src)
420
509
 
421
- asset.attrs.href = result.href
422
- if (result.crossOrigin) {
423
- asset.attrs.crossOrigin = result.crossOrigin
424
- } else {
425
- delete asset.attrs.crossOrigin
426
- }
510
+ script.attrs = {
511
+ ...script.attrs,
512
+ src: result.href,
513
+ }
514
+ if (result.crossOrigin) {
515
+ script.attrs.crossOrigin = result.crossOrigin
516
+ } else {
517
+ delete script.attrs.crossOrigin
427
518
  }
428
519
  }
429
520
  }
430
521
  }
431
522
 
432
- const transformedClientEntry = normalizeTransformAssetResult(
433
- await transformFn({
434
- url: source.clientEntry,
435
- kind: 'clientEntry',
436
- }),
437
- )
438
-
439
- const rootRoute = (manifest.routes[rootRouteId] =
440
- manifest.routes[rootRouteId] || {})
441
- rootRoute.assets = rootRoute.assets || []
442
- rootRoute.assets.push(
443
- buildClientEntryScriptTag(
444
- transformedClientEntry.href,
445
- source.injectedHeadScripts,
446
- ),
447
- )
448
-
449
523
  return manifest
450
524
  }
451
525
 
452
526
  /**
453
- * Builds a final Manifest from a StartManifestWithClientEntry without any
527
+ * Builds a final ServerManifest from a StartManifestWithClientEntry without any
454
528
  * URL transforms. Used when no transformAssets option is provided.
455
529
  *
456
530
  * Returns a new manifest object so the cached base manifest is never mutated.
@@ -458,25 +532,20 @@ export async function transformManifestAssets(
458
532
  export function buildManifestWithClientEntry(
459
533
  source: StartManifestWithClientEntry,
460
534
  opts?: { inlineCss?: boolean },
461
- ): Manifest {
462
- const scriptTag = buildClientEntryScriptTag(
463
- source.clientEntry,
464
- source.injectedHeadScripts,
465
- )
466
-
467
- const baseRootRoute = source.manifest.routes[rootRouteId]
468
- const routes = {
469
- ...source.manifest.routes,
470
- [rootRouteId]: {
471
- ...baseRootRoute,
472
- assets: [...(baseRootRoute?.assets || []), scriptTag],
535
+ ): ServerManifest {
536
+ const manifest: ServerManifest = {
537
+ ...(source.manifest.scriptFormat
538
+ ? { scriptFormat: source.manifest.scriptFormat }
539
+ : {}),
540
+ ...(opts?.inlineCss !== false && source.manifest.inlineCss
541
+ ? { inlineCss: structuredClone(source.manifest.inlineCss) }
542
+ : {}),
543
+ routes: {
544
+ ...source.manifest.routes,
473
545
  },
474
546
  }
475
547
 
476
- return {
477
- ...(opts?.inlineCss === false
478
- ? {}
479
- : { inlineCss: structuredClone(source.manifest.inlineCss) }),
480
- routes,
481
- }
548
+ addClientEntryToManifest(manifest, source.clientEntry)
549
+
550
+ return manifest
482
551
  }