@@ -16,7 +16,14 @@ import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/ut
1616import { shouldWatchRoot } from '../../utils/environment-options' ;
1717import { NormalizedCachedOptions } from '../../utils/normalize-cache' ;
1818import { 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' ;
2027
2128// Watch workspace for package manager changes
2229const packageWatchFiles = [
@@ -49,6 +56,7 @@ export async function* runEsBuildBuildAction(
4956 clearScreen ?: boolean ;
5057 colors ?: boolean ;
5158 jsonLogs ?: boolean ;
59+ incrementalResults ?: boolean ;
5260 } ,
5361) : AsyncIterable < Result > {
5462 const {
@@ -65,6 +73,7 @@ export async function* runEsBuildBuildAction(
6573 preserveSymlinks,
6674 colors,
6775 jsonLogs,
76+ incrementalResults,
6877 } = options ;
6978
7079 const withProgress : typeof withSpinner = progress ? withSpinner : withNoProgress ;
@@ -135,7 +144,7 @@ export async function* runEsBuildBuildAction(
135144 // Output the first build results after setting up the watcher to ensure that any code executed
136145 // higher in the iterator call stack will trigger the watcher. This is particularly relevant for
137146 // unit tests which execute the builder and modify the file system programmatically.
138- yield await emitOutputResult ( result , outputOptions ) ;
147+ yield * emitOutputResults ( result , outputOptions ) ;
139148
140149 // Finish if watch mode is not enabled
141150 if ( ! watcher ) {
@@ -162,9 +171,8 @@ export async function* runEsBuildBuildAction(
162171 // Clear removed files from current watch files
163172 changes . removed . forEach ( ( removedPath ) => currentWatchFiles . delete ( removedPath ) ) ;
164173
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 ) ) ;
168176
169177 // Log all diagnostic (error/warning/logs) messages
170178 await logMessages ( logger , result , colors , jsonLogs ) ;
@@ -188,7 +196,11 @@ export async function* runEsBuildBuildAction(
188196 watcher . remove ( [ ...staleWatchFiles ] ) ;
189197 }
190198
191- yield await emitOutputResult ( result , outputOptions ) ;
199+ yield * emitOutputResults (
200+ result ,
201+ outputOptions ,
202+ incrementalResults ? rebuildState . previousOutputHashes : undefined ,
203+ ) ;
192204 }
193205 } finally {
194206 // Stop the watcher and cleanup incremental rebuild state
@@ -198,7 +210,7 @@ export async function* runEsBuildBuildAction(
198210 }
199211}
200212
201- async function emitOutputResult (
213+ async function * emitOutputResults (
202214 {
203215 outputFiles,
204216 assetFiles,
@@ -210,32 +222,113 @@ async function emitOutputResult(
210222 templateUpdates,
211223 } : ExecutionResult ,
212224 outputOptions : NormalizedApplicationBuildOptions [ 'outputOptions' ] ,
213- ) : Promise < Result > {
225+ previousOutputHashes ?: ReadonlyMap < string , string > ,
226+ ) : AsyncIterable < Result > {
214227 if ( errors . length > 0 ) {
215- return {
228+ yield {
216229 kind : ResultKind . Failure ,
217230 errors : errors as ResultMessage [ ] ,
218231 warnings : warnings as ResultMessage [ ] ,
219232 detail : {
220233 outputOptions,
221234 } ,
222235 } ;
236+
237+ // Only one failure result if there are errors
238+ return ;
223239 }
224240
225- // Template updates only exist if no other changes have occurred
226- if ( templateUpdates ?. size ) {
241+ // Template updates only exist if no other JS changes have occurred
242+ const hasTemplateUpdates = ! ! templateUpdates ?. size ;
243+ if ( hasTemplateUpdates ) {
227244 const updateResult : ComponentUpdateResult = {
228245 kind : ResultKind . ComponentUpdate ,
229- updates : Array . from ( templateUpdates ) . map ( ( [ id , content ] ) => ( {
246+ updates : Array . from ( templateUpdates , ( [ id , content ] ) => ( {
230247 type : 'template' ,
231248 id,
232249 content,
233250 } ) ) ,
234251 } ;
235252
236- return updateResult ;
253+ yield updateResult ;
254+ }
255+
256+ // Use an incremental result if previous output information is available
257+ if ( previousOutputHashes ) {
258+ const incrementalResult : IncrementalResult = {
259+ kind : ResultKind . Incremental ,
260+ warnings : warnings as ResultMessage [ ] ,
261+ added : [ ] ,
262+ removed : [ ] ,
263+ modified : [ ] ,
264+ files : { } ,
265+ detail : {
266+ externalMetadata,
267+ htmlIndexPath,
268+ htmlBaseHref,
269+ outputOptions,
270+ } ,
271+ } ;
272+
273+ // Initially assume all previous output files have been removed
274+ const removedOutputFiles = new Set ( previousOutputHashes . keys ( ) ) ;
275+
276+ for ( const file of outputFiles ) {
277+ removedOutputFiles . delete ( file . path ) ;
278+
279+ // Temporarily ignore JS files until Angular compiler plugin refactor to allow
280+ // bypassing application code bundling for template affecting only changes.
281+ // TODO: Remove once refactor is complete.
282+ if ( hasTemplateUpdates && file . path . endsWith ( '.js' ) ) {
283+ continue ;
284+ }
285+
286+ const previousHash = previousOutputHashes . get ( file . path ) ;
287+ let needFile = false ;
288+ if ( previousHash === undefined ) {
289+ needFile = true ;
290+ incrementalResult . added . push ( file . path ) ;
291+ } else if ( previousHash !== file . hash ) {
292+ needFile = true ;
293+ incrementalResult . modified . push ( file . path ) ;
294+ }
295+
296+ if ( needFile ) {
297+ incrementalResult . files [ file . path ] = {
298+ type : file . type ,
299+ contents : file . contents ,
300+ origin : 'memory' ,
301+ hash : file . hash ,
302+ } ;
303+ }
304+ }
305+
306+ // Include the removed output files
307+ incrementalResult . removed . push (
308+ ...Array . from ( removedOutputFiles , ( file ) => ( {
309+ path : file ,
310+ // FIXME: use actual file type
311+ type : BuildOutputFileType . Browser ,
312+ } ) ) ,
313+ ) ;
314+
315+ // Always consider asset files as added to ensure new/modified assets are available.
316+ // TODO: Consider more comprehensive asset analysis.
317+ for ( const file of assetFiles ) {
318+ incrementalResult . added . push ( file . destination ) ;
319+ incrementalResult . files [ file . destination ] = {
320+ type : BuildOutputFileType . Browser ,
321+ inputPath : file . source ,
322+ origin : 'disk' ,
323+ } ;
324+ }
325+
326+ yield incrementalResult ;
327+
328+ return ;
237329 }
238330
331+ // Otherwise, use a full result
239332 const result : FullResult = {
240333 kind : ResultKind . Full ,
241334 warnings : warnings as ResultMessage [ ] ,
@@ -263,5 +356,5 @@ async function emitOutputResult(
263356 } ;
264357 }
265358
266- return result ;
359+ yield result ;
267360}
0 commit comments