Skip to content

Commit 9e22453

Browse files
committed
wip: revamp state handling
1 parent 5d9390a commit 9e22453

File tree

2 files changed

+263
-78
lines changed

2 files changed

+263
-78
lines changed

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

Lines changed: 212 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import { prepareError } from '../middlewares/error'
1515

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

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

@@ -38,48 +43,210 @@ export class FullBundleDevEnvironment extends DevEnvironment {
3843
override async listen(server: ViteDevServer): Promise<void> {
3944
await super.listen(server)
4045

41-
debug?.('setup bundle options')
46+
debug?.('INITIAL: setup bundle options')
4247
const rollupOptions = await this.getRolldownOptions()
4348
const { rolldown } = await import('rolldown')
44-
this.rolldownOptions = rollupOptions
45-
this.bundle = await rolldown(rollupOptions)
46-
debug?.('bundle created')
49+
const bundle = await rolldown(rollupOptions)
50+
debug?.('INITIAL: bundle created')
4751

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

5156
async onFileChange(
5257
_type: 'create' | 'update' | 'delete',
5358
file: string,
5459
server: ViteDevServer,
5560
): Promise<void> {
56-
// TODO: handle the case when the initial bundle is not generated yet
61+
if (this.state.type === 'initial') {
62+
return
63+
}
5764

58-
debug?.(`file update detected ${file}, generating hmr patch`)
59-
// NOTE: only single outputOptions is supported here
60-
const hmrOutput = (await this.bundle!.generateHmrPatch([file]))!
65+
if (this.state.type === 'bundling') {
66+
debug?.(
67+
`BUNDLING: file update detected ${file}, retriggering bundle generation`,
68+
)
69+
this.state.abortController.abort()
70+
this.triggerGenerateBundle(this.state)
71+
return
72+
}
73+
if (this.state.type === 'bundle-error') {
74+
debug?.(
75+
`BUNDLE-ERROR: file update detected ${file}, retriggering bundle generation`,
76+
)
77+
this.triggerGenerateBundle(this.state)
78+
return
79+
}
6180

62-
debug?.(`handle hmr output for ${file}`, {
63-
...hmrOutput,
64-
code: typeof hmrOutput.code === 'string' ? '[code]' : hmrOutput.code,
65-
})
66-
if (hmrOutput.fullReload) {
81+
if (
82+
this.state.type === 'bundled' ||
83+
this.state.type === 'generating-hmr-patch'
84+
) {
85+
if (this.state.type === 'bundled') {
86+
debug?.(`BUNDLED: file update detected ${file}, generating HMR patch`)
87+
} else if (this.state.type === 'generating-hmr-patch') {
88+
debug?.(
89+
`GENERATING-HMR-PATCH: file update detected ${file}, regenerating HMR patch`,
90+
)
91+
}
92+
93+
this.state = {
94+
type: 'generating-hmr-patch',
95+
options: this.state.options,
96+
bundle: this.state.bundle,
97+
}
98+
99+
let hmrOutput: HmrOutput
67100
try {
68-
await this.generateBundle(this.rolldownOptions!.output)
101+
// NOTE: only single outputOptions is supported here
102+
hmrOutput = (await this.state.bundle.generateHmrPatch([file]))!
69103
} catch (e) {
70104
// TODO: support multiple errors
71105
server.ws.send({ type: 'error', err: prepareError(e.errors[0]) })
106+
107+
this.state = {
108+
type: 'bundled',
109+
options: this.state.options,
110+
bundle: this.state.bundle,
111+
}
72112
return
73113
}
74114

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

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

167-
// TODO: should this be done for hmr patch file generation?
168-
for (const file of await this.bundle!.watchFiles) {
169-
this.watchFiles.add(file)
170-
}
171-
}
305+
type BundleStateCommonProperties = {
306+
options: RolldownOptions
307+
bundle: RolldownBuild
172308
}

0 commit comments

Comments
 (0)