77 */
88
99import { purgeStaleBuildCache } from '@angular/build/private' ;
10- import { BuilderContext , BuilderOutput } from '@angular-devkit/architect' ;
11- import type { Config , ConfigOptions } from 'karma' ;
10+ import type { BuilderContext , BuilderOutput } from '@angular-devkit/architect' ;
11+ import type { Config , ConfigOptions , Server } from 'karma' ;
1212import * as path from 'node:path' ;
13- import { Observable , defaultIfEmpty , from , switchMap } from 'rxjs' ;
14- import { Configuration } from 'webpack' ;
13+ import type { Configuration } from 'webpack' ;
1514import { getCommonConfig , getStylesConfig } from '../../tools/webpack/configs' ;
16- import { ExecutionTransformer } from '../../transforms' ;
15+ import type { ExecutionTransformer } from '../../transforms' ;
1716import { generateBrowserWebpackConfigFromContext } from '../../utils/webpack-browser-config' ;
18- import { Schema as BrowserBuilderOptions , OutputHashing } from '../browser/schema' ;
17+ import { type Schema as BrowserBuilderOptions , OutputHashing } from '../browser/schema' ;
1918import { FindTestsPlugin } from './find-tests-plugin' ;
20- import { Schema as KarmaBuilderOptions } from './schema' ;
19+ import type { Schema as KarmaBuilderOptions } from './schema' ;
2120
2221export type KarmaConfigOptions = ConfigOptions & {
2322 buildWebpack ?: unknown ;
@@ -33,9 +32,22 @@ export function execute(
3332 // The karma options transform cannot be async without a refactor of the builder implementation
3433 karmaOptions ?: ( options : KarmaConfigOptions ) => KarmaConfigOptions ;
3534 } = { } ,
36- ) : Observable < BuilderOutput > {
37- return from ( initializeBrowser ( options , context , transforms . webpackConfiguration ) ) . pipe (
38- switchMap ( async ( [ karma , webpackConfig ] ) => {
35+ ) : AsyncIterable < BuilderOutput > {
36+ let karmaServer : Server ;
37+ let isCancelled = false ;
38+
39+ return new ReadableStream ( {
40+ async start ( controller ) {
41+ const [ karma , webpackConfig ] = await initializeBrowser (
42+ options ,
43+ context ,
44+ transforms . webpackConfiguration ,
45+ ) ;
46+
47+ if ( isCancelled ) {
48+ return ;
49+ }
50+
3951 const projectName = context . target ?. project ;
4052 if ( ! projectName ) {
4153 throw new Error ( `The 'karma' builder requires a target to be specified.` ) ;
@@ -71,44 +83,54 @@ export function execute(
7183 logger : context . logger ,
7284 } ;
7385
74- const parsedKarmaConfig = await karma . config . parseConfig (
86+ const parsedKarmaConfig = ( await karma . config . parseConfig (
7587 options . karmaConfig && path . resolve ( context . workspaceRoot , options . karmaConfig ) ,
7688 transforms . karmaOptions ? transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
7789 { promiseConfig : true , throwErrors : true } ,
78- ) ;
90+ ) ) as KarmaConfigOptions ;
7991
80- return [ karma , parsedKarmaConfig ] as [ typeof karma , KarmaConfigOptions ] ;
81- } ) ,
82- switchMap (
83- ( [ karma , karmaConfig ] ) =>
84- new Observable < BuilderOutput > ( ( subscriber ) => {
85- // Pass onto Karma to emit BuildEvents.
86- karmaConfig . buildWebpack ??= { } ;
87- if ( typeof karmaConfig . buildWebpack === 'object' ) {
88- // eslint-disable-next-line @typescript-eslint/no-explicit-any
89- ( karmaConfig . buildWebpack as any ) . failureCb ??= ( ) =>
90- subscriber . next ( { success : false } ) ;
91- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92- ( karmaConfig . buildWebpack as any ) . successCb ??= ( ) =>
93- subscriber . next ( { success : true } ) ;
94- }
92+ if ( isCancelled ) {
93+ return ;
94+ }
9595
96- // Complete the observable once the Karma server returns.
97- const karmaServer = new karma . Server ( karmaConfig as Config , ( exitCode ) => {
98- subscriber . next ( { success : exitCode === 0 } ) ;
99- subscriber . complete ( ) ;
100- } ) ;
96+ const enqueue = ( value : BuilderOutput ) => {
97+ try {
98+ controller . enqueue ( value ) ;
99+ } catch {
100+ // Controller is already closed
101+ }
102+ } ;
101103
102- const karmaStart = karmaServer . start ( ) ;
104+ const close = ( ) => {
105+ try {
106+ controller . close ( ) ;
107+ } catch {
108+ // Controller is already closed
109+ }
110+ } ;
103111
104- // Cleanup, signal Karma to exit.
105- return ( ) => {
106- void karmaStart . then ( ( ) => karmaServer . stop ( ) ) ;
107- } ;
108- } ) ,
109- ) ,
110- defaultIfEmpty ( { success : false } ) ,
111- ) ;
112+ // Pass onto Karma to emit BuildEvents.
113+ parsedKarmaConfig . buildWebpack ??= { } ;
114+ if ( typeof parsedKarmaConfig . buildWebpack === 'object' ) {
115+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116+ ( parsedKarmaConfig . buildWebpack as any ) . failureCb ??= ( ) => enqueue ( { success : false } ) ;
117+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
118+ ( parsedKarmaConfig . buildWebpack as any ) . successCb ??= ( ) => enqueue ( { success : true } ) ;
119+ }
120+
121+ // Close the stream once the Karma server returns.
122+ karmaServer = new karma . Server ( parsedKarmaConfig as Config , ( exitCode ) => {
123+ enqueue ( { success : exitCode === 0 } ) ;
124+ close ( ) ;
125+ } ) ;
126+
127+ await karmaServer . start ( ) ;
128+ } ,
129+ async cancel ( ) {
130+ isCancelled = true ;
131+ await karmaServer ?. stop ( ) ;
132+ } ,
133+ } ) ;
112134}
113135
114136async function initializeBrowser (
0 commit comments