88
99import type { BuilderOutput } from '@angular-devkit/architect' ;
1010import assert from 'node:assert' ;
11- import { readFile } from 'node:fs/promises' ;
1211import path from 'node:path' ;
13- import type { InlineConfig , Vitest , VitestPlugin } from 'vitest/node' ;
12+ import type { InlineConfig , Vitest } from 'vitest/node' ;
1413import { assertIsError } from '../../../../utils/error' ;
1514import { loadEsmModule } from '../../../../utils/load-esm' ;
1615import { toPosixPath } from '../../../../utils/path' ;
@@ -24,22 +23,22 @@ import { NormalizedUnitTestBuilderOptions } from '../../options';
2423import { findTests , getTestEntrypoints } from '../../test-discovery' ;
2524import type { TestExecutor } from '../api' ;
2625import { setupBrowserConfiguration } from './browser-provider' ;
26+ import { createVitestPlugins } from './plugins' ;
2727
2828type VitestCoverageOption = Exclude < InlineConfig [ 'coverage' ] , undefined > ;
29- type VitestPlugins = Awaited < ReturnType < typeof VitestPlugin > > ;
3029
3130export class VitestExecutor implements TestExecutor {
3231 private vitest : Vitest | undefined ;
3332 private readonly projectName : string ;
3433 private readonly options : NormalizedUnitTestBuilderOptions ;
35- private buildResultFiles = new Map < string , ResultFile > ( ) ;
34+ private readonly buildResultFiles = new Map < string , ResultFile > ( ) ;
3635
3736 // This is a reverse map of the entry points created in `build-options.ts`.
3837 // It is used by the in-memory provider plugin to map the requested test file
3938 // path back to its bundled output path.
4039 // 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 > ( ) ;
4342
4443 constructor ( projectName : string , options : NormalizedUnitTestBuilderOptions ) {
4544 this . projectName = projectName ;
@@ -135,134 +134,6 @@ export class VitestExecutor implements TestExecutor {
135134 return testSetupFiles ;
136135 }
137136
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-
266137 private async initializeVitest ( ) : Promise < Vitest > {
267138 const { codeCoverage, reporters, workspaceRoot, browsers, debug, watch } = this . options ;
268139
@@ -296,7 +167,15 @@ export class VitestExecutor implements TestExecutor {
296167 ) ;
297168
298169 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+ } ) ;
300179
301180 const debugOptions = debug
302181 ? {
0 commit comments