@tanstack/start-plugin-core 1.20.3-alpha.1

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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -0
  3. package/dist/cjs/compilers.cjs +402 -0
  4. package/dist/cjs/compilers.cjs.map +1 -0
  5. package/dist/cjs/compilers.d.cts +21 -0
  6. package/dist/cjs/extractHtmlScripts.cjs +35 -0
  7. package/dist/cjs/extractHtmlScripts.cjs.map +1 -0
  8. package/dist/cjs/extractHtmlScripts.d.cts +4 -0
  9. package/dist/cjs/index.cjs +15 -0
  10. package/dist/cjs/index.cjs.map +1 -0
  11. package/dist/cjs/index.d.cts +7 -0
  12. package/dist/cjs/nitro/build-nitro.cjs +18 -0
  13. package/dist/cjs/nitro/build-nitro.cjs.map +1 -0
  14. package/dist/cjs/nitro/build-nitro.d.cts +2 -0
  15. package/dist/cjs/nitro/build-sitemap.cjs +54 -0
  16. package/dist/cjs/nitro/build-sitemap.cjs.map +1 -0
  17. package/dist/cjs/nitro/build-sitemap.d.cts +9 -0
  18. package/dist/cjs/nitro/dev-server-plugin.cjs +128 -0
  19. package/dist/cjs/nitro/dev-server-plugin.cjs.map +1 -0
  20. package/dist/cjs/nitro/dev-server-plugin.d.cts +5 -0
  21. package/dist/cjs/nitro/nitro-plugin.cjs +128 -0
  22. package/dist/cjs/nitro/nitro-plugin.cjs.map +1 -0
  23. package/dist/cjs/nitro/nitro-plugin.d.cts +3 -0
  24. package/dist/cjs/plugin.cjs +117 -0
  25. package/dist/cjs/plugin.cjs.map +1 -0
  26. package/dist/cjs/plugin.d.cts +2713 -0
  27. package/dist/cjs/prerender.cjs +171 -0
  28. package/dist/cjs/prerender.cjs.map +1 -0
  29. package/dist/cjs/prerender.d.cts +8 -0
  30. package/dist/cjs/queue.cjs +131 -0
  31. package/dist/cjs/queue.cjs.map +1 -0
  32. package/dist/cjs/queue.d.cts +32 -0
  33. package/dist/cjs/routesManifestPlugin.cjs +165 -0
  34. package/dist/cjs/routesManifestPlugin.cjs.map +1 -0
  35. package/dist/cjs/routesManifestPlugin.d.cts +3 -0
  36. package/dist/cjs/schema.cjs +136 -0
  37. package/dist/cjs/schema.cjs.map +1 -0
  38. package/dist/cjs/schema.d.cts +8128 -0
  39. package/dist/cjs/start-compiler-plugin.cjs +72 -0
  40. package/dist/cjs/start-compiler-plugin.cjs.map +1 -0
  41. package/dist/cjs/start-compiler-plugin.d.cts +13 -0
  42. package/dist/cjs/start-server-routes-plugin/config.d.cts +49 -0
  43. package/dist/cjs/start-server-routes-plugin/plugin.cjs +608 -0
  44. package/dist/cjs/start-server-routes-plugin/plugin.cjs.map +1 -0
  45. package/dist/cjs/start-server-routes-plugin/plugin.d.cts +3 -0
  46. package/dist/cjs/start-server-routes-plugin/template.cjs +111 -0
  47. package/dist/cjs/start-server-routes-plugin/template.cjs.map +1 -0
  48. package/dist/cjs/start-server-routes-plugin/template.d.cts +34 -0
  49. package/dist/esm/compilers.d.ts +21 -0
  50. package/dist/esm/compilers.js +384 -0
  51. package/dist/esm/compilers.js.map +1 -0
  52. package/dist/esm/extractHtmlScripts.d.ts +4 -0
  53. package/dist/esm/extractHtmlScripts.js +18 -0
  54. package/dist/esm/extractHtmlScripts.js.map +1 -0
  55. package/dist/esm/index.d.ts +7 -0
  56. package/dist/esm/index.js +15 -0
  57. package/dist/esm/index.js.map +1 -0
  58. package/dist/esm/nitro/build-nitro.d.ts +2 -0
  59. package/dist/esm/nitro/build-nitro.js +18 -0
  60. package/dist/esm/nitro/build-nitro.js.map +1 -0
  61. package/dist/esm/nitro/build-sitemap.d.ts +9 -0
  62. package/dist/esm/nitro/build-sitemap.js +54 -0
  63. package/dist/esm/nitro/build-sitemap.js.map +1 -0
  64. package/dist/esm/nitro/dev-server-plugin.d.ts +5 -0
  65. package/dist/esm/nitro/dev-server-plugin.js +128 -0
  66. package/dist/esm/nitro/dev-server-plugin.js.map +1 -0
  67. package/dist/esm/nitro/nitro-plugin.d.ts +3 -0
  68. package/dist/esm/nitro/nitro-plugin.js +128 -0
  69. package/dist/esm/nitro/nitro-plugin.js.map +1 -0
  70. package/dist/esm/plugin.d.ts +2713 -0
  71. package/dist/esm/plugin.js +117 -0
  72. package/dist/esm/plugin.js.map +1 -0
  73. package/dist/esm/prerender.d.ts +8 -0
  74. package/dist/esm/prerender.js +171 -0
  75. package/dist/esm/prerender.js.map +1 -0
  76. package/dist/esm/queue.d.ts +32 -0
  77. package/dist/esm/queue.js +131 -0
  78. package/dist/esm/queue.js.map +1 -0
  79. package/dist/esm/routesManifestPlugin.d.ts +3 -0
  80. package/dist/esm/routesManifestPlugin.js +165 -0
  81. package/dist/esm/routesManifestPlugin.js.map +1 -0
  82. package/dist/esm/schema.d.ts +8128 -0
  83. package/dist/esm/schema.js +136 -0
  84. package/dist/esm/schema.js.map +1 -0
  85. package/dist/esm/start-compiler-plugin.d.ts +13 -0
  86. package/dist/esm/start-compiler-plugin.js +72 -0
  87. package/dist/esm/start-compiler-plugin.js.map +1 -0
  88. package/dist/esm/start-server-routes-plugin/config.d.ts +49 -0
  89. package/dist/esm/start-server-routes-plugin/plugin.d.ts +3 -0
  90. package/dist/esm/start-server-routes-plugin/plugin.js +608 -0
  91. package/dist/esm/start-server-routes-plugin/plugin.js.map +1 -0
  92. package/dist/esm/start-server-routes-plugin/template.d.ts +34 -0
  93. package/dist/esm/start-server-routes-plugin/template.js +111 -0
  94. package/dist/esm/start-server-routes-plugin/template.js.map +1 -0
  95. package/package.json +72 -0
  96. package/src/compilers.ts +759 -0
  97. package/src/extractHtmlScripts.ts +19 -0
  98. package/src/index.ts +15 -0
  99. package/src/nitro/build-nitro.ts +27 -0
  100. package/src/nitro/build-sitemap.ts +79 -0
  101. package/src/nitro/dev-server-plugin.ts +159 -0
  102. package/src/nitro/nitro-plugin.ts +161 -0
  103. package/src/plugin.ts +145 -0
  104. package/src/prerender.ts +245 -0
  105. package/src/queue.ts +153 -0
  106. package/src/routesManifestPlugin.ts +216 -0
  107. package/src/schema.ts +193 -0
  108. package/src/start-compiler-plugin.ts +111 -0
  109. package/src/start-server-routes-plugin/config.ts +8 -0
  110. package/src/start-server-routes-plugin/plugin.ts +890 -0
  111. package/src/start-server-routes-plugin/template.ts +164 -0
