88
99import { BuildOutputFileType } from '@angular/build' ;
1010import {
11+ ApplicationBuilderInternalOptions ,
1112 ResultFile ,
1213 ResultKind ,
1314 buildApplicationInternal ,
@@ -19,20 +20,95 @@ import glob from 'fast-glob';
1920import * as fs from 'fs/promises' ;
2021import type { Config , ConfigOptions , InlinePluginDef } from 'karma' ;
2122import * as path from 'path' ;
22- import { Observable , catchError , defaultIfEmpty , from , of , switchMap } from 'rxjs' ;
23+ import { Observable , Subscriber , catchError , defaultIfEmpty , from , of , switchMap } from 'rxjs' ;
2324import { Configuration } from 'webpack' ;
2425import { ExecutionTransformer } from '../../transforms' ;
2526import { OutputHashing } from '../browser-esbuild/schema' ;
2627import { findTests } from './find-tests' ;
2728import { Schema as KarmaBuilderOptions } from './schema' ;
2829
30+ interface BuildOptions extends ApplicationBuilderInternalOptions {
31+ // We know that it's always a string since we set it.
32+ outputPath : string ;
33+ }
34+
2935class ApplicationBuildError extends Error {
3036 constructor ( message : string ) {
3137 super ( message ) ;
3238 this . name = 'ApplicationBuildError' ;
3339 }
3440}
3541
42+ function injectKarmaReporter (
43+ context : BuilderContext ,
44+ buildOptions : BuildOptions ,
45+ karmaConfig : Config & ConfigOptions ,
46+ subscriber : Subscriber < BuilderOutput > ,
47+ ) {
48+ const reporterName = 'angular-progress-notifier' ;
49+
50+ interface RunCompleteInfo {
51+ exitCode : number ;
52+ }
53+
54+ interface KarmaEmitter {
55+ refreshFiles ( ) : void ;
56+ }
57+
58+ class ProgressNotifierReporter {
59+ static $inject = [ 'emitter' ] ;
60+
61+ constructor ( private readonly emitter : KarmaEmitter ) {
62+ this . startWatchingBuild ( ) ;
63+ }
64+
65+ private startWatchingBuild ( ) {
66+ void ( async ( ) => {
67+ for await ( const buildOutput of buildApplicationInternal (
68+ {
69+ ...buildOptions ,
70+ watch : true ,
71+ } ,
72+ context ,
73+ ) ) {
74+ if ( buildOutput . kind === ResultKind . Failure ) {
75+ subscriber . next ( { success : false , message : 'Build failed' } ) ;
76+ } else if (
77+ buildOutput . kind === ResultKind . Incremental ||
78+ buildOutput . kind === ResultKind . Full
79+ ) {
80+ await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
81+ this . emitter . refreshFiles ( ) ;
82+ }
83+ }
84+ } ) ( ) ;
85+ }
86+
87+ onRunComplete = function ( _browsers : unknown , results : RunCompleteInfo ) {
88+ if ( results . exitCode === 0 ) {
89+ subscriber . next ( { success : true } ) ;
90+ } else {
91+ subscriber . next ( { success : false } ) ;
92+ }
93+ } ;
94+ }
95+
96+ karmaConfig . reporters ??= [ ] ;
97+ karmaConfig . reporters . push ( reporterName ) ;
98+
99+ karmaConfig . plugins ??= [ ] ;
100+ karmaConfig . plugins . push ( {
101+ [ `reporter:${ reporterName } ` ] : [
102+ 'factory' ,
103+ Object . assign (
104+ ( ...args : ConstructorParameters < typeof ProgressNotifierReporter > ) =>
105+ new ProgressNotifierReporter ( ...args ) ,
106+ ProgressNotifierReporter ,
107+ ) ,
108+ ] ,
109+ } ) ;
110+ }
111+
36112export function execute (
37113 options : KarmaBuilderOptions ,
38114 context : BuilderContext ,
@@ -45,8 +121,12 @@ export function execute(
45121) : Observable < BuilderOutput > {
46122 return from ( initializeApplication ( options , context , karmaOptions , transforms ) ) . pipe (
47123 switchMap (
48- ( [ karma , karmaConfig ] ) =>
124+ ( [ karma , karmaConfig , buildOptions ] ) =>
49125 new Observable < BuilderOutput > ( ( subscriber ) => {
126+ if ( options . watch ) {
127+ injectKarmaReporter ( context , buildOptions , karmaConfig , subscriber ) ;
128+ }
129+
50130 // Complete the observable once the Karma server returns.
51131 const karmaServer = new karma . Server ( karmaConfig as Config , ( exitCode ) => {
52132 subscriber . next ( { success : exitCode === 0 } ) ;
@@ -122,55 +202,50 @@ async function initializeApplication(
122202 webpackConfiguration ?: ExecutionTransformer < Configuration > ;
123203 karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
124204 } = { } ,
125- ) : Promise < [ typeof import ( 'karma' ) , Config & ConfigOptions ] > {
205+ ) : Promise < [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions ] > {
126206 if ( transforms . webpackConfiguration ) {
127207 context . logger . warn (
128208 `This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.` ,
129209 ) ;
130210 }
131211
132- const testDir = path . join ( context . workspaceRoot , 'dist/test-out' , randomUUID ( ) ) ;
212+ const outputPath = path . join ( context . workspaceRoot , 'dist/test-out' , randomUUID ( ) ) ;
133213 const projectSourceRoot = await getProjectSourceRoot ( context ) ;
134214
135215 const [ karma , entryPoints ] = await Promise . all ( [
136216 import ( 'karma' ) ,
137217 collectEntrypoints ( options , context , projectSourceRoot ) ,
138- fs . rm ( testDir , { recursive : true , force : true } ) ,
218+ fs . rm ( outputPath , { recursive : true , force : true } ) ,
139219 ] ) ;
140220
141- const outputPath = testDir ;
142-
143221 const instrumentForCoverage = options . codeCoverage
144222 ? createInstrumentationFilter (
145223 projectSourceRoot ,
146224 getInstrumentationExcludedPaths ( context . workspaceRoot , options . codeCoverageExclude ?? [ ] ) ,
147225 )
148226 : undefined ;
149227
228+ const buildOptions : BuildOptions = {
229+ entryPoints,
230+ tsConfig : options . tsConfig ,
231+ outputPath,
232+ aot : false ,
233+ index : false ,
234+ outputHashing : OutputHashing . None ,
235+ optimization : false ,
236+ sourceMap : {
237+ scripts : true ,
238+ styles : true ,
239+ vendor : true ,
240+ } ,
241+ instrumentForCoverage,
242+ styles : options . styles ,
243+ polyfills : normalizePolyfills ( options . polyfills ) ,
244+ webWorkerTsConfig : options . webWorkerTsConfig ,
245+ } ;
246+
150247 // Build tests with `application` builder, using test files as entry points.
151- const buildOutput = await first (
152- buildApplicationInternal (
153- {
154- entryPoints,
155- tsConfig : options . tsConfig ,
156- outputPath,
157- aot : false ,
158- index : false ,
159- outputHashing : OutputHashing . None ,
160- optimization : false ,
161- sourceMap : {
162- scripts : true ,
163- styles : true ,
164- vendor : true ,
165- } ,
166- instrumentForCoverage,
167- styles : options . styles ,
168- polyfills : normalizePolyfills ( options . polyfills ) ,
169- webWorkerTsConfig : options . webWorkerTsConfig ,
170- } ,
171- context ,
172- ) ,
173- ) ;
248+ const buildOutput = await first ( buildApplicationInternal ( buildOptions , context ) ) ;
174249 if ( buildOutput . kind === ResultKind . Failure ) {
175250 throw new ApplicationBuildError ( 'Build failed' ) ;
176251 } else if ( buildOutput . kind !== ResultKind . Full ) {
@@ -180,24 +255,24 @@ async function initializeApplication(
180255 }
181256
182257 // Write test files
183- await writeTestFiles ( buildOutput . files , testDir ) ;
258+ await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
184259
185260 karmaOptions . files ??= [ ] ;
186261 karmaOptions . files . push (
187262 // Serve polyfills first.
188- { pattern : `${ testDir } /polyfills.js` , type : 'module' } ,
263+ { pattern : `${ outputPath } /polyfills.js` , type : 'module' } ,
189264 // Allow loading of chunk-* files but don't include them all on load.
190- { pattern : `${ testDir } /{chunk,worker}-*.js` , type : 'module' , included : false } ,
265+ { pattern : `${ outputPath } /{chunk,worker}-*.js` , type : 'module' , included : false } ,
191266 ) ;
192267
193268 karmaOptions . files . push (
194269 // Serve remaining JS on page load, these are the test entrypoints.
195- { pattern : `${ testDir } /*.js` , type : 'module' } ,
270+ { pattern : `${ outputPath } /*.js` , type : 'module' } ,
196271 ) ;
197272
198273 if ( options . styles ?. length ) {
199274 // Serve CSS outputs on page load, these are the global styles.
200- karmaOptions . files . push ( { pattern : `${ testDir } /*.css` , type : 'css' } ) ;
275+ karmaOptions . files . push ( { pattern : `${ outputPath } /*.css` , type : 'css' } ) ;
201276 }
202277
203278 const parsedKarmaConfig : Config & ConfigOptions = await karma . config . parseConfig (
@@ -238,7 +313,7 @@ async function initializeApplication(
238313 parsedKarmaConfig . reporters = ( parsedKarmaConfig . reporters ?? [ ] ) . concat ( [ 'coverage' ] ) ;
239314 }
240315
241- return [ karma , parsedKarmaConfig ] ;
316+ return [ karma , parsedKarmaConfig , buildOptions ] ;
242317}
243318
244319export async function writeTestFiles ( files : Record < string , ResultFile > , testDir : string ) {
0 commit comments