@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.
- package/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/cjs/compilers.cjs +402 -0
- package/dist/cjs/compilers.cjs.map +1 -0
- package/dist/cjs/compilers.d.cts +21 -0
- package/dist/cjs/extractHtmlScripts.cjs +35 -0
- package/dist/cjs/extractHtmlScripts.cjs.map +1 -0
- package/dist/cjs/extractHtmlScripts.d.cts +4 -0
- package/dist/cjs/index.cjs +15 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +7 -0
- package/dist/cjs/nitro/build-nitro.cjs +18 -0
- package/dist/cjs/nitro/build-nitro.cjs.map +1 -0
- package/dist/cjs/nitro/build-nitro.d.cts +2 -0
- package/dist/cjs/nitro/build-sitemap.cjs +54 -0
- package/dist/cjs/nitro/build-sitemap.cjs.map +1 -0
- package/dist/cjs/nitro/build-sitemap.d.cts +9 -0
- package/dist/cjs/nitro/dev-server-plugin.cjs +128 -0
- package/dist/cjs/nitro/dev-server-plugin.cjs.map +1 -0
- package/dist/cjs/nitro/dev-server-plugin.d.cts +5 -0
- package/dist/cjs/nitro/nitro-plugin.cjs +128 -0
- package/dist/cjs/nitro/nitro-plugin.cjs.map +1 -0
- package/dist/cjs/nitro/nitro-plugin.d.cts +3 -0
- package/dist/cjs/plugin.cjs +117 -0
- package/dist/cjs/plugin.cjs.map +1 -0
- package/dist/cjs/plugin.d.cts +2713 -0
- package/dist/cjs/prerender.cjs +171 -0
- package/dist/cjs/prerender.cjs.map +1 -0
- package/dist/cjs/prerender.d.cts +8 -0
- package/dist/cjs/queue.cjs +131 -0
- package/dist/cjs/queue.cjs.map +1 -0
- package/dist/cjs/queue.d.cts +32 -0
- package/dist/cjs/routesManifestPlugin.cjs +165 -0
- package/dist/cjs/routesManifestPlugin.cjs.map +1 -0
- package/dist/cjs/routesManifestPlugin.d.cts +3 -0
- package/dist/cjs/schema.cjs +136 -0
- package/dist/cjs/schema.cjs.map +1 -0
- package/dist/cjs/schema.d.cts +8128 -0
- package/dist/cjs/start-compiler-plugin.cjs +72 -0
- package/dist/cjs/start-compiler-plugin.cjs.map +1 -0
- package/dist/cjs/start-compiler-plugin.d.cts +13 -0
- package/dist/cjs/start-server-routes-plugin/config.d.cts +49 -0
- package/dist/cjs/start-server-routes-plugin/plugin.cjs +608 -0
- package/dist/cjs/start-server-routes-plugin/plugin.cjs.map +1 -0
- package/dist/cjs/start-server-routes-plugin/plugin.d.cts +3 -0
- package/dist/cjs/start-server-routes-plugin/template.cjs +111 -0
- package/dist/cjs/start-server-routes-plugin/template.cjs.map +1 -0
- package/dist/cjs/start-server-routes-plugin/template.d.cts +34 -0
- package/dist/esm/compilers.d.ts +21 -0
- package/dist/esm/compilers.js +384 -0
- package/dist/esm/compilers.js.map +1 -0
- package/dist/esm/extractHtmlScripts.d.ts +4 -0
- package/dist/esm/extractHtmlScripts.js +18 -0
- package/dist/esm/extractHtmlScripts.js.map +1 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/nitro/build-nitro.d.ts +2 -0
- package/dist/esm/nitro/build-nitro.js +18 -0
- package/dist/esm/nitro/build-nitro.js.map +1 -0
- package/dist/esm/nitro/build-sitemap.d.ts +9 -0
- package/dist/esm/nitro/build-sitemap.js +54 -0
- package/dist/esm/nitro/build-sitemap.js.map +1 -0
- package/dist/esm/nitro/dev-server-plugin.d.ts +5 -0
- package/dist/esm/nitro/dev-server-plugin.js +128 -0
- package/dist/esm/nitro/dev-server-plugin.js.map +1 -0
- package/dist/esm/nitro/nitro-plugin.d.ts +3 -0
- package/dist/esm/nitro/nitro-plugin.js +128 -0
- package/dist/esm/nitro/nitro-plugin.js.map +1 -0
- package/dist/esm/plugin.d.ts +2713 -0
- package/dist/esm/plugin.js +117 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/prerender.d.ts +8 -0
- package/dist/esm/prerender.js +171 -0
- package/dist/esm/prerender.js.map +1 -0
- package/dist/esm/queue.d.ts +32 -0
- package/dist/esm/queue.js +131 -0
- package/dist/esm/queue.js.map +1 -0
- package/dist/esm/routesManifestPlugin.d.ts +3 -0
- package/dist/esm/routesManifestPlugin.js +165 -0
- package/dist/esm/routesManifestPlugin.js.map +1 -0
- package/dist/esm/schema.d.ts +8128 -0
- package/dist/esm/schema.js +136 -0
- package/dist/esm/schema.js.map +1 -0
- package/dist/esm/start-compiler-plugin.d.ts +13 -0
- package/dist/esm/start-compiler-plugin.js +72 -0
- package/dist/esm/start-compiler-plugin.js.map +1 -0
- package/dist/esm/start-server-routes-plugin/config.d.ts +49 -0
- package/dist/esm/start-server-routes-plugin/plugin.d.ts +3 -0
- package/dist/esm/start-server-routes-plugin/plugin.js +608 -0
- package/dist/esm/start-server-routes-plugin/plugin.js.map +1 -0
- package/dist/esm/start-server-routes-plugin/template.d.ts +34 -0
- package/dist/esm/start-server-routes-plugin/template.js +111 -0
- package/dist/esm/start-server-routes-plugin/template.js.map +1 -0
- package/package.json +72 -0
- package/src/compilers.ts +759 -0
- package/src/extractHtmlScripts.ts +19 -0
- package/src/index.ts +15 -0
- package/src/nitro/build-nitro.ts +27 -0
- package/src/nitro/build-sitemap.ts +79 -0
- package/src/nitro/dev-server-plugin.ts +159 -0
- package/src/nitro/nitro-plugin.ts +161 -0
- package/src/plugin.ts +145 -0
- package/src/prerender.ts +245 -0
- package/src/queue.ts +153 -0
- package/src/routesManifestPlugin.ts +216 -0
- package/src/schema.ts +193 -0
- package/src/start-compiler-plugin.ts +111 -0
- package/src/start-server-routes-plugin/config.ts +8 -0
- package/src/start-server-routes-plugin/plugin.ts +890 -0
- package/src/start-server-routes-plugin/template.ts +164 -0
package/src/prerender.ts
ADDED
|
@@ -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
|
+
}
|