9
9
import { BuildOutputFileType } from '@angular/build' ;
10
10
import {
11
11
ApplicationBuilderInternalOptions ,
12
+ Result ,
12
13
ResultFile ,
13
14
ResultKind ,
14
15
buildApplicationInternal ,
@@ -42,6 +43,7 @@ class ApplicationBuildError extends Error {
42
43
function injectKarmaReporter (
43
44
context : BuilderContext ,
44
45
buildOptions : BuildOptions ,
46
+ buildIterator : AsyncIterator < Result > ,
45
47
karmaConfig : Config & ConfigOptions ,
46
48
subscriber : Subscriber < BuilderOutput > ,
47
49
) {
@@ -64,13 +66,15 @@ function injectKarmaReporter(
64
66
65
67
private startWatchingBuild ( ) {
66
68
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
+
74
78
if ( buildOutput . kind === ResultKind . Failure ) {
75
79
subscriber . next ( { success : false , message : 'Build failed' } ) ;
76
80
} else if (
@@ -121,12 +125,12 @@ export function execute(
121
125
) : Observable < BuilderOutput > {
122
126
return from ( initializeApplication ( options , context , karmaOptions , transforms ) ) . pipe (
123
127
switchMap (
124
- ( [ karma , karmaConfig , buildOptions ] ) =>
128
+ ( [ karma , karmaConfig , buildOptions , buildIterator ] ) =>
125
129
new Observable < BuilderOutput > ( ( subscriber ) => {
126
130
// If `--watch` is explicitly enabled or if we are keeping the Karma
127
131
// 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 ) ;
130
134
}
131
135
132
136
// Complete the observable once the Karma server returns.
@@ -199,7 +203,9 @@ async function initializeApplication(
199
203
webpackConfiguration ?: ExecutionTransformer < Configuration > ;
200
204
karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
201
205
} = { } ,
202
- ) : Promise < [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions ] > {
206
+ ) : Promise <
207
+ [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions , AsyncIterator < Result > | null ]
208
+ > {
203
209
if ( transforms . webpackConfiguration ) {
204
210
context . logger . warn (
205
211
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.` ,
@@ -247,10 +253,14 @@ async function initializeApplication(
247
253
styles : options . styles ,
248
254
polyfills : normalizePolyfills ( options . polyfills ) ,
249
255
webWorkerTsConfig : options . webWorkerTsConfig ,
256
+ watch : options . watch ?? ! karmaOptions . singleRun ,
250
257
} ;
251
258
252
259
// 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
+ ) ;
254
264
if ( buildOutput . kind === ResultKind . Failure ) {
255
265
throw new ApplicationBuildError ( 'Build failed' ) ;
256
266
} else if ( buildOutput . kind !== ResultKind . Full ) {
@@ -265,28 +275,33 @@ async function initializeApplication(
265
275
karmaOptions . files ??= [ ] ;
266
276
karmaOptions . files . push (
267
277
// Serve polyfills first.
268
- { pattern : `${ outputPath } /polyfills.js` , type : 'module' } ,
278
+ { pattern : `${ outputPath } /polyfills.js` , type : 'module' , watched : false } ,
269
279
// Serve global setup script.
270
- { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' } ,
280
+ { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' , watched : false } ,
271
281
// Serve all source maps.
272
- { pattern : `${ outputPath } /*.map` , included : false } ,
282
+ { pattern : `${ outputPath } /*.map` , included : false , watched : false } ,
273
283
) ;
274
284
275
285
if ( hasChunkOrWorkerFiles ( buildOutput . files ) ) {
276
286
karmaOptions . files . push (
277
287
// 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
+ } ,
279
294
) ;
280
295
}
281
296
282
297
karmaOptions . files . push (
283
298
// 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 } ,
285
300
) ;
286
301
287
302
if ( options . styles ?. length ) {
288
303
// 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 } ) ;
290
305
}
291
306
292
307
const parsedKarmaConfig : Config & ConfigOptions = await karma . config . parseConfig (
@@ -327,7 +342,7 @@ async function initializeApplication(
327
342
parsedKarmaConfig . reporters = ( parsedKarmaConfig . reporters ?? [ ] ) . concat ( [ 'coverage' ] ) ;
328
343
}
329
344
330
- return [ karma , parsedKarmaConfig , buildOptions ] ;
345
+ return [ karma , parsedKarmaConfig , buildOptions , buildIterator ] ;
331
346
}
332
347
333
348
function hasChunkOrWorkerFiles ( files : Record < string , unknown > ) : boolean {
@@ -364,9 +379,22 @@ export async function writeTestFiles(files: Record<string, ResultFile>, testDir:
364
379
}
365
380
366
381
/** 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
+
368
396
for await ( const value of generator ) {
369
- return value ;
397
+ return [ value , null ] ;
370
398
}
371
399
372
400
throw new Error ( 'Expected generator to emit at least once.' ) ;
0 commit comments