6
6
* found in the LICENSE file at https://angular.dev/license
7
7
*/
8
8
9
- import { assertCompatibleAngularVersion , purgeStaleBuildCache } from '@angular/build/private' ;
10
- import { BuilderContext , BuilderOutput , createBuilder } from '@angular-devkit/architect' ;
9
+ import { assertCompatibleAngularVersion } from '@angular/build/private' ;
10
+ import {
11
+ BuilderContext ,
12
+ BuilderOutput ,
13
+ createBuilder ,
14
+ targetFromTargetString ,
15
+ } from '@angular-devkit/architect' ;
11
16
import { strings } from '@angular-devkit/core' ;
12
- import type { Config , ConfigOptions } from 'karma' ;
17
+ import type { ConfigOptions } from 'karma' ;
13
18
import { createRequire } from 'module' ;
14
19
import * as path from 'path' ;
15
- import { Observable , defaultIfEmpty , from , switchMap } from 'rxjs' ;
20
+ import { Observable , from , mergeMap } from 'rxjs' ;
16
21
import { Configuration } from 'webpack' ;
17
- import { getCommonConfig , getStylesConfig } from '../../tools/webpack/configs' ;
18
22
import { ExecutionTransformer } from '../../transforms' ;
19
- import { generateBrowserWebpackConfigFromContext } from '../../utils/webpack-browser-config' ;
20
- import { Schema as BrowserBuilderOptions , OutputHashing } from '../browser/schema' ;
21
- import { FindTestsPlugin } from './find-tests-plugin' ;
22
- import { Schema as KarmaBuilderOptions } from './schema' ;
23
+ import { BuilderMode , Schema as KarmaBuilderOptions } from './schema' ;
23
24
24
25
export type KarmaConfigOptions = ConfigOptions & {
25
26
buildWebpack ?: unknown ;
26
27
configFile ?: string ;
27
28
} ;
28
29
29
- async function initialize (
30
- options : KarmaBuilderOptions ,
31
- context : BuilderContext ,
32
- webpackConfigurationTransformer ?: ExecutionTransformer < Configuration > ,
33
- ) : Promise < [ typeof import ( 'karma' ) , Configuration ] > {
34
- // Purge old build disk cache.
35
- await purgeStaleBuildCache ( context ) ;
36
-
37
- const { config } = await generateBrowserWebpackConfigFromContext (
38
- // only two properties are missing:
39
- // * `outputPath` which is fixed for tests
40
- // * `budgets` which might be incorrect due to extra dev libs
41
- {
42
- ...( options as unknown as BrowserBuilderOptions ) ,
43
- outputPath : '' ,
44
- budgets : undefined ,
45
- optimization : false ,
46
- buildOptimizer : false ,
47
- aot : false ,
48
- vendorChunk : true ,
49
- namedChunks : true ,
50
- extractLicenses : false ,
51
- outputHashing : OutputHashing . None ,
52
- // The webpack tier owns the watch behavior so we want to force it in the config.
53
- // When not in watch mode, webpack-dev-middleware will call `compiler.watch` anyway.
54
- // https://github.com/webpack/webpack-dev-middleware/blob/698c9ae5e9bb9a013985add6189ff21c1a1ec185/src/index.js#L65
55
- // https://github.com/webpack/webpack/blob/cde1b73e12eb8a77eb9ba42e7920c9ec5d29c2c9/lib/Compiler.js#L379-L388
56
- watch : true ,
57
- } ,
58
- context ,
59
- ( wco ) => [ getCommonConfig ( wco ) , getStylesConfig ( wco ) ] ,
60
- ) ;
61
-
62
- const karma = await import ( 'karma' ) ;
63
-
64
- return [ karma , ( await webpackConfigurationTransformer ?.( config ) ) ?? config ] ;
65
- }
66
-
67
30
/**
68
31
* @experimental Direct usage of this function is considered experimental.
69
32
*/
@@ -79,122 +42,68 @@ export function execute(
79
42
// Check Angular version.
80
43
assertCompatibleAngularVersion ( context . workspaceRoot ) ;
81
44
45
+ return from ( getExecuteWithBuilder ( options , context ) ) . pipe (
46
+ mergeMap ( ( [ useEsbuild , executeWithBuilder ] ) => {
47
+ const karmaOptions = getBaseKarmaOptions ( options , context , useEsbuild ) ;
48
+
49
+ return executeWithBuilder . execute ( options , context , karmaOptions , transforms ) ;
50
+ } ) ,
51
+ ) ;
52
+ }
53
+
54
+ function getBaseKarmaOptions (
55
+ options : KarmaBuilderOptions ,
56
+ context : BuilderContext ,
57
+ useEsbuild : boolean ,
58
+ ) : KarmaConfigOptions {
82
59
let singleRun : boolean | undefined ;
83
60
if ( options . watch !== undefined ) {
84
61
singleRun = ! options . watch ;
85
62
}
86
63
87
- return from ( initialize ( options , context , transforms . webpackConfiguration ) ) . pipe (
88
- switchMap ( async ( [ karma , webpackConfig ] ) => {
89
- // Determine project name from builder context target
90
- const projectName = context . target ?. project ;
91
- if ( ! projectName ) {
92
- throw new Error ( `The 'karma' builder requires a target to be specified.` ) ;
93
- }
94
-
95
- const karmaOptions : KarmaConfigOptions = options . karmaConfig
96
- ? { }
97
- : getBuiltInKarmaConfig ( context . workspaceRoot , projectName ) ;
98
-
99
- karmaOptions . singleRun = singleRun ;
100
-
101
- // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
102
- // for single run executions. Not clearing context for multi-run (watched) builds allows the
103
- // Jasmine Spec Runner to be visible in the browser after test execution.
104
- karmaOptions . client ??= { } ;
105
- karmaOptions . client . clearContext ??= singleRun ?? false ; // `singleRun` defaults to `false` per Karma docs.
106
-
107
- // Convert browsers from a string to an array
108
- if ( typeof options . browsers === 'string' && options . browsers ) {
109
- karmaOptions . browsers = options . browsers . split ( ',' ) ;
110
- } else if ( options . browsers === false ) {
111
- karmaOptions . browsers = [ ] ;
112
- }
113
-
114
- if ( options . reporters ) {
115
- // Split along commas to make it more natural, and remove empty strings.
116
- const reporters = options . reporters
117
- . reduce < string [ ] > ( ( acc , curr ) => acc . concat ( curr . split ( ',' ) ) , [ ] )
118
- . filter ( ( x ) => ! ! x ) ;
119
-
120
- if ( reporters . length > 0 ) {
121
- karmaOptions . reporters = reporters ;
122
- }
123
- }
124
-
125
- if ( ! options . main ) {
126
- webpackConfig . entry ??= { } ;
127
- if ( typeof webpackConfig . entry === 'object' && ! Array . isArray ( webpackConfig . entry ) ) {
128
- if ( Array . isArray ( webpackConfig . entry [ 'main' ] ) ) {
129
- webpackConfig . entry [ 'main' ] . push ( getBuiltInMainFile ( ) ) ;
130
- } else {
131
- webpackConfig . entry [ 'main' ] = [ getBuiltInMainFile ( ) ] ;
132
- }
133
- }
134
- }
135
-
136
- const projectMetadata = await context . getProjectMetadata ( projectName ) ;
137
- const sourceRoot = ( projectMetadata . sourceRoot ?? projectMetadata . root ?? '' ) as string ;
64
+ // Determine project name from builder context target
65
+ const projectName = context . target ?. project ;
66
+ if ( ! projectName ) {
67
+ throw new Error ( `The 'karma' builder requires a target to be specified.` ) ;
68
+ }
138
69
139
- webpackConfig . plugins ??= [ ] ;
140
- webpackConfig . plugins . push (
141
- new FindTestsPlugin ( {
142
- include : options . include ,
143
- exclude : options . exclude ,
144
- workspaceRoot : context . workspaceRoot ,
145
- projectSourceRoot : path . join ( context . workspaceRoot , sourceRoot ) ,
146
- } ) ,
147
- ) ;
70
+ const karmaOptions : KarmaConfigOptions = options . karmaConfig
71
+ ? { }
72
+ : getBuiltInKarmaConfig ( context . workspaceRoot , projectName , useEsbuild ) ;
148
73
149
- karmaOptions . buildWebpack = {
150
- options,
151
- webpackConfig,
152
- logger : context . logger ,
153
- } ;
74
+ karmaOptions . singleRun = singleRun ;
154
75
155
- const parsedKarmaConfig = await karma . config . parseConfig (
156
- options . karmaConfig && path . resolve ( context . workspaceRoot , options . karmaConfig ) ,
157
- transforms . karmaOptions ? transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
158
- { promiseConfig : true , throwErrors : true } ,
159
- ) ;
76
+ // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
77
+ // for single run executions. Not clearing context for multi-run (watched) builds allows the
78
+ // Jasmine Spec Runner to be visible in the browser after test execution.
79
+ karmaOptions . client ??= { } ;
80
+ karmaOptions . client . clearContext ??= singleRun ?? false ; // `singleRun` defaults to `false` per Karma docs.
160
81
161
- return [ karma , parsedKarmaConfig ] as [ typeof karma , KarmaConfigOptions ] ;
162
- } ) ,
163
- switchMap (
164
- ( [ karma , karmaConfig ] ) =>
165
- new Observable < BuilderOutput > ( ( subscriber ) => {
166
- // Pass onto Karma to emit BuildEvents.
167
- karmaConfig . buildWebpack ??= { } ;
168
- if ( typeof karmaConfig . buildWebpack === 'object' ) {
169
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
- ( karmaConfig . buildWebpack as any ) . failureCb ??= ( ) =>
171
- subscriber . next ( { success : false } ) ;
172
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
- ( karmaConfig . buildWebpack as any ) . successCb ??= ( ) =>
174
- subscriber . next ( { success : true } ) ;
175
- }
82
+ // Convert browsers from a string to an array
83
+ if ( typeof options . browsers === 'string' && options . browsers ) {
84
+ karmaOptions . browsers = options . browsers . split ( ',' ) ;
85
+ } else if ( options . browsers === false ) {
86
+ karmaOptions . browsers = [ ] ;
87
+ }
176
88
177
- // Complete the observable once the Karma server returns.
178
- const karmaServer = new karma . Server ( karmaConfig as Config , ( exitCode ) => {
179
- subscriber . next ( { success : exitCode === 0 } ) ;
180
- subscriber . complete ( ) ;
181
- } ) ;
89
+ if ( options . reporters ) {
90
+ // Split along commas to make it more natural, and remove empty strings.
91
+ const reporters = options . reporters
92
+ . reduce < string [ ] > ( ( acc , curr ) => acc . concat ( curr . split ( ',' ) ) , [ ] )
93
+ . filter ( ( x ) => ! ! x ) ;
182
94
183
- const karmaStart = karmaServer . start ( ) ;
95
+ if ( reporters . length > 0 ) {
96
+ karmaOptions . reporters = reporters ;
97
+ }
98
+ }
184
99
185
- // Cleanup, signal Karma to exit.
186
- return ( ) => {
187
- void karmaStart . then ( ( ) => karmaServer . stop ( ) ) ;
188
- } ;
189
- } ) ,
190
- ) ,
191
- defaultIfEmpty ( { success : false } ) ,
192
- ) ;
100
+ return karmaOptions ;
193
101
}
194
102
195
103
function getBuiltInKarmaConfig (
196
104
workspaceRoot : string ,
197
105
projectName : string ,
106
+ useEsbuild : boolean ,
198
107
) : ConfigOptions & Record < string , unknown > {
199
108
let coverageFolderName = projectName . charAt ( 0 ) === '@' ? projectName . slice ( 1 ) : projectName ;
200
109
if ( / [ A - Z ] / . test ( coverageFolderName ) ) {
@@ -206,13 +115,13 @@ function getBuiltInKarmaConfig(
206
115
// Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
207
116
return {
208
117
basePath : '' ,
209
- frameworks : [ 'jasmine' , '@angular-devkit/build-angular' ] ,
118
+ frameworks : [ 'jasmine' , ... ( useEsbuild ? [ ] : [ '@angular-devkit/build-angular' ] ) ] ,
210
119
plugins : [
211
120
'karma-jasmine' ,
212
121
'karma-chrome-launcher' ,
213
122
'karma-jasmine-html-reporter' ,
214
123
'karma-coverage' ,
215
- '@angular-devkit/build-angular/plugins/karma' ,
124
+ ... ( useEsbuild ? [ ] : [ '@angular-devkit/build-angular/plugins/karma' ] ) ,
216
125
] . map ( ( p ) => workspaceRootRequire ( p ) ) ,
217
126
jasmineHtmlReporter : {
218
127
suppressAll : true , // removes the duplicated traces
@@ -243,22 +152,62 @@ function getBuiltInKarmaConfig(
243
152
export type { KarmaBuilderOptions } ;
244
153
export default createBuilder < Record < string , string > & KarmaBuilderOptions > ( execute ) ;
245
154
246
- function getBuiltInMainFile ( ) : string {
247
- const content = Buffer . from (
248
- `
249
- import { getTestBed } from '@angular/core/testing';
250
- import {
251
- BrowserDynamicTestingModule,
252
- platformBrowserDynamicTesting,
253
- } from '@angular/platform-browser-dynamic/testing';
155
+ async function getExecuteWithBuilder (
156
+ options : KarmaBuilderOptions ,
157
+ context : BuilderContext ,
158
+ ) : Promise < [ boolean , typeof import ( './application_builder' ) | typeof import ( './browser_builder' ) ] > {
159
+ const useEsbuild = await checkForEsbuild ( options , context ) ;
160
+ const executeWithBuilderModule = useEsbuild
161
+ ? import ( './application_builder' )
162
+ : import ( './browser_builder' ) ;
163
+
164
+ return [ useEsbuild , await executeWithBuilderModule ] ;
165
+ }
254
166
255
- // Initialize the Angular testing environment.
256
- getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
257
- errorOnUnknownElements: true,
258
- errorOnUnknownProperties: true
259
- });
260
- ` ,
261
- ) . toString ( 'base64' ) ;
167
+ async function checkForEsbuild (
168
+ options : KarmaBuilderOptions ,
169
+ context : BuilderContext ,
170
+ ) : Promise < boolean > {
171
+ if ( options . builderMode !== BuilderMode . Detect ) {
172
+ return options . builderMode === BuilderMode . Application ;
173
+ }
174
+
175
+ // Look up the current project's build target using a development configuration.
176
+ const buildTargetSpecifier = `::development` ;
177
+ const buildTarget = targetFromTargetString (
178
+ buildTargetSpecifier ,
179
+ context . target ?. project ,
180
+ 'build' ,
181
+ ) ;
182
+
183
+ try {
184
+ const developmentBuilderName = await context . getBuilderNameForTarget ( buildTarget ) ;
185
+
186
+ return isEsbuildBased ( developmentBuilderName ) ;
187
+ } catch ( e ) {
188
+ if ( ! ( e instanceof Error ) || e . message !== 'Project target does not exist.' ) {
189
+ throw e ;
190
+ }
191
+ // If we can't find a development builder, we can't use 'detect'.
192
+ throw new Error (
193
+ 'Failed to detect the builder used by the application. Please set builderMode explicitly.' ,
194
+ ) ;
195
+ }
196
+ }
197
+
198
+ function isEsbuildBased (
199
+ builderName : string ,
200
+ ) : builderName is
201
+ | '@angular/build:application'
202
+ | '@angular-devkit/build-angular:application'
203
+ | '@angular-devkit/build-angular:browser-esbuild' {
204
+ if (
205
+ builderName === '@angular/build:application' ||
206
+ builderName === '@angular-devkit/build-angular:application' ||
207
+ builderName === '@angular-devkit/build-angular:browser-esbuild'
208
+ ) {
209
+ return true ;
210
+ }
262
211
263
- return `ng-virtual-main.js!=!data:text/javascript;base64, ${ content } ` ;
212
+ return false ;
264
213
}
0 commit comments