8
8
9
9
import { BuildOutputFileType } from '@angular/build' ;
10
10
import {
11
+ ApplicationBuilderInternalOptions ,
11
12
ResultFile ,
12
13
ResultKind ,
13
14
buildApplicationInternal ,
@@ -19,20 +20,95 @@ import glob from 'fast-glob';
19
20
import * as fs from 'fs/promises' ;
20
21
import type { Config , ConfigOptions , InlinePluginDef } from 'karma' ;
21
22
import * 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' ;
23
24
import { Configuration } from 'webpack' ;
24
25
import { ExecutionTransformer } from '../../transforms' ;
25
26
import { OutputHashing } from '../browser-esbuild/schema' ;
26
27
import { findTests } from './find-tests' ;
27
28
import { Schema as KarmaBuilderOptions } from './schema' ;
28
29
30
+ interface BuildOptions extends ApplicationBuilderInternalOptions {
31
+ // We know that it's always a string since we set it.
32
+ outputPath : string ;
33
+ }
34
+
29
35
class ApplicationBuildError extends Error {
30
36
constructor ( message : string ) {
31
37
super ( message ) ;
32
38
this . name = 'ApplicationBuildError' ;
33
39
}
34
40
}
35
41
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
+
36
112
export function execute (
37
113
options : KarmaBuilderOptions ,
38
114
context : BuilderContext ,
@@ -45,8 +121,12 @@ export function execute(
45
121
) : Observable < BuilderOutput > {
46
122
return from ( initializeApplication ( options , context , karmaOptions , transforms ) ) . pipe (
47
123
switchMap (
48
- ( [ karma , karmaConfig ] ) =>
124
+ ( [ karma , karmaConfig , buildOptions ] ) =>
49
125
new Observable < BuilderOutput > ( ( subscriber ) => {
126
+ if ( options . watch ) {
127
+ injectKarmaReporter ( context , buildOptions , karmaConfig , subscriber ) ;
128
+ }
129
+
50
130
// Complete the observable once the Karma server returns.
51
131
const karmaServer = new karma . Server ( karmaConfig as Config , ( exitCode ) => {
52
132
subscriber . next ( { success : exitCode === 0 } ) ;
@@ -122,55 +202,50 @@ async function initializeApplication(
122
202
webpackConfiguration ?: ExecutionTransformer < Configuration > ;
123
203
karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
124
204
} = { } ,
125
- ) : Promise < [ typeof import ( 'karma' ) , Config & ConfigOptions ] > {
205
+ ) : Promise < [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions ] > {
126
206
if ( transforms . webpackConfiguration ) {
127
207
context . logger . warn (
128
208
`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.` ,
129
209
) ;
130
210
}
131
211
132
- const testDir = path . join ( context . workspaceRoot , 'dist/test-out' , randomUUID ( ) ) ;
212
+ const outputPath = path . join ( context . workspaceRoot , 'dist/test-out' , randomUUID ( ) ) ;
133
213
const projectSourceRoot = await getProjectSourceRoot ( context ) ;
134
214
135
215
const [ karma , entryPoints ] = await Promise . all ( [
136
216
import ( 'karma' ) ,
137
217
collectEntrypoints ( options , context , projectSourceRoot ) ,
138
- fs . rm ( testDir , { recursive : true , force : true } ) ,
218
+ fs . rm ( outputPath , { recursive : true , force : true } ) ,
139
219
] ) ;
140
220
141
- const outputPath = testDir ;
142
-
143
221
const instrumentForCoverage = options . codeCoverage
144
222
? createInstrumentationFilter (
145
223
projectSourceRoot ,
146
224
getInstrumentationExcludedPaths ( context . workspaceRoot , options . codeCoverageExclude ?? [ ] ) ,
147
225
)
148
226
: undefined ;
149
227
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
+
150
247
// 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 ) ) ;
174
249
if ( buildOutput . kind === ResultKind . Failure ) {
175
250
throw new ApplicationBuildError ( 'Build failed' ) ;
176
251
} else if ( buildOutput . kind !== ResultKind . Full ) {
@@ -180,24 +255,24 @@ async function initializeApplication(
180
255
}
181
256
182
257
// Write test files
183
- await writeTestFiles ( buildOutput . files , testDir ) ;
258
+ await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
184
259
185
260
karmaOptions . files ??= [ ] ;
186
261
karmaOptions . files . push (
187
262
// Serve polyfills first.
188
- { pattern : `${ testDir } /polyfills.js` , type : 'module' } ,
263
+ { pattern : `${ outputPath } /polyfills.js` , type : 'module' } ,
189
264
// 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 } ,
191
266
) ;
192
267
193
268
karmaOptions . files . push (
194
269
// Serve remaining JS on page load, these are the test entrypoints.
195
- { pattern : `${ testDir } /*.js` , type : 'module' } ,
270
+ { pattern : `${ outputPath } /*.js` , type : 'module' } ,
196
271
) ;
197
272
198
273
if ( options . styles ?. length ) {
199
274
// 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' } ) ;
201
276
}
202
277
203
278
const parsedKarmaConfig : Config & ConfigOptions = await karma . config . parseConfig (
@@ -238,7 +313,7 @@ async function initializeApplication(
238
313
parsedKarmaConfig . reporters = ( parsedKarmaConfig . reporters ?? [ ] ) . concat ( [ 'coverage' ] ) ;
239
314
}
240
315
241
- return [ karma , parsedKarmaConfig ] ;
316
+ return [ karma , parsedKarmaConfig , buildOptions ] ;
242
317
}
243
318
244
319
export async function writeTestFiles ( files : Record < string , ResultFile > , testDir : string ) {
0 commit comments