Skip to content

Commit 7bdd8a9

Browse files
committed
wip: revamp state handling
1 parent 804d752 commit 7bdd8a9

File tree

2 files changed

+264
-79
lines changed

2 files changed

+264
-79
lines changed

packages/vite/src/node/server/environments/fullBundleEnvironment.ts

Lines changed: 213 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import { prepareError } from '../middlewares/error'
1616

1717
const debug = createDebugger('vite:full-bundle-mode')
1818

19+
type HmrOutput = Exclude<
20+
Awaited<ReturnType<RolldownBuild['generateHmrPatch']>>,
21+
undefined
22+
>
23+
1924
export class FullBundleDevEnvironment extends DevEnvironment {
20-
private rolldownOptions: RolldownOptions | undefined
21-
private bundle: RolldownBuild | undefined
25+
private state: BundleState = { type: 'initial' }
26+
2227
watchFiles = new Set<string>()
2328
memoryFiles = new Map<string, string | Uint8Array>()
2429

@@ -39,48 +44,211 @@ export class FullBundleDevEnvironment extends DevEnvironment {
3944
override async listen(server: ViteDevServer): Promise<void> {
4045
await super.listen(server)
4146

42-
debug?.('setup bundle options')
47+
debug?.('INITIAL: setup bundle options')
4348
const rollupOptions = await this.getRolldownOptions()
4449
const { rolldown } = await import('rolldown')
45-
this.rolldownOptions = rollupOptions
46-
this.bundle = await rolldown(rollupOptions)
47-
debug?.('bundle created')
50+
const bundle = await rolldown(rollupOptions)
51+
debug?.('INITIAL: bundle created')
4852

49-
this.triggerGenerateInitialBundle(rollupOptions.output)
53+
debug?.('BUNDLING: trigger initial bundle')
54+
this.triggerGenerateBundle({ options: rollupOptions, bundle })
5055
}
5156

5257
async onFileChange(
5358
_type: 'create' | 'update' | 'delete',
5459
file: string,
5560
server: ViteDevServer,
5661
): Promise<void> {
57-
// TODO: handle the case when the initial bundle is not generated yet
62+
if (this.state.type === 'initial') {
63+
return
64+
}
65+
66+
if (this.state.type === 'bundling') {
67+
debug?.(
68+
`BUNDLING: file update detected ${file}, retriggering bundle generation`,
69+
)
70+
this.state.abortController.abort()
71+
this.triggerGenerateBundle(this.state)
72+
return
73+
}
74+
if (this.state.type === 'bundle-error') {
75+
debug?.(
76+
`BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`,
77+
)
78+
this.triggerGenerateBundle(this.state)
79+
return
80+
}
5881

59-
debug?.(`file update detected ${file}, generating hmr patch`)
60-
// NOTE: only single outputOptions is supported here
61-
const hmrOutput = (await this.bundle!.generateHmrPatch([file]))!
82+
if (
83+
this.state.type === 'bundled' ||
84+
this.state.type === 'generating-hmr-patch'
85+
) {
86+
if (this.state.type === 'bundled') {
87+
debug?.(`BUNDLED: file update detected ${file}, generating HMR patch`)
88+
} else if (this.state.type === 'generating-hmr-patch') {
89+
debug?.(
90+
`GENERATING-HMR-PATCH: file update detected ${file}, regenerating HMR patch`,
91+
)
92+
}
6293

63-
debug?.(`handle hmr output for ${file}`, {
64-
...hmrOutput,
65-
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
66-
})
67-
if (hmrOutput.fullReload) {
94+
this.state = {
95+
type: 'generating-hmr-patch',
96+
options: this.state.options,
97+
bundle: this.state.bundle,
98+
}
99+
100+
let hmrOutput: HmrOutput
68101
try {
69-
await this.generateBundle(this.rolldownOptions!.output)
102+
// NOTE: only single outputOptions is supported here
103+
hmrOutput = (await this.state.bundle.generateHmrPatch([file]))!
70104
} catch (e) {
71105
// TODO: support multiple errors
72106
server.ws.send({ type: 'error', err: prepareError(e.errors[0]) })
107+
108+
this.state = {
109+
type: 'bundled',
110+
options: this.state.options,
111+
bundle: this.state.bundle,
112+
}
73113
return
74114
}
75115

76-
server.ws.send({ type: 'full-reload' })
116+
debug?.(`handle hmr output for ${file}`, {
117+
...hmrOutput,
118+
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
119+
})
120+
121+
this.handleHmrOutput(file, hmrOutput, this.state)
122+
return
123+
}
124+
this.state satisfies never // exhaustive check
125+
}
126+
127+
override async close(): Promise<void> {
128+
await Promise.all([
129+
super.close(),
130+
(async () => {
131+
if (this.state.type === 'initial') {
132+
return
133+
}
134+
if (this.state.type === 'bundling') {
135+
this.state.abortController.abort()
136+
}
137+
const bundle = this.state.bundle
138+
this.state = { type: 'initial' }
139+
140+
this.watchFiles.clear()
141+
this.memoryFiles.clear()
142+
await bundle.close()
143+
})(),
144+
])
145+
}
146+
147+
private async getRolldownOptions() {
148+
const chunkMetadataMap = new Map<string, ChunkMetadata>()
149+
const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap)
150+
rolldownOptions.experimental ??= {}
151+
rolldownOptions.experimental.hmr = {
152+
implement: await getHmrImplementation(this.getTopLevelConfig()),
153+
}
154+
155+
rolldownOptions.treeshake = false
156+
157+
return rolldownOptions
158+
}
159+
160+
private triggerGenerateBundle({
161+
options,
162+
bundle,
163+
}: BundleStateCommonProperties) {
164+
const controller = new AbortController()
165+
const promise = this.generateBundle(
166+
options.output,
167+
bundle,
168+
controller.signal,
169+
)
170+
this.state = {
171+
type: 'bundling',
172+
options,
173+
bundle,
174+
promise,
175+
abortController: controller,
176+
}
177+
}
178+
179+
private async generateBundle(
180+
outOpts: RolldownOptions['output'],
181+
bundle: RolldownBuild,
182+
signal: AbortSignal,
183+
) {
184+
try {
185+
const newMemoryFiles = new Map<string, string | Uint8Array>()
186+
for (const outputOpts of arraify(outOpts)) {
187+
const output = await bundle.generate(outputOpts)
188+
if (signal.aborted) return
189+
190+
for (const outputFile of output.output) {
191+
newMemoryFiles.set(
192+
outputFile.fileName,
193+
outputFile.type === 'chunk' ? outputFile.code : outputFile.source,
194+
)
195+
}
196+
}
197+
198+
this.memoryFiles.clear()
199+
for (const [file, code] of newMemoryFiles) {
200+
this.memoryFiles.set(file, code)
201+
}
202+
203+
// TODO: should this be done for hmr patch file generation?
204+
for (const file of await bundle.watchFiles) {
205+
this.watchFiles.add(file)
206+
}
207+
if (signal.aborted) return
208+
209+
if (this.state.type === 'initial') throw new Error('unreachable')
210+
this.state = {
211+
type: 'bundled',
212+
bundle: this.state.bundle,
213+
options: this.state.options,
214+
}
215+
debug?.('BUNDLED: bundle generated')
216+
217+
this.hot.send({ type: 'full-reload' })
218+
this.logger.info(colors.green(`page reload`), { timestamp: true })
219+
} catch (e) {
220+
enhanceRollupError(e)
221+
clearLine()
222+
this.logger.error(`${colors.red('✗')} Build failed` + e.stack)
223+
224+
// TODO: support multiple errors
225+
this.hot.send({ type: 'error', err: prepareError(e.errors[0]) })
226+
227+
if (this.state.type === 'initial') throw new Error('unreachable')
228+
this.state = {
229+
type: 'bundle-error',
230+
bundle: this.state.bundle,
231+
options: this.state.options,
232+
}
233+
debug?.('BUNDLED: bundle errored')
234+
}
235+
}
236+
237+
private async handleHmrOutput(
238+
file: string,
239+
hmrOutput: HmrOutput,
240+
{ options, bundle }: BundleStateCommonProperties,
241+
) {
242+
if (hmrOutput.fullReload) {
243+
this.triggerGenerateBundle({ options, bundle })
244+
77245
const reason = hmrOutput.fullReloadReason
78246
? colors.dim(` (${hmrOutput.fullReloadReason})`)
79247
: ''
80248
this.logger.info(
81-
colors.green(`page reload `) + colors.dim(file) + reason,
249+
colors.green(`trigger page reload `) + colors.dim(file) + reason,
82250
{
83-
clear: !hmrOutput.firstInvalidatedBy,
251+
// clear: !hmrOutput.firstInvalidatedBy,
84252
timestamp: true,
85253
},
86254
)
@@ -102,7 +270,7 @@ export class FullBundleDevEnvironment extends DevEnvironment {
102270
timestamp: 0,
103271
}
104272
})
105-
server!.ws.send({
273+
this.hot.send({
106274
type: 'update',
107275
updates,
108276
})
@@ -113,62 +281,30 @@ export class FullBundleDevEnvironment extends DevEnvironment {
113281
)
114282
}
115283
}
284+
}
116285

117-
override async close(): Promise<void> {
118-
await Promise.all([
119-
super.close(),
120-
this.bundle?.close().finally(() => {
121-
this.bundle = undefined
122-
this.watchFiles.clear()
123-
this.memoryFiles.clear()
124-
}),
125-
])
126-
}
127-
128-
private async getRolldownOptions() {
129-
const chunkMetadataMap = new Map<string, ChunkMetadata>()
130-
const rolldownOptions = resolveRolldownOptions(this, chunkMetadataMap)
131-
rolldownOptions.experimental ??= {}
132-
rolldownOptions.experimental.hmr = {
133-
implement: await getHmrImplementation(this.getTopLevelConfig()),
134-
}
135-
136-
rolldownOptions.treeshake = false
137-
138-
return rolldownOptions
139-
}
140-
141-
private async triggerGenerateInitialBundle(
142-
outOpts: RolldownOptions['output'],
143-
) {
144-
this.generateBundle(outOpts).then(
145-
() => {
146-
debug?.('initial bundle generated')
147-
},
148-
(e) => {
149-
enhanceRollupError(e)
150-
clearLine()
151-
this.logger.error(`${colors.red('✗')} Build failed` + e.stack)
152-
// TODO: show error message on the browser
153-
},
154-
)
155-
}
156-
157-
// TODO: should debounce this
158-
private async generateBundle(outOpts: RolldownOptions['output']) {
159-
for (const outputOpts of arraify(outOpts)) {
160-
const output = await this.bundle!.generate(outputOpts)
161-
for (const outputFile of output.output) {
162-
this.memoryFiles.set(
163-
outputFile.fileName,
164-
outputFile.type === 'chunk' ? outputFile.code : outputFile.source,
165-
)
166-
}
167-
}
286+
// https://mermaid.live/edit#pako:eNqdUk1v4jAQ_SujuRSkFAUMJOSwalWuPXVPq0jIjYfEWmeMHKe0i_jvaxJoqcRuUX2x3se8mZFmh4VVhBmujd0WlXQefi5zhvAaH9Bg0H3DIdze_gDN2mtpev0IOuG5ZWU0l71yQkECcs66Dw-tOuLMd2QO3rU2BGEILumL1OudTVsU1DRnE6jz5upSWklMTvqQsKpqt9pIX1R90SXl0pbq__bTUIPADr9RxhY-V76v_q_S61bsM-7vdtBUckMZeHr1ERj5TCaDHLcVMRC_aGe5JvagGyiMbUhFoD1stTFQWvAWbo7XcZMj7HPGCGtytdQqnNru0CZHX1FNOR5ylXS_c8x5H3yy9fbpjQvMvGspQmfbssJsLU0TULtR0tNSy9LJ-p3dSP5lbX0qIaW9dY_9YXf3HWHpDr2PkcSK3INt2WM2XswnXQJmO3wNOJmOxCIdx0ksRDwX4zTCN8zS-SidTRbxNAlkIvYR_uk6xqNkFk9TMZ2JSSKSREz2fwERkhWq
287+
type BundleState =
288+
| BundleStateInitial
289+
| BundleStateBundling
290+
| BundleStateBundled
291+
| BundleStateBundleError
292+
| BundleStateGeneratingHmrPatch
293+
type BundleStateInitial = { type: 'initial' }
294+
type BundleStateBundling = {
295+
type: 'bundling'
296+
promise: Promise<void>
297+
abortController: AbortController
298+
} & BundleStateCommonProperties
299+
type BundleStateBundled = { type: 'bundled' } & BundleStateCommonProperties
300+
type BundleStateBundleError = {
301+
type: 'bundle-error'
302+
} & BundleStateCommonProperties
303+
type BundleStateGeneratingHmrPatch = {
304+
type: 'generating-hmr-patch'
305+
} & BundleStateCommonProperties
168306

169-
// TODO: should this be done for hmr patch file generation?
170-
for (const file of await this.bundle!.watchFiles) {
171-
this.watchFiles.add(file)
172-
}
173-
}
307+
type BundleStateCommonProperties = {
308+
options: RolldownOptions
309+
bundle: RolldownBuild
174310
}

0 commit comments

Comments
 (0)