@@ -19,6 +19,7 @@ import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
19
19
import { randomUUID } from 'crypto' ;
20
20
import glob from 'fast-glob' ;
21
21
import * as fs from 'fs/promises' ;
22
+ import { IncomingMessage , ServerResponse } from 'http' ;
22
23
import type { Config , ConfigOptions , InlinePluginDef } from 'karma' ;
23
24
import * as path from 'path' ;
24
25
import { Observable , Subscriber , catchError , defaultIfEmpty , from , of , switchMap } from 'rxjs' ;
@@ -40,6 +41,71 @@ class ApplicationBuildError extends Error {
40
41
}
41
42
}
42
43
44
+ interface ServeFileFunction {
45
+ (
46
+ filepath : string ,
47
+ rangeHeader : string | string [ ] | undefined ,
48
+ response : ServerResponse ,
49
+ transform ?: ( c : string | Uint8Array ) => string | Uint8Array ,
50
+ content ?: string | Uint8Array ,
51
+ doNotCache ?: boolean ,
52
+ ) : void ;
53
+ }
54
+
55
+ interface LatestBuildFiles {
56
+ files : Record < string , ResultFile | undefined > ;
57
+ }
58
+
59
+ const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles' ;
60
+
61
+ class AngularAssetsMiddleware {
62
+ static readonly $inject = [ 'serveFile' , LATEST_BUILD_FILES_TOKEN ] ;
63
+
64
+ static readonly NAME = 'angular-test-assets' ;
65
+
66
+ constructor (
67
+ private readonly serveFile : ServeFileFunction ,
68
+ private readonly latestBuildFiles : LatestBuildFiles ,
69
+ ) { }
70
+
71
+ handle ( req : IncomingMessage , res : ServerResponse , next : ( err ?: unknown ) => unknown ) {
72
+ let err = null ;
73
+ try {
74
+ const url = new URL ( `http://${ req . headers [ 'host' ] } ${ req . url } ` ) ;
75
+ const file = this . latestBuildFiles . files [ url . pathname . slice ( 1 ) ] ;
76
+
77
+ if ( file ?. origin === 'disk' ) {
78
+ this . serveFile ( file . inputPath , undefined , res ) ;
79
+
80
+ return ;
81
+ } else if ( file ?. origin === 'memory' ) {
82
+ // Include pathname to help with Content-Type headers.
83
+ this . serveFile ( `/unused/${ url . pathname } ` , undefined , res , undefined , file . contents , true ) ;
84
+
85
+ return ;
86
+ }
87
+ } catch ( e ) {
88
+ err = e ;
89
+ }
90
+ next ( err ) ;
91
+ }
92
+
93
+ static createPlugin ( initialFiles : LatestBuildFiles ) : InlinePluginDef {
94
+ return {
95
+ [ LATEST_BUILD_FILES_TOKEN ] : [ 'value' , { files : { ...initialFiles . files } } ] ,
96
+
97
+ [ `middleware:${ AngularAssetsMiddleware . NAME } ` ] : [
98
+ 'factory' ,
99
+ Object . assign ( ( ...args : ConstructorParameters < typeof AngularAssetsMiddleware > ) => {
100
+ const inst = new AngularAssetsMiddleware ( ...args ) ;
101
+
102
+ return inst . handle . bind ( inst ) ;
103
+ } , AngularAssetsMiddleware ) ,
104
+ ] ,
105
+ } ;
106
+ }
107
+ }
108
+
43
109
function injectKarmaReporter (
44
110
context : BuilderContext ,
45
111
buildOptions : BuildOptions ,
@@ -58,9 +124,12 @@ function injectKarmaReporter(
58
124
}
59
125
60
126
class ProgressNotifierReporter {
61
- static $inject = [ 'emitter' ] ;
127
+ static $inject = [ 'emitter' , LATEST_BUILD_FILES_TOKEN ] ;
62
128
63
- constructor ( private readonly emitter : KarmaEmitter ) {
129
+ constructor (
130
+ private readonly emitter : KarmaEmitter ,
131
+ private readonly latestBuildFiles : LatestBuildFiles ,
132
+ ) {
64
133
this . startWatchingBuild ( ) ;
65
134
}
66
135
@@ -81,6 +150,14 @@ function injectKarmaReporter(
81
150
buildOutput . kind === ResultKind . Incremental ||
82
151
buildOutput . kind === ResultKind . Full
83
152
) {
153
+ if ( buildOutput . kind === ResultKind . Full ) {
154
+ this . latestBuildFiles . files = buildOutput . files ;
155
+ } else {
156
+ this . latestBuildFiles . files = {
157
+ ...this . latestBuildFiles . files ,
158
+ ...buildOutput . files ,
159
+ } ;
160
+ }
84
161
await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
85
162
this . emitter . refreshFiles ( ) ;
86
163
}
@@ -237,6 +314,7 @@ async function initializeApplication(
237
314
: undefined ;
238
315
239
316
const buildOptions : BuildOptions = {
317
+ assets : options . assets ,
240
318
entryPoints,
241
319
tsConfig : options . tsConfig ,
242
320
outputPath,
@@ -293,7 +371,6 @@ async function initializeApplication(
293
371
} ,
294
372
) ;
295
373
}
296
-
297
374
karmaOptions . files . push (
298
375
// Serve remaining JS on page load, these are the test entrypoints.
299
376
{ pattern : `${ outputPath } /*.js` , type : 'module' , watched : false } ,
@@ -313,8 +390,9 @@ async function initializeApplication(
313
390
// Remove the webpack plugin/framework:
314
391
// Alternative would be to make the Karma plugin "smart" but that's a tall order
315
392
// with managing unneeded imports etc..
316
- const pluginLengthBefore = ( parsedKarmaConfig . plugins ?? [ ] ) . length ;
317
- parsedKarmaConfig . plugins = ( parsedKarmaConfig . plugins ?? [ ] ) . filter (
393
+ parsedKarmaConfig . plugins ??= [ ] ;
394
+ const pluginLengthBefore = parsedKarmaConfig . plugins . length ;
395
+ parsedKarmaConfig . plugins = parsedKarmaConfig . plugins . filter (
318
396
( plugin : string | InlinePluginDef ) => {
319
397
if ( typeof plugin === 'string' ) {
320
398
return plugin !== 'framework:@angular-devkit/build-angular' ;
@@ -323,16 +401,21 @@ async function initializeApplication(
323
401
return ! plugin [ 'framework:@angular-devkit/build-angular' ] ;
324
402
} ,
325
403
) ;
326
- parsedKarmaConfig . frameworks = parsedKarmaConfig . frameworks ?. filter (
404
+ parsedKarmaConfig . frameworks ??= [ ] ;
405
+ parsedKarmaConfig . frameworks = parsedKarmaConfig . frameworks . filter (
327
406
( framework : string ) => framework !== '@angular-devkit/build-angular' ,
328
407
) ;
329
- const pluginLengthAfter = ( parsedKarmaConfig . plugins ?? [ ] ) . length ;
408
+ const pluginLengthAfter = parsedKarmaConfig . plugins . length ;
330
409
if ( pluginLengthBefore !== pluginLengthAfter ) {
331
410
context . logger . warn (
332
411
`Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.` ,
333
412
) ;
334
413
}
335
414
415
+ parsedKarmaConfig . plugins . push ( AngularAssetsMiddleware . createPlugin ( buildOutput ) ) ;
416
+ parsedKarmaConfig . middleware ??= [ ] ;
417
+ parsedKarmaConfig . middleware . push ( AngularAssetsMiddleware . NAME ) ;
418
+
336
419
// When using code-coverage, auto-add karma-coverage.
337
420
// This was done as part of the karma plugin for webpack.
338
421
if (
0 commit comments