@@ -19,6 +19,7 @@ import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
1919import { randomUUID } from 'crypto' ;
2020import glob from 'fast-glob' ;
2121import * as fs from 'fs/promises' ;
22+ import { IncomingMessage , ServerResponse } from 'http' ;
2223import type { Config , ConfigOptions , InlinePluginDef } from 'karma' ;
2324import * as path from 'path' ;
2425import { Observable , Subscriber , catchError , defaultIfEmpty , from , of , switchMap } from 'rxjs' ;
@@ -40,6 +41,71 @@ class ApplicationBuildError extends Error {
4041 }
4142}
4243
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+
43109function injectKarmaReporter (
44110 context : BuilderContext ,
45111 buildOptions : BuildOptions ,
@@ -58,9 +124,12 @@ function injectKarmaReporter(
58124 }
59125
60126 class ProgressNotifierReporter {
61- static $inject = [ 'emitter' ] ;
127+ static $inject = [ 'emitter' , LATEST_BUILD_FILES_TOKEN ] ;
62128
63- constructor ( private readonly emitter : KarmaEmitter ) {
129+ constructor (
130+ private readonly emitter : KarmaEmitter ,
131+ private readonly latestBuildFiles : LatestBuildFiles ,
132+ ) {
64133 this . startWatchingBuild ( ) ;
65134 }
66135
@@ -81,6 +150,14 @@ function injectKarmaReporter(
81150 buildOutput . kind === ResultKind . Incremental ||
82151 buildOutput . kind === ResultKind . Full
83152 ) {
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+ }
84161 await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
85162 this . emitter . refreshFiles ( ) ;
86163 }
@@ -237,6 +314,7 @@ async function initializeApplication(
237314 : undefined ;
238315
239316 const buildOptions : BuildOptions = {
317+ assets : options . assets ,
240318 entryPoints,
241319 tsConfig : options . tsConfig ,
242320 outputPath,
@@ -293,7 +371,6 @@ async function initializeApplication(
293371 } ,
294372 ) ;
295373 }
296-
297374 karmaOptions . files . push (
298375 // Serve remaining JS on page load, these are the test entrypoints.
299376 { pattern : `${ outputPath } /*.js` , type : 'module' , watched : false } ,
@@ -313,8 +390,9 @@ async function initializeApplication(
313390 // Remove the webpack plugin/framework:
314391 // Alternative would be to make the Karma plugin "smart" but that's a tall order
315392 // 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 (
318396 ( plugin : string | InlinePluginDef ) => {
319397 if ( typeof plugin === 'string' ) {
320398 return plugin !== 'framework:@angular-devkit/build-angular' ;
@@ -323,16 +401,21 @@ async function initializeApplication(
323401 return ! plugin [ 'framework:@angular-devkit/build-angular' ] ;
324402 } ,
325403 ) ;
326- parsedKarmaConfig . frameworks = parsedKarmaConfig . frameworks ?. filter (
404+ parsedKarmaConfig . frameworks ??= [ ] ;
405+ parsedKarmaConfig . frameworks = parsedKarmaConfig . frameworks . filter (
327406 ( framework : string ) => framework !== '@angular-devkit/build-angular' ,
328407 ) ;
329- const pluginLengthAfter = ( parsedKarmaConfig . plugins ?? [ ] ) . length ;
408+ const pluginLengthAfter = parsedKarmaConfig . plugins . length ;
330409 if ( pluginLengthBefore !== pluginLengthAfter ) {
331410 context . logger . warn (
332411 `Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.` ,
333412 ) ;
334413 }
335414
415+ parsedKarmaConfig . plugins . push ( AngularAssetsMiddleware . createPlugin ( buildOutput ) ) ;
416+ parsedKarmaConfig . middleware ??= [ ] ;
417+ parsedKarmaConfig . middleware . push ( AngularAssetsMiddleware . NAME ) ;
418+
336419 // When using code-coverage, auto-add karma-coverage.
337420 // This was done as part of the karma plugin for webpack.
338421 if (
0 commit comments