@@ -11,20 +11,59 @@ import { basename, dirname, extname, join, relative } from 'node:path';
11
11
import { glob , isDynamicPattern } from 'tinyglobby' ;
12
12
import { toPosixPath } from '../../utils/path' ;
13
13
14
- /* Go through all patterns and find unique list of files */
14
+ /**
15
+ * Finds all test files in the project.
16
+ *
17
+ * @param include Glob patterns of files to include.
18
+ * @param exclude Glob patterns of files to exclude.
19
+ * @param workspaceRoot The absolute path to the workspace root.
20
+ * @param projectSourceRoot The absolute path to the project's source root.
21
+ * @returns A unique set of absolute paths to all test files.
22
+ */
15
23
export async function findTests (
16
24
include : string [ ] ,
17
25
exclude : string [ ] ,
18
26
workspaceRoot : string ,
19
27
projectSourceRoot : string ,
20
28
) : Promise < string [ ] > {
21
- const matchingTestsPromises = include . map ( ( pattern ) =>
22
- findMatchingTests ( pattern , exclude , workspaceRoot , projectSourceRoot ) ,
29
+ const staticMatches = new Set < string > ( ) ;
30
+ const dynamicPatterns : string [ ] = [ ] ;
31
+
32
+ const normalizedExcludes = exclude . map ( ( p ) =>
33
+ normalizePattern ( p , workspaceRoot , projectSourceRoot ) ,
23
34
) ;
24
- const files = await Promise . all ( matchingTestsPromises ) ;
25
35
26
- // Unique file names
27
- return [ ...new Set ( files . flat ( ) ) ] ;
36
+ // 1. Separate static and dynamic patterns
37
+ for ( const pattern of include ) {
38
+ const normalized = normalizePattern ( pattern , workspaceRoot , projectSourceRoot ) ;
39
+ if ( isDynamicPattern ( normalized ) ) {
40
+ dynamicPatterns . push ( normalized ) ;
41
+ } else {
42
+ const result = await handleStaticPattern ( normalized , projectSourceRoot ) ;
43
+ if ( Array . isArray ( result ) ) {
44
+ result . forEach ( ( file ) => staticMatches . add ( file ) ) ;
45
+ } else {
46
+ // It was a static path that didn't resolve to a spec, treat as dynamic
47
+ dynamicPatterns . push ( result ) ;
48
+ }
49
+ }
50
+ }
51
+
52
+ // 2. Execute a single glob for all dynamic patterns
53
+ if ( dynamicPatterns . length > 0 ) {
54
+ const globMatches = await glob ( dynamicPatterns , {
55
+ cwd : projectSourceRoot ,
56
+ absolute : true ,
57
+ ignore : [ '**/node_modules/**' , ...normalizedExcludes ] ,
58
+ } ) ;
59
+
60
+ for ( const match of globMatches ) {
61
+ staticMatches . add ( match ) ;
62
+ }
63
+ }
64
+
65
+ // 3. Combine and de-duplicate results
66
+ return [ ...staticMatches ] ;
28
67
}
29
68
30
69
interface TestEntrypointsOptions {
@@ -33,7 +72,14 @@ interface TestEntrypointsOptions {
33
72
removeTestExtension ?: boolean ;
34
73
}
35
74
36
- /** Generate unique bundle names for a set of test files. */
75
+ /**
76
+ * Generates unique, dash-delimited bundle names for a set of test files.
77
+ * This is used to create distinct output files for each test.
78
+ *
79
+ * @param testFiles An array of absolute paths to test files.
80
+ * @param options Configuration options for generating entry points.
81
+ * @returns A map where keys are the generated unique bundle names and values are the original file paths.
82
+ */
37
83
export function getTestEntrypoints (
38
84
testFiles : string [ ] ,
39
85
{ projectSourceRoot, workspaceRoot, removeTestExtension } : TestEntrypointsOptions ,
@@ -82,6 +128,10 @@ const removeRelativeRoot = (path: string, root: string): string => {
82
128
return path ;
83
129
} ;
84
130
131
+ /**
132
+ * Removes potential root paths from a file path, returning a relative path.
133
+ * If no root path matches, it returns the file's basename.
134
+ */
85
135
function removeRoots ( path : string , roots : string [ ] ) : string {
86
136
for ( const root of roots ) {
87
137
if ( path . startsWith ( root ) ) {
@@ -92,12 +142,20 @@ function removeRoots(path: string, roots: string[]): string {
92
142
return basename ( path ) ;
93
143
}
94
144
95
- async function findMatchingTests (
145
+ /**
146
+ * Normalizes a glob pattern by converting it to a POSIX path, removing leading slashes,
147
+ * and making it relative to the project source root.
148
+ *
149
+ * @param pattern The glob pattern to normalize.
150
+ * @param workspaceRoot The absolute path to the workspace root.
151
+ * @param projectSourceRoot The absolute path to the project's source root.
152
+ * @returns A normalized glob pattern.
153
+ */
154
+ function normalizePattern (
96
155
pattern : string ,
97
- ignore : string [ ] ,
98
156
workspaceRoot : string ,
99
157
projectSourceRoot : string ,
100
- ) : Promise < string [ ] > {
158
+ ) : string {
101
159
// normalize pattern, glob lib only accepts forward slashes
102
160
let normalizedPattern = toPosixPath ( pattern ) ;
103
161
normalizedPattern = removeLeadingSlash ( normalizedPattern ) ;
@@ -106,40 +164,43 @@ async function findMatchingTests(
106
164
107
165
// remove relativeProjectRoot to support relative paths from root
108
166
// such paths are easy to get when running scripts via IDEs
109
- normalizedPattern = removeRelativeRoot ( normalizedPattern , relativeProjectRoot ) ;
167
+ return removeRelativeRoot ( normalizedPattern , relativeProjectRoot ) ;
168
+ }
110
169
111
- // special logic when pattern does not look like a glob
112
- if ( ! isDynamicPattern ( normalizedPattern ) ) {
113
- if ( await isDirectory ( join ( projectSourceRoot , normalizedPattern ) ) ) {
114
- normalizedPattern = `${ normalizedPattern } /**/*.spec.@(ts|tsx)` ;
115
- } else {
116
- // see if matching spec file exists
117
- const fileExt = extname ( normalizedPattern ) ;
118
- // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts`
119
- const potentialSpec = join (
120
- projectSourceRoot ,
121
- dirname ( normalizedPattern ) ,
122
- `${ basename ( normalizedPattern , fileExt ) } .spec${ fileExt } ` ,
123
- ) ;
124
-
125
- if ( await exists ( potentialSpec ) ) {
126
- return [ potentialSpec ] ;
127
- }
128
- }
170
+ /**
171
+ * Handles static (non-glob) patterns by attempting to resolve them to a directory
172
+ * of spec files or a corresponding `.spec` file.
173
+ *
174
+ * @param pattern The static path pattern.
175
+ * @param projectSourceRoot The absolute path to the project's source root.
176
+ * @returns A promise that resolves to either an array of found spec files, a new glob pattern,
177
+ * or the original pattern if no special handling was applied.
178
+ */
179
+ async function handleStaticPattern (
180
+ pattern : string ,
181
+ projectSourceRoot : string ,
182
+ ) : Promise < string [ ] | string > {
183
+ const fullPath = join ( projectSourceRoot , pattern ) ;
184
+ if ( await isDirectory ( fullPath ) ) {
185
+ return `${ pattern } /**/*.spec.@(ts|tsx)` ;
129
186
}
130
187
131
- // normalize the patterns in the ignore list
132
- const normalizedIgnorePatternList = ignore . map ( ( pattern : string ) =>
133
- removeRelativeRoot ( removeLeadingSlash ( toPosixPath ( pattern ) ) , relativeProjectRoot ) ,
188
+ const fileExt = extname ( pattern ) ;
189
+ // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts`
190
+ const potentialSpec = join (
191
+ projectSourceRoot ,
192
+ dirname ( pattern ) ,
193
+ `${ basename ( pattern , fileExt ) } .spec${ fileExt } ` ,
134
194
) ;
135
195
136
- return glob ( normalizedPattern , {
137
- cwd : projectSourceRoot ,
138
- absolute : true ,
139
- ignore : [ '**/node_modules/**' , ... normalizedIgnorePatternList ] ,
140
- } ) ;
196
+ if ( await exists ( potentialSpec ) ) {
197
+ return [ potentialSpec ] ;
198
+ }
199
+
200
+ return pattern ;
141
201
}
142
202
203
+ /** Checks if a path exists and is a directory. */
143
204
async function isDirectory ( path : PathLike ) : Promise < boolean > {
144
205
try {
145
206
const stats = await fs . stat ( path ) ;
@@ -150,6 +211,7 @@ async function isDirectory(path: PathLike): Promise<boolean> {
150
211
}
151
212
}
152
213
214
+ /** Checks if a path exists on the file system. */
153
215
async function exists ( path : PathLike ) : Promise < boolean > {
154
216
try {
155
217
await fs . access ( path , constants . F_OK ) ;
0 commit comments