@@ -20,7 +20,7 @@ import { randomUUID } from 'crypto';
20
20
import glob from 'fast-glob' ;
21
21
import * as fs from 'fs/promises' ;
22
22
import { IncomingMessage , ServerResponse } from 'http' ;
23
- import type { Config , ConfigOptions , InlinePluginDef } from 'karma' ;
23
+ import type { Config , ConfigOptions , FilePattern , InlinePluginDef } from 'karma' ;
24
24
import * as path from 'path' ;
25
25
import { Observable , Subscriber , catchError , defaultIfEmpty , from , of , switchMap } from 'rxjs' ;
26
26
import { Configuration } from 'webpack' ;
@@ -106,6 +106,66 @@ class AngularAssetsMiddleware {
106
106
}
107
107
}
108
108
109
+ class AngularPolyfillsPlugin {
110
+ static readonly $inject = [ 'config.files' ] ;
111
+
112
+ static readonly NAME = 'angular-polyfills' ;
113
+
114
+ static createPlugin (
115
+ polyfillsFile : FilePattern ,
116
+ jasmineCleanupFiles : FilePattern ,
117
+ ) : InlinePluginDef {
118
+ return {
119
+ // This has to be a "reporter" because reporters run _after_ frameworks
120
+ // and karma-jasmine-html-reporter injects additional scripts that may
121
+ // depend on Jasmine but aren't modules - which means that they would run
122
+ // _before_ all module code (including jasmine).
123
+ [ `reporter:${ AngularPolyfillsPlugin . NAME } ` ] : [
124
+ 'factory' ,
125
+ Object . assign ( ( files : ( string | FilePattern ) [ ] ) => {
126
+ // The correct order is zone.js -> jasmine -> zone.js/testing.
127
+ // Jasmine has to see the patched version of the global `setTimeout`
128
+ // function so it doesn't cache the unpatched version. And /testing
129
+ // needs to see the global `jasmine` object so it can patch it.
130
+ const polyfillsIndex = 0 ;
131
+ files . splice ( polyfillsIndex , 0 , polyfillsFile ) ;
132
+
133
+ // Insert just before test_main.js.
134
+ const zoneTestingIndex = files . findIndex ( ( f ) => {
135
+ if ( typeof f === 'string' ) {
136
+ return false ;
137
+ }
138
+
139
+ return f . pattern . endsWith ( '/test_main.js' ) ;
140
+ } ) ;
141
+ if ( zoneTestingIndex === - 1 ) {
142
+ throw new Error ( 'Could not find test entrypoint file.' ) ;
143
+ }
144
+ files . splice ( zoneTestingIndex , 0 , jasmineCleanupFiles ) ;
145
+
146
+ // We need to ensure that all files are served as modules, otherwise
147
+ // the order in the files list gets really confusing: Karma doesn't
148
+ // set defer on scripts, so all scripts with type=js will run first,
149
+ // even if type=module files appeared earlier in `files`.
150
+ for ( const f of files ) {
151
+ if ( typeof f === 'string' ) {
152
+ throw new Error ( `Unexpected string-based file: "${ f } "` ) ;
153
+ }
154
+ if ( f . included === false ) {
155
+ // Don't worry about files that aren't included on the initial
156
+ // page load. `type` won't affect them.
157
+ continue ;
158
+ }
159
+ if ( 'js' === ( f . type ?? 'js' ) ) {
160
+ f . type = 'module' ;
161
+ }
162
+ }
163
+ } , AngularPolyfillsPlugin ) ,
164
+ ] ,
165
+ } ;
166
+ }
167
+ }
168
+
109
169
function injectKarmaReporter (
110
170
buildOptions : BuildOptions ,
111
171
buildIterator : AsyncIterator < Result > ,
@@ -247,12 +307,27 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
247
307
return path . join ( context . workspaceRoot , sourceRoot ) ;
248
308
}
249
309
250
- function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : string [ ] {
310
+ function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : [ string [ ] , string [ ] ] {
251
311
if ( typeof polyfills === 'string' ) {
252
- return [ polyfills ] ;
312
+ polyfills = [ polyfills ] ;
313
+ } else if ( ! polyfills ) {
314
+ polyfills = [ ] ;
253
315
}
254
316
255
- return polyfills ?? [ ] ;
317
+ const jasmineGlobalEntryPoint =
318
+ '@angular-devkit/build-angular/src/builders/karma/jasmine_global.js' ;
319
+ const jasmineGlobalCleanupEntrypoint =
320
+ '@angular-devkit/build-angular/src/builders/karma/jasmine_global_cleanup.js' ;
321
+
322
+ const zoneTestingEntryPoint = 'zone.js/testing' ;
323
+ const polyfillsExludingZoneTesting = polyfills . filter ( ( p ) => p !== zoneTestingEntryPoint ) ;
324
+
325
+ return [
326
+ polyfillsExludingZoneTesting . concat ( [ jasmineGlobalEntryPoint ] ) ,
327
+ polyfillsExludingZoneTesting . length === polyfills . length
328
+ ? [ jasmineGlobalCleanupEntrypoint ]
329
+ : [ jasmineGlobalCleanupEntrypoint , zoneTestingEntryPoint ] ,
330
+ ] ;
256
331
}
257
332
258
333
async function collectEntrypoints (
@@ -311,6 +386,11 @@ async function initializeApplication(
311
386
)
312
387
: undefined ;
313
388
389
+ const [ polyfills , jasmineCleanup ] = normalizePolyfills ( options . polyfills ) ;
390
+ for ( let idx = 0 ; idx < jasmineCleanup . length ; ++ idx ) {
391
+ entryPoints . set ( `jasmine-cleanup-${ idx } ` , jasmineCleanup [ idx ] ) ;
392
+ }
393
+
314
394
const buildOptions : BuildOptions = {
315
395
assets : options . assets ,
316
396
entryPoints,
@@ -327,7 +407,7 @@ async function initializeApplication(
327
407
} ,
328
408
instrumentForCoverage,
329
409
styles : options . styles ,
330
- polyfills : normalizePolyfills ( options . polyfills ) ,
410
+ polyfills,
331
411
webWorkerTsConfig : options . webWorkerTsConfig ,
332
412
watch : options . watch ?? ! karmaOptions . singleRun ,
333
413
stylePreprocessorOptions : options . stylePreprocessorOptions ,
@@ -349,10 +429,25 @@ async function initializeApplication(
349
429
// Write test files
350
430
await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
351
431
432
+ // We need to add this to the beginning *after* the testing framework has
433
+ // prepended its files.
434
+ const polyfillsFile : FilePattern = {
435
+ pattern : `${ outputPath } /polyfills.js` ,
436
+ included : true ,
437
+ served : true ,
438
+ type : 'module' ,
439
+ watched : false ,
440
+ } ;
441
+ const jasmineCleanupFiles : FilePattern = {
442
+ pattern : `${ outputPath } /jasmine-cleanup-*.js` ,
443
+ included : true ,
444
+ served : true ,
445
+ type : 'module' ,
446
+ watched : false ,
447
+ } ;
448
+
352
449
karmaOptions . files ??= [ ] ;
353
450
karmaOptions . files . push (
354
- // Serve polyfills first.
355
- { pattern : `${ outputPath } /polyfills.js` , type : 'module' , watched : false } ,
356
451
// Serve global setup script.
357
452
{ pattern : `${ outputPath } /${ mainName } .js` , type : 'module' , watched : false } ,
358
453
// Serve all source maps.
@@ -413,6 +508,12 @@ async function initializeApplication(
413
508
parsedKarmaConfig . middleware ??= [ ] ;
414
509
parsedKarmaConfig . middleware . push ( AngularAssetsMiddleware . NAME ) ;
415
510
511
+ parsedKarmaConfig . plugins . push (
512
+ AngularPolyfillsPlugin . createPlugin ( polyfillsFile , jasmineCleanupFiles ) ,
513
+ ) ;
514
+ parsedKarmaConfig . reporters ??= [ ] ;
515
+ parsedKarmaConfig . reporters . push ( AngularPolyfillsPlugin . NAME ) ;
516
+
416
517
// When using code-coverage, auto-add karma-coverage.
417
518
// This was done as part of the karma plugin for webpack.
418
519
if (
0 commit comments