@@ -16,7 +16,14 @@ import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/ut
16
16
import { shouldWatchRoot } from '../../utils/environment-options' ;
17
17
import { NormalizedCachedOptions } from '../../utils/normalize-cache' ;
18
18
import { NormalizedApplicationBuildOptions , NormalizedOutputOptions } from './options' ;
19
- import { ComponentUpdateResult , FullResult , Result , ResultKind , ResultMessage } from './results' ;
19
+ import {
20
+ ComponentUpdateResult ,
21
+ FullResult ,
22
+ IncrementalResult ,
23
+ Result ,
24
+ ResultKind ,
25
+ ResultMessage ,
26
+ } from './results' ;
20
27
21
28
// Watch workspace for package manager changes
22
29
const packageWatchFiles = [
@@ -49,6 +56,7 @@ export async function* runEsBuildBuildAction(
49
56
clearScreen ?: boolean ;
50
57
colors ?: boolean ;
51
58
jsonLogs ?: boolean ;
59
+ incrementalResults ?: boolean ;
52
60
} ,
53
61
) : AsyncIterable < Result > {
54
62
const {
@@ -65,6 +73,7 @@ export async function* runEsBuildBuildAction(
65
73
preserveSymlinks,
66
74
colors,
67
75
jsonLogs,
76
+ incrementalResults,
68
77
} = options ;
69
78
70
79
const withProgress : typeof withSpinner = progress ? withSpinner : withNoProgress ;
@@ -135,7 +144,7 @@ export async function* runEsBuildBuildAction(
135
144
// Output the first build results after setting up the watcher to ensure that any code executed
136
145
// higher in the iterator call stack will trigger the watcher. This is particularly relevant for
137
146
// unit tests which execute the builder and modify the file system programmatically.
138
- yield await emitOutputResult ( result , outputOptions ) ;
147
+ yield emitOutputResult ( result , outputOptions ) ;
139
148
140
149
// Finish if watch mode is not enabled
141
150
if ( ! watcher ) {
@@ -162,9 +171,8 @@ export async function* runEsBuildBuildAction(
162
171
// Clear removed files from current watch files
163
172
changes . removed . forEach ( ( removedPath ) => currentWatchFiles . delete ( removedPath ) ) ;
164
173
165
- result = await withProgress ( 'Changes detected. Rebuilding...' , ( ) =>
166
- action ( result . createRebuildState ( changes ) ) ,
167
- ) ;
174
+ const rebuildState = result . createRebuildState ( changes ) ;
175
+ result = await withProgress ( 'Changes detected. Rebuilding...' , ( ) => action ( rebuildState ) ) ;
168
176
169
177
// Log all diagnostic (error/warning/logs) messages
170
178
await logMessages ( logger , result , colors , jsonLogs ) ;
@@ -188,7 +196,11 @@ export async function* runEsBuildBuildAction(
188
196
watcher . remove ( [ ...staleWatchFiles ] ) ;
189
197
}
190
198
191
- yield await emitOutputResult ( result , outputOptions ) ;
199
+ yield emitOutputResult (
200
+ result ,
201
+ outputOptions ,
202
+ incrementalResults ? rebuildState . previousOutputInfo : undefined ,
203
+ ) ;
192
204
}
193
205
} finally {
194
206
// Stop the watcher and cleanup incremental rebuild state
@@ -198,7 +210,7 @@ export async function* runEsBuildBuildAction(
198
210
}
199
211
}
200
212
201
- async function emitOutputResult (
213
+ function emitOutputResult (
202
214
{
203
215
outputFiles,
204
216
assetFiles,
@@ -210,7 +222,8 @@ async function emitOutputResult(
210
222
templateUpdates,
211
223
} : ExecutionResult ,
212
224
outputOptions : NormalizedApplicationBuildOptions [ 'outputOptions' ] ,
213
- ) : Promise < Result > {
225
+ previousOutputInfo ?: ReadonlyMap < string , { hash : string ; type : BuildOutputFileType } > ,
226
+ ) : Result {
214
227
if ( errors . length > 0 ) {
215
228
return {
216
229
kind : ResultKind . Failure ,
@@ -222,11 +235,12 @@ async function emitOutputResult(
222
235
} ;
223
236
}
224
237
225
- // Template updates only exist if no other changes have occurred
226
- if ( templateUpdates ?. size ) {
238
+ // Template updates only exist if no other JS changes have occurred
239
+ const hasTemplateUpdates = ! ! templateUpdates ?. size ;
240
+ if ( hasTemplateUpdates ) {
227
241
const updateResult : ComponentUpdateResult = {
228
242
kind : ResultKind . ComponentUpdate ,
229
- updates : Array . from ( templateUpdates ) . map ( ( [ id , content ] ) => ( {
243
+ updates : Array . from ( templateUpdates , ( [ id , content ] ) => ( {
230
244
type : 'template' ,
231
245
id,
232
246
content,
@@ -236,6 +250,72 @@ async function emitOutputResult(
236
250
return updateResult ;
237
251
}
238
252
253
+ // Use an incremental result if previous output information is available
254
+ if ( previousOutputInfo ) {
255
+ const incrementalResult : IncrementalResult = {
256
+ kind : ResultKind . Incremental ,
257
+ warnings : warnings as ResultMessage [ ] ,
258
+ added : [ ] ,
259
+ removed : [ ] ,
260
+ modified : [ ] ,
261
+ files : { } ,
262
+ detail : {
263
+ externalMetadata,
264
+ htmlIndexPath,
265
+ htmlBaseHref,
266
+ outputOptions,
267
+ } ,
268
+ } ;
269
+
270
+ // Initially assume all previous output files have been removed
271
+ const removedOutputFiles = new Map ( previousOutputInfo ) ;
272
+
273
+ for ( const file of outputFiles ) {
274
+ removedOutputFiles . delete ( file . path ) ;
275
+
276
+ const previousHash = previousOutputInfo . get ( file . path ) ?. hash ;
277
+ let needFile = false ;
278
+ if ( previousHash === undefined ) {
279
+ needFile = true ;
280
+ incrementalResult . added . push ( file . path ) ;
281
+ } else if ( previousHash !== file . hash ) {
282
+ needFile = true ;
283
+ incrementalResult . modified . push ( file . path ) ;
284
+ }
285
+
286
+ if ( needFile ) {
287
+ incrementalResult . files [ file . path ] = {
288
+ type : file . type ,
289
+ contents : file . contents ,
290
+ origin : 'memory' ,
291
+ hash : file . hash ,
292
+ } ;
293
+ }
294
+ }
295
+
296
+ // Include the removed output files
297
+ incrementalResult . removed . push (
298
+ ...Array . from ( removedOutputFiles , ( [ file , { type } ] ) => ( {
299
+ path : file ,
300
+ type,
301
+ } ) ) ,
302
+ ) ;
303
+
304
+ // Always consider asset files as added to ensure new/modified assets are available.
305
+ // TODO: Consider more comprehensive asset analysis.
306
+ for ( const file of assetFiles ) {
307
+ incrementalResult . added . push ( file . destination ) ;
308
+ incrementalResult . files [ file . destination ] = {
309
+ type : BuildOutputFileType . Browser ,
310
+ inputPath : file . source ,
311
+ origin : 'disk' ,
312
+ } ;
313
+ }
314
+
315
+ return incrementalResult ;
316
+ }
317
+
318
+ // Otherwise, use a full result
239
319
const result : FullResult = {
240
320
kind : ResultKind . Full ,
241
321
warnings : warnings as ResultMessage [ ] ,
0 commit comments