@@ -0,0 +1,245 @@
1
+ import { promises as fsp } from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { getRollupConfig } from 'nitropack/rollup'
5
+ import { build as buildNitro, createNitro } from 'nitropack'
6
+ import { joinURL, withBase, withoutBase } from 'ufo'
7
+ import { Queue } from './queue'
8
+ import { buildNitroEnvironment } from './nitro/build-nitro'
9
+ import type { ViteBuilder } from 'vite'
10
+ import type { $Fetch, Nitro } from 'nitropack'
11
+ import type { TanStackStartOutputConfig } from './plugin'
12
+ import type { Page } from './schema'
13
+
14
+ export async function prerender({
15
+ options,
16
+ nitro,
17
+ builder,
18
+ }: {
19
+ options: TanStackStartOutputConfig
20
+ nitro: Nitro
21
+ builder: ViteBuilder
22
+ }) {
23
+ nitro.logger.info('Prendering pages...')
24
+
25
+ // If prerender is enabled but no pages are provided, default to prerendering the root page
26
+ if (options.prerender?.enabled && !options.pages.length) {
27
+ options.pages = [
28
+ {
29
+ path: '/',
30
+ },
31
+ ]
32
+ }
33
+
34
+ const serverEnv = builder.environments['server']
35
+
36
+ if (!serverEnv) {
37
+ throw new Error(`Vite's "server" environment not found`)
38
+ }
39
+
40
+ const prerenderOutputDir = path.resolve(
41
+ options.root,
42
+ '.tanstack-start/build/prerenderer',
43
+ )
44
+
45
+ const nodeNitro = await createNitro({
46
+ ...nitro.options._config,
47
+ preset: 'nitro-prerender',
48
+ logLevel: 0,
49
+ output: {
50
+ dir: prerenderOutputDir,
51
+ serverDir: path.resolve(prerenderOutputDir, 'server'),
52
+ publicDir: path.resolve(prerenderOutputDir, 'public'),
53
+ },
54
+ })
55
+
56
+ const nodeNitroRollupOptions = getRollupConfig(nodeNitro)
57
+
58
+ const build = serverEnv.config.build
59
+
60
+ build.outDir = prerenderOutputDir
61
+
62
+ build.rollupOptions = {
63
+ ...build.rollupOptions,
64
+ ...nodeNitroRollupOptions,
65
+ output: {
66
+ ...build.rollupOptions.output,
67
+ ...nodeNitroRollupOptions.output,
68
+ sourcemap: undefined,
69
+ },
70
+ }
71
+
72
+ await buildNitroEnvironment(nodeNitro, () => buildNitro(nodeNitro))
73
+
74
+ // Import renderer entry
75
+ const serverFilename =
76
+ typeof nodeNitroRollupOptions.output.entryFileNames === 'string'
77
+ ? nodeNitroRollupOptions.output.entryFileNames
78
+ : 'index.mjs'
79
+
80
+ const serverEntrypoint = path.resolve(
81
+ path.join(nodeNitro.options.output.serverDir, serverFilename),
82
+ )
83
+
84
+ const { closePrerenderer, localFetch } = (await import(serverEntrypoint)) as {
85
+ closePrerenderer: () => void
86
+ localFetch: $Fetch
87
+ }
88
+
89
+ try {
90
+ // Crawl all pages
91
+ const pages = await prerenderPages()
92
+
93
+ nitro.logger.info(`Prerendered ${pages.length} pages:`)
94
+ pages.forEach((page) => {
95
+ nitro.logger.info(`- ${page}`)
96
+ })
97
+
98
+ // TODO: Write the prerendered pages to the output directory
99
+ } catch (error) {
100
+ nitro.logger.error(error)
101
+ } finally {
102
+ // Ensure server is always closed
103
+ // server.process.kill()
104
+ closePrerenderer()
105
+ }
106
+
107
+ function extractLinks(html: string): Array<string> {
108
+ const linkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/g
109
+ const links: Array<string> = []
110
+ let match
111
+
112
+ while ((match = linkRegex.exec(html)) !== null) {
113
+ const href = match[1]
114
+ if (href && (href.startsWith('/') || href.startsWith('./'))) {
115
+ links.push(href)
116
+ }
117
+ }
118
+
119
+ return links
120
+ }
121
+
122
+ async function prerenderPages() {
123
+ const seen = new Set<string>()
124
+ const retriesByPath = new Map<string, number>()
125
+ const concurrency = options.prerender?.concurrency ?? os.cpus().length
126
+ nitro.logger.info(`Concurrency: ${concurrency}`)
127
+ const queue = new Queue({ concurrency })
128
+
129
+ options.pages.forEach((_page) => {
130
+ let page = _page as Page
131
+
132
+ if (typeof _page === 'string') {
133
+ page = { path: _page }
134
+ }
135
+
136
+ addCrawlPageTask(page)
137
+ })
138
+
139
+ await queue.start()
140
+
141
+ return Array.from(seen)
142
+
143
+ function addCrawlPageTask(page: Page) {
144
+ // Was the page already seen?
145
+ if (seen.has(page.path)) return
146
+
147
+ // Add the page to the seen set
148
+ seen.add(page.path)
149
+
150
+ if (page.fromCrawl) {
151
+ options.pages.push(page)
152
+ }
153
+
154
+ // If not enabled, skip
155
+ if (!(page.prerender?.enabled ?? true)) return
156
+
157
+ // If there is a filter link, check if the page should be prerendered
158
+ if (options.prerender?.filter && !options.prerender.filter(page)) return
159
+
160
+ // Resolve the merged default and page-specific prerender options
161
+ const prerenderOptions = {
162
+ ...options.prerender,
163
+ ...page.prerender,
164
+ }
165
+
166
+ // Add the task
167
+ queue.add(async () => {
168
+ nitro.logger.info(`Crawling: ${page.path}`)
169
+ const retries = retriesByPath.get(page.path) || 0
170
+ try {
171
+ // Fetch the route
172
+ const encodedRoute = encodeURI(page.path)
173
+
174
+ const res = await localFetch<Response>(
175
+ withBase(encodedRoute, nodeNitro.options.baseURL),
176
+ {
177
+ headers: { 'x-nitro-prerender': encodedRoute },
178
+ },
179
+ )
180
+
181
+ if (!res.ok) {
182
+ throw new Error(`Failed to fetch ${page.path}: ${res.statusText}`)
183
+ }
184
+
185
+ // Guess route type and populate fileName
186
+ const contentType = res.headers.get('content-type') || ''
187
+ const isImplicitHTML =
188
+ !page.path.endsWith('.html') && contentType.includes('html')
189
+ // &&
190
+ // !JsonSigRx.test(dataBuff.subarray(0, 32).toString('utf8'))
191
+ const routeWithIndex = page.path.endsWith('/')
192
+ ? page.path + 'index'
193
+ : page.path
194
+
195
+ const htmlPath =
196
+ page.path.endsWith('/') || prerenderOptions.autoSubfolderIndex
197
+ ? joinURL(page.path, 'index.html')
198
+ : page.path + '.html'
199
+
200
+ const filename = withoutBase(
201
+ isImplicitHTML ? htmlPath : routeWithIndex,
202
+ nitro.options.baseURL,
203
+ )
204
+
205
+ const html = await res.text()
206
+
207
+ const filepath = path.join(nitro.options.output.publicDir, filename)
208
+
209
+ await fsp.mkdir(path.dirname(filepath), {
210
+ recursive: true,
211
+ })
212
+
213
+ await fsp.writeFile(filepath, html)
214
+
215
+ const newPage = await prerenderOptions.onSuccess?.({ page, html })
216
+
217
+ if (newPage) {
218
+ Object.assign(page, newPage)
219
+ }
220
+
221
+ // Find new links
222
+ if (prerenderOptions.crawlLinks ?? true) {
223
+ const links = extractLinks(html)
224
+ for (const link of links) {
225
+ addCrawlPageTask({ path: link, fromCrawl: true })
226
+ }
227
+ }
228
+ } catch (error) {
229
+ if (retries < (prerenderOptions.retryCount ?? 0)) {
230
+ nitro.logger.warn(
231
+ `Encountered error, retrying: ${page.path} in 500ms`,
232
+ )
233
+ await new Promise((resolve) =>
234
+ setTimeout(resolve, prerenderOptions.retryDelay),
235
+ )
236
+ retriesByPath.set(page.path, retries + 1)
237
+ addCrawlPageTask(page)
238
+ } else {
239
+ throw error
240
+ }
241
+ }
242
+ })
243
+ }
244
+ }
245
+ }
package/src/queue.ts ADDED
@@ -0,0 +1,153 @@
1
+ interface PoolConfig {
2
+ concurrency?: number
3
+ started?: boolean
4
+ tasks?: Array<() => Promise<any>>
5
+ }
6
+
7
+ const defaultConfig: PoolConfig = {
8
+ concurrency: 5,
9
+ started: false,
10
+ tasks: [],
11
+ }
12
+
13
+ export class Queue<T> {
14
+ private onSettles: Array<(res: any, error: any) => void> = []
15
+ private onErrors: Array<(error: any, task: () => Promise<any>) => void> = []
16
+ private onSuccesses: Array<(result: any, task: () => Promise<any>) => void> =
17
+ []
18
+ private running: boolean
19
+ private active: Array<() => Promise<any>> = []
20
+ private pending: Array<() => Promise<any>>
21
+ private currentConcurrency: number
22
+
23
+ constructor(config: PoolConfig = defaultConfig) {
24
+ const { concurrency, started, tasks } = {
25
+ ...defaultConfig,
26
+ ...config,
27
+ }
28
+ this.running = started!
29
+ this.pending = tasks as Array<() => Promise<any>>
30
+ this.currentConcurrency = concurrency!
31
+ }
32
+
33
+ private tick() {
34
+ if (!this.running) {
35
+ return
36
+ }
37
+ while (
38
+ this.active.length < this.currentConcurrency &&
39
+ this.pending.length
40
+ ) {
41
+ const nextFn = this.pending.shift()
42
+ if (!nextFn) {
43
+ throw new Error('Found task that is not a function')
44
+ }
45
+ this.active.push(nextFn)
46
+ ;(async () => {
47
+ let success = false
48
+ let res!: T
49
+ let error: any
50
+ try {
51
+ res = await nextFn()
52
+ success = true
53
+ } catch (e) {
54
+ error = e
55
+ }
56
+ this.active = this.active.filter((d) => d !== nextFn)
57
+ if (success) {
58
+ this.onSuccesses.forEach((d) => d(res, nextFn))
59
+ } else {
60
+ this.onErrors.forEach((d) => d(error, nextFn))
61
+ }
62
+ this.onSettles.forEach((d) => d(res, error))
63
+ this.tick()
64
+ })()
65
+ }
66
+ }
67
+
68
+ add(fn: () => Promise<T> | T, { priority }: { priority?: boolean } = {}) {
69
+ return new Promise<any>((resolve, reject) => {
70
+ const task = () =>
71
+ Promise.resolve(fn())
72
+ .then((res) => {
73
+ resolve(res)
74
+ return res
75
+ })
76
+ .catch((err) => {
77
+ reject(err)
78
+ throw err
79
+ })
80
+ if (priority) {
81
+ this.pending.unshift(task)
82
+ } else {
83
+ this.pending.push(task)
84
+ }
85
+ this.tick()
86
+ })
87
+ }
88
+
89
+ throttle(n: number) {
90
+ this.currentConcurrency = n
91
+ }
92
+
93
+ onSettled(cb: () => void) {
94
+ this.onSettles.push(cb)
95
+ return () => {
96
+ this.onSettles = this.onSettles.filter((d) => d !== cb)
97
+ }
98
+ }
99
+
100
+ onError(cb: (error: any, task: () => Promise<any>) => void) {
101
+ this.onErrors.push(cb)
102
+ return () => {
103
+ this.onErrors = this.onErrors.filter((d) => d !== cb)
104
+ }
105
+ }
106
+
107
+ onSuccess(cb: (result: any, task: () => Promise<any>) => void) {
108
+ this.onSuccesses.push(cb)
109
+ return () => {
110
+ this.onSuccesses = this.onSuccesses.filter((d) => d !== cb)
111
+ }
112
+ }
113
+
114
+ stop() {
115
+ this.running = false
116
+ }
117
+
118
+ start() {
119
+ this.running = true
120
+ this.tick()
121
+ return new Promise<void>((resolve) => {
122
+ this.onSettled(() => {
123
+ if (this.isSettled()) {
124
+ resolve()
125
+ }
126
+ })
127
+ })
128
+ }
129
+
130
+ clear() {
131
+ this.pending = []
132
+ }
133
+
134
+ getActive() {
135
+ return this.active
136
+ }
137
+
138
+ getPending() {
139
+ return this.pending
140
+ }
141
+
142
+ getAll() {
143
+ return [...this.active, ...this.pending]
144
+ }
145
+
146
+ isRunning() {
147
+ return this.running
148
+ }
149
+
150
+ isSettled() {
151
+ return !this.active.length && !this.pending.length
152
+ }
153
+ }
@@ -0,0 +1,216 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { joinURL } from 'ufo'
4
+ import { rootRouteId } from '@tanstack/router-core'
5
+ import type {
6
+ PluginOption,
7
+ ResolvedConfig,
8
+ Manifest as ViteManifest,
9
+ ManifestChunk as ViteManifestChunk,
10
+ } from 'vite'
11
+ import type { Manifest, RouterManagedTag } from '@tanstack/router-core'
12
+ import type { TanStackStartOutputConfig } from './plugin'
13
+
14
+ export function startManifestPlugin(
15
+ opts: TanStackStartOutputConfig,
16
+ ): PluginOption {
17
+ let config: ResolvedConfig
18
+
19
+ return {
20
+ name: 'tsr-routes-manifest',
21
+ enforce: 'pre',
22
+
23
+ configResolved(resolvedConfig) {
24
+ config = resolvedConfig
25
+ },
26
+ // configEnvironment(env, envConfig) {
27
+ // config = envConfig.
28
+ // },
29
+ resolveId(id) {
30
+ if (id === 'tanstack:start-manifest') {
31
+ return id
32
+ }
33
+ return
34
+ },
35
+ load(id) {
36
+ if (id === 'tanstack:start-manifest') {
37
+ if (this.environment.config.consumer !== 'server') {
38
+ // this will ultimately fail the build if the plugin is used outside the server environment
39
+ // TODO: do we need special handling for `serve`?
40
+ return `export default {}`
41
+ }
42
+ // If we're in development, return a dummy manifest
43
+
44
+ if (config.command === 'serve') {
45
+ return `export const tsrStartManifest = () => ({
46
+ entry: "$${process.env.TSS_CLIENT_BASE}/",
47
+ routes: {}
48
+ })`
49
+ }
50
+
51
+ const clientViteManifestPath = path.resolve(
52
+ opts.root,
53
+ '.tanstack-start/build/client-dist/.vite/manifest.json',
54
+ )
55
+
56
+ let viteManifest: ViteManifest
57
+ try {
58
+ viteManifest = JSON.parse(
59
+ readFileSync(clientViteManifestPath, 'utf-8'),
60
+ )
61
+ } catch (err) {
62
+ console.error(err)
63
+ throw new Error(
64
+ `Could not find the production client vite manifest at '${clientViteManifestPath}'!`,
65
+ )
66
+ }
67
+
68
+ const routeTreePath = path.resolve(opts.tsr.generatedRouteTree)
69
+
70
+ let routeTreeContent: string
71
+ try {
72
+ routeTreeContent = readFileSync(routeTreePath, 'utf-8')
73
+ } catch (err) {
74
+ console.error(err)
75
+ throw new Error(
76
+ `Could not find the generated route tree at '${routeTreePath}'!`,
77
+ )
78
+ }
79
+
80
+ // Extract the routesManifest JSON from the route tree file.
81
+ // It's located between the /* ROUTE_MANIFEST_START and ROUTE_MANIFEST_END */ comment block.
82
+
83
+ const routerManifest = JSON.parse(
84
+ routeTreeContent.match(
85
+ /\/\* ROUTE_MANIFEST_START([\s\S]*?)ROUTE_MANIFEST_END \*\//,
86
+ )?.[1] || '{ routes: {} }',
87
+ ) as Manifest
88
+
89
+ const routes = routerManifest.routes
90
+
91
+ let entryFile: ViteManifestChunk | undefined
92
+
93
+ const filesByRouteFilePath: ViteManifest = Object.fromEntries(
94
+ Object.entries(viteManifest).map(([k, v]) => {
95
+ if (v.isEntry) {
96
+ entryFile = v
97
+ }
98
+
99
+ const rPath = k.split('?')[0]
100
+
101
+ return [rPath, v]
102
+ }, {}),
103
+ )
104
+
105
+ const routesDirectoryFromRoot = path.relative(
106
+ opts.root,
107
+ opts.tsr.routesDirectory,
108
+ )
109
+
110
+ // Add preloads to the routes from the vite manifest
111
+ Object.entries(routes).forEach(([k, v]) => {
112
+ const file =
113
+ filesByRouteFilePath[
114
+ path.join(routesDirectoryFromRoot, v.filePath as string)
115
+ ]
116
+
117
+ if (file) {
118
+ const preloads = (file.imports ?? []).map((d) =>
119
+ path.join('/', viteManifest[d]!.file),
120
+ )
121
+
122
+ if (file.file) {
123
+ preloads.unshift(path.join('/', file.file))
124
+ }
125
+
126
+ const cssFiles = file.css ?? []
127
+ const cssAssetsList: Array<RouterManagedTag> = cssFiles.map(
128
+ (cssFile) => ({
129
+ tag: 'link',
130
+ attrs: {
131
+ rel: 'stylesheet',
132
+ href: joinURL('/', cssFile),
133
+ type: 'text/css',
134
+ },
135
+ }),
136
+ )
137
+
138
+ routes[k] = {
139
+ ...v,
140
+ assets: [...(v.assets || []), ...cssAssetsList],
141
+ preloads,
142
+ }
143
+ }
144
+ })
145
+
146
+ if (entryFile) {
147
+ routes[rootRouteId]!.preloads = [
148
+ path.join('/', entryFile.file),
149
+ ...(entryFile.imports?.map((d) =>
150
+ path.join('/', viteManifest[d]!.file),
151
+ ) || []),
152
+ ]
153
+
154
+ // Gather all the CSS files from the entry file in
155
+ // the `css` key and add them to the root route
156
+ const entryCssFiles = entryFile.css ?? []
157
+ const entryCssAssetsList: Array<RouterManagedTag> = entryCssFiles.map(
158
+ (cssFile) => ({
159
+ tag: 'link',
160
+ attrs: {
161
+ rel: 'stylesheet',
162
+ href: joinURL('/', cssFile),
163
+ type: 'text/css',
164
+ },
165
+ }),
166
+ )
167
+
168
+ routes[rootRouteId]!.assets = [
169
+ ...(routes[rootRouteId]!.assets || []),
170
+ ...entryCssAssetsList,
171
+ {
172
+ tag: 'script',
173
+ attrs: {
174
+ src: joinURL('/', entryFile.file),
175
+ type: 'module',
176
+ },
177
+ },
178
+ ]
179
+ }
180
+
181
+ const recurseRoute = (
182
+ route: {
183
+ preloads?: Array<string>
184
+ children?: Array<any>
185
+ },
186
+ seenPreloads = {} as Record<string, true>,
187
+ ) => {
188
+ route.preloads = route.preloads?.filter((preload) => {
189
+ if (seenPreloads[preload]) {
190
+ return false
191
+ }
192
+ seenPreloads[preload] = true
193
+ return true
194
+ })
195
+
196
+ if (route.children) {
197
+ route.children.forEach((child) => {
198
+ const childRoute = routes[child]!
199
+ recurseRoute(childRoute, { ...seenPreloads })
200
+ })
201
+ }
202
+ }
203
+
204
+ // @ts-expect-error
205
+ recurseRoute(routes[rootRouteId])
206
+
207
+ const routesManifest = {
208
+ routes,
209
+ }
210
+
211
+ return `export const tsrStartManifest = () => (${JSON.stringify(routesManifest)})`
212
+ }
213
+ return
214
+ },
215
+ }
216
+ }