99import { BuildOutputFileType } from '@angular/build' ;
1010import {
1111 ApplicationBuilderInternalOptions ,
12+ Result ,
1213 ResultFile ,
1314 ResultKind ,
1415 buildApplicationInternal ,
@@ -42,6 +43,7 @@ class ApplicationBuildError extends Error {
4243function injectKarmaReporter (
4344 context : BuilderContext ,
4445 buildOptions : BuildOptions ,
46+ buildIterator : AsyncIterator < Result > ,
4547 karmaConfig : Config & ConfigOptions ,
4648 subscriber : Subscriber < BuilderOutput > ,
4749) {
@@ -64,13 +66,15 @@ function injectKarmaReporter(
6466
6567 private startWatchingBuild ( ) {
6668 void ( async ( ) => {
67- for await ( const buildOutput of buildApplicationInternal (
68- {
69- ...buildOptions ,
70- watch : true ,
71- } ,
72- context ,
73- ) ) {
69+ // This is effectively "for await of but skip what's already consumed".
70+ let isDone = false ; // to mark the loop condition as "not constant".
71+ while ( ! isDone ) {
72+ const { done, value : buildOutput } = await buildIterator . next ( ) ;
73+ if ( done ) {
74+ isDone = true ;
75+ break ;
76+ }
77+
7478 if ( buildOutput . kind === ResultKind . Failure ) {
7579 subscriber . next ( { success : false , message : 'Build failed' } ) ;
7680 } else if (
@@ -121,12 +125,12 @@ export function execute(
121125) : Observable < BuilderOutput > {
122126 return from ( initializeApplication ( options , context , karmaOptions , transforms ) ) . pipe (
123127 switchMap (
124- ( [ karma , karmaConfig , buildOptions ] ) =>
128+ ( [ karma , karmaConfig , buildOptions , buildIterator ] ) =>
125129 new Observable < BuilderOutput > ( ( subscriber ) => {
126130 // If `--watch` is explicitly enabled or if we are keeping the Karma
127131 // process running, we should hook Karma into the build.
128- if ( options . watch ?? ! karmaConfig . singleRun ) {
129- injectKarmaReporter ( context , buildOptions , karmaConfig , subscriber ) ;
132+ if ( buildIterator ) {
133+ injectKarmaReporter ( context , buildOptions , buildIterator , karmaConfig , subscriber ) ;
130134 }
131135
132136 // Complete the observable once the Karma server returns.
@@ -199,7 +203,9 @@ async function initializeApplication(
199203 webpackConfiguration ?: ExecutionTransformer < Configuration > ;
200204 karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
201205 } = { } ,
202- ) : Promise < [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions ] > {
206+ ) : Promise <
207+ [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions , AsyncIterator < Result > | null ]
208+ > {
203209 if ( transforms . webpackConfiguration ) {
204210 context . logger . warn (
205211 `This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.` ,
@@ -247,10 +253,14 @@ async function initializeApplication(
247253 styles : options . styles ,
248254 polyfills : normalizePolyfills ( options . polyfills ) ,
249255 webWorkerTsConfig : options . webWorkerTsConfig ,
256+ watch : options . watch ?? ! karmaOptions . singleRun ,
250257 } ;
251258
252259 // Build tests with `application` builder, using test files as entry points.
253- const buildOutput = await first ( buildApplicationInternal ( buildOptions , context ) ) ;
260+ const [ buildOutput , buildIterator ] = await first (
261+ buildApplicationInternal ( buildOptions , context ) ,
262+ { cancel : ! buildOptions . watch } ,
263+ ) ;
254264 if ( buildOutput . kind === ResultKind . Failure ) {
255265 throw new ApplicationBuildError ( 'Build failed' ) ;
256266 } else if ( buildOutput . kind !== ResultKind . Full ) {
@@ -265,28 +275,33 @@ async function initializeApplication(
265275 karmaOptions . files ??= [ ] ;
266276 karmaOptions . files . push (
267277 // Serve polyfills first.
268- { pattern : `${ outputPath } /polyfills.js` , type : 'module' } ,
278+ { pattern : `${ outputPath } /polyfills.js` , type : 'module' , watched : false } ,
269279 // Serve global setup script.
270- { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' } ,
280+ { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' , watched : false } ,
271281 // Serve all source maps.
272- { pattern : `${ outputPath } /*.map` , included : false } ,
282+ { pattern : `${ outputPath } /*.map` , included : false , watched : false } ,
273283 ) ;
274284
275285 if ( hasChunkOrWorkerFiles ( buildOutput . files ) ) {
276286 karmaOptions . files . push (
277287 // Allow loading of chunk-* files but don't include them all on load.
278- { pattern : `${ outputPath } /{chunk,worker}-*.js` , type : 'module' , included : false } ,
288+ {
289+ pattern : `${ outputPath } /{chunk,worker}-*.js` ,
290+ type : 'module' ,
291+ included : false ,
292+ watched : false ,
293+ } ,
279294 ) ;
280295 }
281296
282297 karmaOptions . files . push (
283298 // Serve remaining JS on page load, these are the test entrypoints.
284- { pattern : `${ outputPath } /*.js` , type : 'module' } ,
299+ { pattern : `${ outputPath } /*.js` , type : 'module' , watched : false } ,
285300 ) ;
286301
287302 if ( options . styles ?. length ) {
288303 // Serve CSS outputs on page load, these are the global styles.
289- karmaOptions . files . push ( { pattern : `${ outputPath } /*.css` , type : 'css' } ) ;
304+ karmaOptions . files . push ( { pattern : `${ outputPath } /*.css` , type : 'css' , watched : false } ) ;
290305 }
291306
292307 const parsedKarmaConfig : Config & ConfigOptions = await karma . config . parseConfig (
@@ -327,7 +342,7 @@ async function initializeApplication(
327342 parsedKarmaConfig . reporters = ( parsedKarmaConfig . reporters ?? [ ] ) . concat ( [ 'coverage' ] ) ;
328343 }
329344
330- return [ karma , parsedKarmaConfig , buildOptions ] ;
345+ return [ karma , parsedKarmaConfig , buildOptions , buildIterator ] ;
331346}
332347
333348function hasChunkOrWorkerFiles ( files : Record < string , unknown > ) : boolean {
@@ -364,9 +379,22 @@ export async function writeTestFiles(files: Record<string, ResultFile>, testDir:
364379}
365380
366381/** Returns the first item yielded by the given generator and cancels the execution. */
367- async function first < T > ( generator : AsyncIterable < T > ) : Promise < T > {
382+ async function first < T > (
383+ generator : AsyncIterable < T > ,
384+ { cancel } : { cancel : boolean } ,
385+ ) : Promise < [ T , AsyncIterator < T > | null ] > {
386+ if ( ! cancel ) {
387+ const iterator : AsyncIterator < T > = generator [ Symbol . asyncIterator ] ( ) ;
388+ const firstValue = await iterator . next ( ) ;
389+ if ( firstValue . done ) {
390+ throw new Error ( 'Expected generator to emit at least once.' ) ;
391+ }
392+
393+ return [ firstValue . value , iterator ] ;
394+ }
395+
368396 for await ( const value of generator ) {
369- return value ;
397+ return [ value , null ] ;
370398 }
371399
372400 throw new Error ( 'Expected generator to emit at least once.' ) ;
0 commit comments