8
8
9
9
import type { BuilderOutput } from '@angular-devkit/architect' ;
10
10
import assert from 'node:assert' ;
11
- import { readFile } from 'node:fs/promises' ;
12
11
import path from 'node:path' ;
13
- import type { InlineConfig , Vitest , VitestPlugin } from 'vitest/node' ;
12
+ import type { InlineConfig , Vitest } from 'vitest/node' ;
14
13
import { assertIsError } from '../../../../utils/error' ;
15
14
import { loadEsmModule } from '../../../../utils/load-esm' ;
16
15
import { toPosixPath } from '../../../../utils/path' ;
@@ -24,22 +23,22 @@ import { NormalizedUnitTestBuilderOptions } from '../../options';
24
23
import { findTests , getTestEntrypoints } from '../../test-discovery' ;
25
24
import type { TestExecutor } from '../api' ;
26
25
import { setupBrowserConfiguration } from './browser-provider' ;
26
+ import { createVitestPlugins } from './plugins' ;
27
27
28
28
type VitestCoverageOption = Exclude < InlineConfig [ 'coverage' ] , undefined > ;
29
- type VitestPlugins = Awaited < ReturnType < typeof VitestPlugin > > ;
30
29
31
30
export class VitestExecutor implements TestExecutor {
32
31
private vitest : Vitest | undefined ;
33
32
private readonly projectName : string ;
34
33
private readonly options : NormalizedUnitTestBuilderOptions ;
35
- private buildResultFiles = new Map < string , ResultFile > ( ) ;
34
+ private readonly buildResultFiles = new Map < string , ResultFile > ( ) ;
36
35
37
36
// This is a reverse map of the entry points created in `build-options.ts`.
38
37
// It is used by the in-memory provider plugin to map the requested test file
39
38
// path back to its bundled output path.
40
39
// Example: `Map<'/path/to/src/app.spec.ts', 'spec-src-app-spec'>`
41
- private testFileToEntryPoint = new Map < string , string > ( ) ;
42
- private entryPointToTestFile = new Map < string , string > ( ) ;
40
+ private readonly testFileToEntryPoint = new Map < string , string > ( ) ;
41
+ private readonly entryPointToTestFile = new Map < string , string > ( ) ;
43
42
44
43
constructor ( projectName : string , options : NormalizedUnitTestBuilderOptions ) {
45
44
this . projectName = projectName ;
@@ -135,134 +134,6 @@ export class VitestExecutor implements TestExecutor {
135
134
return testSetupFiles ;
136
135
}
137
136
138
- private createVitestPlugins (
139
- testSetupFiles : string [ ] ,
140
- browserOptions : Awaited < ReturnType < typeof setupBrowserConfiguration > > ,
141
- ) : VitestPlugins {
142
- const { workspaceRoot } = this . options ;
143
-
144
- return [
145
- {
146
- name : 'angular:project-init' ,
147
- // Type is incorrect. This allows a Promise<void>.
148
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
149
- configureVitest : async ( context ) => {
150
- // Create a subproject that can be configured with plugins for browser mode.
151
- // Plugins defined directly in the vite overrides will not be present in the
152
- // browser specific Vite instance.
153
- await context . injectTestProjects ( {
154
- test : {
155
- name : this . projectName ,
156
- root : workspaceRoot ,
157
- globals : true ,
158
- setupFiles : testSetupFiles ,
159
- // Use `jsdom` if no browsers are explicitly configured.
160
- // `node` is effectively no "environment" and the default.
161
- environment : browserOptions . browser ? 'node' : 'jsdom' ,
162
- browser : browserOptions . browser ,
163
- include : this . options . include ,
164
- ...( this . options . exclude ? { exclude : this . options . exclude } : { } ) ,
165
- } ,
166
- plugins : [
167
- {
168
- name : 'angular:test-in-memory-provider' ,
169
- enforce : 'pre' ,
170
- resolveId : ( id , importer ) => {
171
- if ( importer && ( id [ 0 ] === '.' || id [ 0 ] === '/' ) ) {
172
- let fullPath ;
173
- if ( this . testFileToEntryPoint . has ( importer ) ) {
174
- fullPath = toPosixPath ( path . join ( this . options . workspaceRoot , id ) ) ;
175
- } else {
176
- fullPath = toPosixPath ( path . join ( path . dirname ( importer ) , id ) ) ;
177
- }
178
-
179
- const relativePath = path . relative ( this . options . workspaceRoot , fullPath ) ;
180
- if ( this . buildResultFiles . has ( toPosixPath ( relativePath ) ) ) {
181
- return fullPath ;
182
- }
183
- }
184
-
185
- if ( this . testFileToEntryPoint . has ( id ) ) {
186
- return id ;
187
- }
188
-
189
- assert (
190
- this . buildResultFiles . size > 0 ,
191
- 'buildResult must be available for resolving.' ,
192
- ) ;
193
- const relativePath = path . relative ( this . options . workspaceRoot , id ) ;
194
- if ( this . buildResultFiles . has ( toPosixPath ( relativePath ) ) ) {
195
- return id ;
196
- }
197
- } ,
198
- load : async ( id ) => {
199
- assert (
200
- this . buildResultFiles . size > 0 ,
201
- 'buildResult must be available for in-memory loading.' ,
202
- ) ;
203
-
204
- // Attempt to load as a source test file.
205
- const entryPoint = this . testFileToEntryPoint . get ( id ) ;
206
- let outputPath ;
207
- if ( entryPoint ) {
208
- outputPath = entryPoint + '.js' ;
209
-
210
- // To support coverage exclusion of the actual test file, the virtual
211
- // test entry point only references the built and bundled intermediate file.
212
- return {
213
- code : `import "./${ outputPath } ";` ,
214
- } ;
215
- } else {
216
- // Attempt to load as a built artifact.
217
- const relativePath = path . relative ( this . options . workspaceRoot , id ) ;
218
- outputPath = toPosixPath ( relativePath ) ;
219
- }
220
-
221
- const outputFile = this . buildResultFiles . get ( outputPath ) ;
222
- if ( outputFile ) {
223
- const sourceMapPath = outputPath + '.map' ;
224
- const sourceMapFile = this . buildResultFiles . get ( sourceMapPath ) ;
225
- const code =
226
- outputFile . origin === 'memory'
227
- ? Buffer . from ( outputFile . contents ) . toString ( 'utf-8' )
228
- : await readFile ( outputFile . inputPath , 'utf-8' ) ;
229
- const map = sourceMapFile
230
- ? sourceMapFile . origin === 'memory'
231
- ? Buffer . from ( sourceMapFile . contents ) . toString ( 'utf-8' )
232
- : await readFile ( sourceMapFile . inputPath , 'utf-8' )
233
- : undefined ;
234
-
235
- return {
236
- code,
237
- map : map ? JSON . parse ( map ) : undefined ,
238
- } ;
239
- }
240
- } ,
241
- } ,
242
- {
243
- name : 'angular:html-index' ,
244
- transformIndexHtml : ( ) => {
245
- // Add all global stylesheets
246
- if ( this . buildResultFiles . has ( 'styles.css' ) ) {
247
- return [
248
- {
249
- tag : 'link' ,
250
- attrs : { href : 'styles.css' , rel : 'stylesheet' } ,
251
- injectTo : 'head' ,
252
- } ,
253
- ] ;
254
- }
255
-
256
- return [ ] ;
257
- } ,
258
- } ,
259
- ] ,
260
- } ) ;
261
- } ,
262
- } ,
263
- ] ;
264
- }
265
-
266
137
private async initializeVitest ( ) : Promise < Vitest > {
267
138
const { codeCoverage, reporters, workspaceRoot, browsers, debug, watch } = this . options ;
268
139
@@ -296,7 +167,15 @@ export class VitestExecutor implements TestExecutor {
296
167
) ;
297
168
298
169
const testSetupFiles = this . prepareSetupFiles ( ) ;
299
- const plugins = this . createVitestPlugins ( testSetupFiles , browserOptions ) ;
170
+ const plugins = createVitestPlugins ( this . options , testSetupFiles , browserOptions , {
171
+ workspaceRoot,
172
+ projectSourceRoot : this . options . projectSourceRoot ,
173
+ projectName : this . projectName ,
174
+ include : this . options . include ,
175
+ exclude : this . options . exclude ,
176
+ buildResultFiles : this . buildResultFiles ,
177
+ testFileToEntryPoint : this . testFileToEntryPoint ,
178
+ } ) ;
300
179
301
180
const debugOptions = debug
302
181
? {
0 commit comments