88
99import { BuilderContext , BuilderOutput , createBuilder } from '@angular-devkit/architect' ;
1010import * as assert from 'assert' ;
11- import type { Message , OutputFile } from 'esbuild' ;
11+ import type { BuildInvalidate , BuildOptions , Message , OutputFile } from 'esbuild' ;
1212import * as fs from 'fs/promises' ;
1313import * as path from 'path' ;
1414import { deleteOutputDir } from '../../utils' ;
@@ -19,18 +19,56 @@ import { FileInfo } from '../../utils/index-file/augment-index-html';
1919import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator' ;
2020import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker' ;
2121import { getSupportedBrowsers } from '../../utils/supported-browsers' ;
22- import { createCompilerPlugin } from './compiler-plugin' ;
22+ import { SourceFileCache , createCompilerPlugin } from './compiler-plugin' ;
2323import { bundle , logMessages } from './esbuild' ;
2424import { logExperimentalWarnings } from './experimental-warnings' ;
2525import { NormalizedBrowserOptions , normalizeOptions } from './options' ;
2626import { Schema as BrowserBuilderOptions } from './schema' ;
2727import { bundleStylesheetText } from './stylesheets' ;
28- import { createWatcher } from './watcher' ;
28+ import { ChangedFiles , createWatcher } from './watcher' ;
29+
30+ interface RebuildState {
31+ codeRebuild ?: BuildInvalidate ;
32+ codeBundleCache ?: SourceFileCache ;
33+ fileChanges : ChangedFiles ;
34+ }
35+
36+ /**
37+ * Represents the result of a single builder execute call.
38+ */
39+ class ExecutionResult {
40+ constructor (
41+ private success : boolean ,
42+ private codeRebuild ?: BuildInvalidate ,
43+ private codeBundleCache ?: SourceFileCache ,
44+ ) { }
45+
46+ get output ( ) {
47+ return {
48+ success : this . success ,
49+ } ;
50+ }
51+
52+ createRebuildState ( fileChanges : ChangedFiles ) : RebuildState {
53+ this . codeBundleCache ?. invalidate ( [ ...fileChanges . modified , ...fileChanges . removed ] ) ;
54+
55+ return {
56+ codeRebuild : this . codeRebuild ,
57+ codeBundleCache : this . codeBundleCache ,
58+ fileChanges,
59+ } ;
60+ }
61+
62+ dispose ( ) : void {
63+ this . codeRebuild ?. dispose ( ) ;
64+ }
65+ }
2966
3067async function execute (
3168 options : NormalizedBrowserOptions ,
3269 context : BuilderContext ,
33- ) : Promise < BuilderOutput > {
70+ rebuildState ?: RebuildState ,
71+ ) : Promise < ExecutionResult > {
3472 const startTime = Date . now ( ) ;
3573
3674 const {
@@ -47,9 +85,13 @@ async function execute(
4785 getSupportedBrowsers ( projectRoot , context . logger ) ,
4886 ) ;
4987
88+ const codeBundleCache = options . watch
89+ ? rebuildState ?. codeBundleCache ?? new SourceFileCache ( )
90+ : undefined ;
91+
5092 const [ codeResults , styleResults ] = await Promise . all ( [
5193 // Execute esbuild to bundle the application code
52- bundleCode ( options , target ) ,
94+ bundle ( rebuildState ?. codeRebuild ?? createCodeBundleOptions ( options , target , codeBundleCache ) ) ,
5395 // Execute esbuild to bundle the global stylesheets
5496 bundleGlobalStylesheets ( options , target ) ,
5597 ] ) ;
@@ -62,7 +104,7 @@ async function execute(
62104
63105 // Return if the bundling failed to generate output files or there are errors
64106 if ( ! codeResults . outputFiles || codeResults . errors . length ) {
65- return { success : false } ;
107+ return new ExecutionResult ( false , rebuildState ?. codeRebuild , codeBundleCache ) ;
66108 }
67109
68110 // Structure the code bundling output files
@@ -93,7 +135,7 @@ async function execute(
93135
94136 // Return if the global stylesheet bundling has errors
95137 if ( styleResults . errors . length ) {
96- return { success : false } ;
138+ return new ExecutionResult ( false , codeResults . rebuild , codeBundleCache ) ;
97139 }
98140
99141 // Generate index HTML file
@@ -160,13 +202,13 @@ async function execute(
160202 } catch ( error ) {
161203 context . logger . error ( error instanceof Error ? error . message : `${ error } ` ) ;
162204
163- return { success : false } ;
205+ return new ExecutionResult ( false , codeResults . rebuild , codeBundleCache ) ;
164206 }
165207 }
166208
167209 context . logger . info ( `Complete. [${ ( Date . now ( ) - startTime ) / 1000 } seconds]` ) ;
168210
169- return { success : true } ;
211+ return new ExecutionResult ( true , codeResults . rebuild , codeBundleCache ) ;
170212}
171213
172214function createOutputFileFromText ( path : string , text : string ) : OutputFile {
@@ -179,7 +221,11 @@ function createOutputFileFromText(path: string, text: string): OutputFile {
179221 } ;
180222}
181223
182- async function bundleCode ( options : NormalizedBrowserOptions , target : string [ ] ) {
224+ function createCodeBundleOptions (
225+ options : NormalizedBrowserOptions ,
226+ target : string [ ] ,
227+ sourceFileCache ?: SourceFileCache ,
228+ ) : BuildOptions {
183229 const {
184230 workspaceRoot,
185231 entryPoints,
@@ -194,9 +240,10 @@ async function bundleCode(options: NormalizedBrowserOptions, target: string[]) {
194240 advancedOptimizations,
195241 } = options ;
196242
197- return bundle ( {
243+ return {
198244 absWorkingDir : workspaceRoot ,
199245 bundle : true ,
246+ incremental : options . watch ,
200247 format : 'esm' ,
201248 entryPoints,
202249 entryNames : outputNames . bundles ,
@@ -234,6 +281,7 @@ async function bundleCode(options: NormalizedBrowserOptions, target: string[]) {
234281 tsconfig,
235282 advancedOptimizations,
236283 fileReplacements,
284+ sourceFileCache,
237285 } ,
238286 // Component stylesheet options
239287 {
@@ -255,7 +303,7 @@ async function bundleCode(options: NormalizedBrowserOptions, target: string[]) {
255303 ...( optimizationOptions . scripts ? { 'ngDevMode' : 'false' } : undefined ) ,
256304 'ngJitMode' : 'false' ,
257305 } ,
258- } ) ;
306+ } ;
259307}
260308
261309async function bundleGlobalStylesheets ( options : NormalizedBrowserOptions , target : string [ ] ) {
@@ -383,13 +431,16 @@ export async function* buildEsbuildBrowser(
383431 }
384432
385433 // Initial build
386- yield await execute ( normalizedOptions , context ) ;
434+ let result = await execute ( normalizedOptions , context ) ;
435+ yield result . output ;
387436
388437 // Finish if watch mode is not enabled
389438 if ( ! initialOptions . watch ) {
390439 return ;
391440 }
392441
442+ context . logger . info ( 'Watch mode enabled. Watching for file changes...' ) ;
443+
393444 // Setup a watcher
394445 const watcher = createWatcher ( {
395446 polling : typeof initialOptions . poll === 'number' ,
@@ -416,10 +467,14 @@ export async function* buildEsbuildBrowser(
416467 context . logger . info ( changes . toDebugString ( ) ) ;
417468 }
418469
419- yield await execute ( normalizedOptions , context ) ;
470+ result = await execute ( normalizedOptions , context , result . createRebuildState ( changes ) ) ;
471+ yield result . output ;
420472 }
421473 } finally {
474+ // Stop the watcher
422475 await watcher . close ( ) ;
476+ // Cleanup incremental rebuild state
477+ result . dispose ( ) ;
423478 }
424479}
425480
0 commit comments