@rpcbase/test 0.307.0 → 0.308.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,10 +24,10 @@ Only `RB_DISABLE_COVERAGE=1` skips the hooks; every other option lives inside th
24
24
 
25
25
  If you omit `collectCoverageFrom`, `rb-test` uses a reasonable default:
26
26
 
27
- - `src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}` when `src/` exists
27
+ - `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` when `src/` exists
28
28
  - if `src/` is missing, `rb-test` throws unless you set `collectCoverageFrom`
29
29
 
30
- Unit test files are excluded by default: `!**/*.test.{ts,tsx,js,jsx,mts,cts,mjs,cjs}`.
30
+ Unit test files are excluded by default: `!**/*.test.{ts,tsx,js,jsx,mjs,cjs}`.
31
31
 
32
32
  `collectCoverageFrom` supports negated globs (prefix with `!`) to exclude files.
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/test",
3
- "version": "0.307.0",
3
+ "version": "0.308.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
package/src/cli.js CHANGED
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from "child_process"
3
+ import { spawn, spawnSync } from "child_process"
4
4
  import fs from "fs"
5
5
  import fsPromises from "fs/promises"
6
6
  import path from "path"
7
7
  import { createRequire } from "module"
8
8
  import { fileURLToPath } from "url"
9
9
 
10
+ import fg from "fast-glob"
11
+
10
12
  import { createCoverageConfig } from "./coverage/config.js"
11
- import { isInsideAnyRoot, resolveCollectCoverageRoots } from "./coverage/collect.js"
13
+ import { createCollectCoverageMatcher, isInsideAnyRoot, resolveCollectCoverageRoots } from "./coverage/collect.js"
12
14
  import { loadCoverageOptions } from "./coverage/config-loader.js"
13
15
  import { removeCoverageFiles } from "./coverage/files.js"
14
- import { CoverageThresholdError, generateCoverageReport } from "./coverage/report.js"
16
+ import { collectCoveredFiles, CoverageThresholdError, generateCoverageReport } from "./coverage/report.js"
15
17
 
16
18
 
17
19
  const require = createRequire(import.meta.url)
@@ -30,26 +32,60 @@ if (process.env.IS_AIDER !== undefined && process.env.IS_AIDER !== "yes") {
30
32
 
31
33
  async function runTests() {
32
34
  const userArgs = process.argv.slice(2)
35
+ const buildSpecsMap = userArgs.includes("--build-specs-map")
36
+ const auto = userArgs.includes("--auto")
37
+ const showMapping = userArgs.includes("--show-mapping")
38
+ const filteredArgs = userArgs.filter((arg) => arg !== "--build-specs-map" && arg !== "--auto" && arg !== "--show-mapping")
39
+
40
+ if (buildSpecsMap && auto) {
41
+ throw new Error("[rb-test] --auto cannot be combined with --build-specs-map")
42
+ }
43
+
44
+ if (showMapping && !auto) {
45
+ throw new Error("[rb-test] --show-mapping requires --auto")
46
+ }
33
47
 
34
48
  const playwrightCoverage = await loadPlaywrightCoverageConfig()
35
49
  const vitestCoverage = await loadVitestCoverageConfig()
36
50
  const combinedCoverage = resolveCombinedCoverage(playwrightCoverage, vitestCoverage)
37
51
 
38
- if (combinedCoverage?.enabled) {
52
+ if (buildSpecsMap) {
53
+ await buildSpecsMapFromCoverage({
54
+ userArgs: filteredArgs,
55
+ playwrightCoverage,
56
+ vitestCoverage,
57
+ combinedCoverage,
58
+ })
59
+ return
60
+ }
61
+
62
+ const shouldGenerateCoverageReport = combinedCoverage?.enabled && !auto
63
+
64
+ if (shouldGenerateCoverageReport) {
39
65
  await cleanCoverageArtifacts(combinedCoverage.config)
40
66
  }
41
67
 
42
68
  let testError = null
43
69
 
44
70
  try {
45
- await runVitest(vitestCoverage, combinedCoverage?.config ?? null)
71
+ await runVitest(vitestCoverage, combinedCoverage?.config ?? null, { disableCoverage: auto })
46
72
  console.log("\nRunning Playwright Tests...")
47
- await runPlaywright(userArgs)
73
+ const playwrightArgs = auto
74
+ ? await resolveAutoPlaywrightArgs({
75
+ userArgs: filteredArgs,
76
+ playwrightCoverage,
77
+ vitestCoverage,
78
+ showMapping,
79
+ })
80
+ : userArgs
81
+ if (playwrightArgs) {
82
+ await runPlaywright(playwrightArgs, { disableCoverage: auto })
83
+ }
48
84
  } catch (error) {
49
85
  testError = error
50
86
  }
51
87
 
52
- if (combinedCoverage?.enabled) {
88
+ if (shouldGenerateCoverageReport) {
53
89
  try {
54
90
  await finalizeCoverage(combinedCoverage.config)
55
91
  } catch (error) {
@@ -73,7 +109,7 @@ runTests()
73
109
  process.exit(1)
74
110
  })
75
111
 
76
- async function runVitest(coverage, combinedConfig) {
112
+ async function runVitest(coverage, combinedConfig, { disableCoverage = false } = {}) {
77
113
  const vitestArgs = ["run", "--passWithNoTests"]
78
114
  const vitestConfig = resolveVitestConfig()
79
115
 
@@ -83,7 +119,11 @@ async function runVitest(coverage, combinedConfig) {
83
119
  const launcher = resolveVitestLauncher()
84
120
  const env = withRegisterShim(process.env)
85
121
 
86
- if (coverage?.enabled) {
122
+ if (disableCoverage) {
123
+ env.RB_DISABLE_COVERAGE = "1"
124
+ }
125
+
126
+ if (coverage?.enabled && !disableCoverage) {
87
127
  env.NODE_V8_COVERAGE = resolveNodeCoverageDir(combinedConfig ?? coverage.config)
88
128
  }
89
129
 
@@ -96,7 +136,7 @@ async function runVitest(coverage, combinedConfig) {
96
136
  failureMessage: "Vitest failed",
97
137
  })
98
138
 
99
- if (coverage?.enabled) {
139
+ if (coverage?.enabled && !disableCoverage) {
100
140
  await convertNodeCoverage({
101
141
  config: combinedConfig ?? coverage.config,
102
142
  nodeCoverageDir: resolveNodeCoverageDir(combinedConfig ?? coverage.config),
@@ -104,7 +144,411 @@ async function runVitest(coverage, combinedConfig) {
104
144
  }
105
145
  }
106
146
 
107
- function runPlaywright(userArgs) {
147
+ async function buildSpecsMapFromCoverage({ userArgs, combinedCoverage }) {
148
+ if (!combinedCoverage?.enabled) {
149
+ throw new Error("[specs-map] Coverage must be enabled to build the specs map.")
150
+ }
151
+
152
+ const config = combinedCoverage.config
153
+ const workspaceRoot = findWorkspaceRoot(process.cwd())
154
+
155
+ const specSourceFiles = await findSpecSourceFiles(config.rootDir)
156
+ if (specSourceFiles.length === 0) {
157
+ throw new Error("[specs-map] No spec files found under spec/**/*.spec.ts")
158
+ }
159
+
160
+ const filesMapDir = path.join(config.testResultsRoot, "files-map")
161
+ await fsPromises.rm(filesMapDir, { recursive: true, force: true })
162
+ await fsPromises.mkdir(filesMapDir, { recursive: true })
163
+
164
+ for (const specSourceFile of specSourceFiles) {
165
+ const specProjectPath = path.relative(config.rootDir, specSourceFile)
166
+ const specWorkspacePath = toPosixPath(path.relative(workspaceRoot, specSourceFile))
167
+ const testFile = resolvePlaywrightSpecFile(specProjectPath)
168
+
169
+ console.log(`\n[specs-map] Running ${specWorkspacePath}`)
170
+
171
+ await removeCoverageFiles(config)
172
+
173
+ let error = null
174
+ let failed = false
175
+ try {
176
+ await runPlaywright([...userArgs, testFile])
177
+ } catch (runError) {
178
+ error = runError
179
+ failed = true
180
+ console.error(`[specs-map] Failed: ${specWorkspacePath}`)
181
+ console.error(runError?.stack ?? String(runError))
182
+ }
183
+
184
+ const coveredFiles = await collectCoveredFiles(config)
185
+ const impactedFiles = coveredFiles
186
+ .map((filePath) => toPosixPath(path.relative(workspaceRoot, filePath)))
187
+ .filter((relativePath) => relativePath && !relativePath.startsWith("../") && relativePath !== "..")
188
+ .sort()
189
+
190
+ const outputFile = path.join(filesMapDir, `${specProjectPath}.json`)
191
+ await fsPromises.mkdir(path.dirname(outputFile), { recursive: true })
192
+ await fsPromises.writeFile(
193
+ outputFile,
194
+ JSON.stringify(
195
+ {
196
+ spec: specWorkspacePath,
197
+ files: impactedFiles,
198
+ failed,
199
+ },
200
+ null,
201
+ 2,
202
+ ),
203
+ "utf8",
204
+ )
205
+
206
+ if (failed) {
207
+ throw error
208
+ }
209
+ }
210
+ }
211
+
212
+ async function resolveAutoPlaywrightArgs({ userArgs, playwrightCoverage, vitestCoverage, showMapping = false }) {
213
+ const config = playwrightCoverage?.config ?? vitestCoverage?.config ?? null
214
+ if (!config) {
215
+ console.warn("[auto] Coverage config not found; running full Playwright suite.")
216
+ return userArgs
217
+ }
218
+
219
+ const filesMapDir = path.join(config.testResultsRoot, "files-map")
220
+ const mapFiles = await findFilesMapJson(filesMapDir)
221
+ if (mapFiles.length === 0) {
222
+ console.warn("[auto] Specs map not found; running full Playwright suite.")
223
+ return userArgs
224
+ }
225
+
226
+ const workspaceRoot = findWorkspaceRoot(process.cwd())
227
+ const gitChanges = getGitChanges(workspaceRoot)
228
+ const renameMap = new Map(gitChanges.filter((change) => change.kind === "rename").map((change) => [change.oldPath, change.newPath]))
229
+
230
+ const specRootAbs = path.join(config.rootDir, "spec")
231
+ const matchesCollectCoverageFrom = createCollectCoverageMatcher(config.collectCoverageFrom, config.rootDir)
232
+
233
+ const directSpecChanges = new Set()
234
+ const sourceChanges = []
235
+
236
+ for (const change of gitChanges) {
237
+ if (change.kind === "rename") {
238
+ const oldAbs = path.join(workspaceRoot, change.oldPath)
239
+ const newAbs = path.join(workspaceRoot, change.newPath)
240
+
241
+ if (isSpecSourceFile(newAbs, specRootAbs) && fs.existsSync(newAbs)) {
242
+ directSpecChanges.add(change.newPath)
243
+ }
244
+
245
+ const oldMatches = matchesCollectCoverageFrom(oldAbs)
246
+ const newMatches = matchesCollectCoverageFrom(newAbs)
247
+ if (oldMatches || newMatches) {
248
+ sourceChanges.push(change)
249
+ }
250
+ continue
251
+ }
252
+
253
+ const abs = path.join(workspaceRoot, change.path)
254
+
255
+ if (isSpecSourceFile(abs, specRootAbs) && fs.existsSync(abs)) {
256
+ directSpecChanges.add(change.path)
257
+ }
258
+
259
+ if (matchesCollectCoverageFrom(abs)) {
260
+ sourceChanges.push(change)
261
+ }
262
+ }
263
+
264
+ if (directSpecChanges.size === 0 && sourceChanges.length === 0) {
265
+ console.log("[auto] No relevant git changes.")
266
+ return null
267
+ }
268
+
269
+ const parsedMaps = []
270
+ for (const file of mapFiles) {
271
+ const json = await readJson(file)
272
+ if (!json) {
273
+ continue
274
+ }
275
+
276
+ if (json.failed === true) {
277
+ console.warn("[auto] Specs map contains failed entries; running full Playwright suite.")
278
+ return userArgs
279
+ }
280
+
281
+ const spec = typeof json?.spec === "string" ? json.spec : null
282
+ if (!spec) {
283
+ continue
284
+ }
285
+
286
+ const files = Array.isArray(json?.files) ? json.files.filter((entry) => typeof entry === "string") : []
287
+ parsedMaps.push({ spec, files })
288
+ }
289
+
290
+ if (parsedMaps.length === 0) {
291
+ console.warn("[auto] Specs map is empty; running full Playwright suite.")
292
+ return userArgs
293
+ }
294
+
295
+ const specsByImpactedFile = new Map()
296
+
297
+ for (const entry of parsedMaps) {
298
+ const resolvedSpec = resolveRenamedPath(entry.spec, renameMap)
299
+ for (const file of entry.files) {
300
+ const list = specsByImpactedFile.get(file) ?? []
301
+ list.push(resolvedSpec)
302
+ specsByImpactedFile.set(file, list)
303
+ }
304
+ }
305
+
306
+ const unmappedSourceChanges = sourceChanges.filter((change) => {
307
+ if (change.kind === "path") {
308
+ return !specsByImpactedFile.has(change.path)
309
+ }
310
+ return !specsByImpactedFile.has(change.oldPath) && !specsByImpactedFile.has(change.newPath)
311
+ })
312
+
313
+ if (unmappedSourceChanges.length > 0) {
314
+ console.warn("[auto] Unmapped source changes detected:")
315
+ for (const change of unmappedSourceChanges) {
316
+ if (change.kind === "path") {
317
+ console.warn(` - ${change.path}`)
318
+ } else {
319
+ console.warn(` - ${change.oldPath} -> ${change.newPath}`)
320
+ }
321
+ }
322
+ }
323
+
324
+ const selectedSpecs = new Set(directSpecChanges)
325
+ const triggersBySpec = new Map()
326
+
327
+ for (const spec of directSpecChanges) {
328
+ if (showMapping) {
329
+ triggersBySpec.set(spec, new Set([spec]))
330
+ }
331
+ }
332
+
333
+ for (const change of sourceChanges) {
334
+ if (change.kind === "path") {
335
+ const specs = specsByImpactedFile.get(change.path) ?? []
336
+ specs.forEach((spec) => selectedSpecs.add(spec))
337
+ if (showMapping) {
338
+ for (const spec of specs) {
339
+ const current = triggersBySpec.get(spec) ?? new Set()
340
+ current.add(change.path)
341
+ triggersBySpec.set(spec, current)
342
+ }
343
+ }
344
+ continue
345
+ }
346
+
347
+ const oldSpecs = specsByImpactedFile.get(change.oldPath) ?? []
348
+ oldSpecs.forEach((spec) => selectedSpecs.add(spec))
349
+ const newSpecs = specsByImpactedFile.get(change.newPath) ?? []
350
+ newSpecs.forEach((spec) => selectedSpecs.add(spec))
351
+ if (showMapping) {
352
+ const key = `${change.oldPath} -> ${change.newPath}`
353
+ const allSpecs = new Set([...oldSpecs, ...newSpecs])
354
+ for (const spec of allSpecs) {
355
+ const current = triggersBySpec.get(spec) ?? new Set()
356
+ current.add(key)
357
+ triggersBySpec.set(spec, current)
358
+ }
359
+ }
360
+ }
361
+
362
+ const missingSpecs = []
363
+ const specsToRun = Array.from(selectedSpecs)
364
+ .filter((spec) => {
365
+ const abs = path.join(workspaceRoot, spec)
366
+ if (fs.existsSync(abs)) {
367
+ return true
368
+ }
369
+ missingSpecs.push(spec)
370
+ return false
371
+ })
372
+ .sort()
373
+ if (missingSpecs.length > 0) {
374
+ console.warn(`[auto] Ignoring ${missingSpecs.length} missing spec file(s):`)
375
+ for (const spec of missingSpecs.sort()) {
376
+ console.warn(` - ${spec}`)
377
+ }
378
+ }
379
+
380
+ if (specsToRun.length === 0) {
381
+ console.log("[auto] No impacted specs.")
382
+ return null
383
+ }
384
+
385
+ if (showMapping) {
386
+ console.log("[auto] Mapping:")
387
+ for (const spec of specsToRun) {
388
+ const triggers = Array.from(triggersBySpec.get(spec) ?? []).sort()
389
+ if (triggers.length === 0) {
390
+ continue
391
+ }
392
+ console.log(` - ${spec}`)
393
+ for (const trigger of triggers) {
394
+ console.log(` <- ${trigger}`)
395
+ }
396
+ }
397
+ }
398
+
399
+ const playwrightFiles = specsToRun
400
+ .map((specWorkspacePath) => path.join(workspaceRoot, specWorkspacePath))
401
+ .filter((specAbs) => isSubpath(specAbs, config.rootDir))
402
+ .map((specAbs) => {
403
+ const specProjectPath = path.relative(config.rootDir, specAbs)
404
+ return resolvePlaywrightSpecFile(specProjectPath)
405
+ })
406
+
407
+ const totalSpecFiles = (await findSpecSourceFiles(config.rootDir)).length
408
+ console.log(`[auto] Running ${playwrightFiles.length}/${totalSpecFiles} spec file(s).`)
409
+ return [...userArgs, ...playwrightFiles]
410
+ }
411
+
412
+ async function findFilesMapJson(filesMapDir) {
413
+ const patterns = ["spec/**/*.spec.ts.json", "spec/**/*.spec.tsx.json"]
414
+ const matches = await fg(patterns, { cwd: filesMapDir, absolute: true, onlyFiles: true }).catch(() => [])
415
+ return matches.sort()
416
+ }
417
+
418
+ function getGitChanges(workspaceRoot) {
419
+ const result = spawnSync("git", ["status", "--porcelain=1", "-z"], {
420
+ cwd: workspaceRoot,
421
+ encoding: "utf8",
422
+ })
423
+
424
+ if (result.status !== 0) {
425
+ throw new Error(`[auto] Failed to read git status: ${result.stderr || "unknown error"}`)
426
+ }
427
+
428
+ const tokens = String(result.stdout ?? "").split("\0").filter(Boolean)
429
+ const changes = []
430
+
431
+ for (let i = 0; i < tokens.length; i += 1) {
432
+ const record = tokens[i]
433
+ if (record.length < 4) {
434
+ continue
435
+ }
436
+
437
+ const status = record.slice(0, 2)
438
+ const pathPart = toPosixPath(record.slice(3))
439
+
440
+ if (isRenameOrCopyStatus(status)) {
441
+ const next = tokens[i + 1]
442
+ if (typeof next !== "string") {
443
+ continue
444
+ }
445
+ changes.push({
446
+ kind: "rename",
447
+ oldPath: pathPart,
448
+ newPath: toPosixPath(next),
449
+ })
450
+ i += 1
451
+ continue
452
+ }
453
+
454
+ changes.push({
455
+ kind: "path",
456
+ path: pathPart,
457
+ })
458
+ }
459
+
460
+ return changes
461
+ }
462
+
463
+ function isRenameOrCopyStatus(status) {
464
+ return status.includes("R") || status.includes("C")
465
+ }
466
+
467
+ function resolveRenamedPath(original, renameMap) {
468
+ let current = original
469
+ const visited = new Set()
470
+
471
+ while (renameMap.has(current) && !visited.has(current)) {
472
+ visited.add(current)
473
+ current = renameMap.get(current)
474
+ }
475
+
476
+ return current
477
+ }
478
+
479
+ function isSubpath(candidate, root) {
480
+ const relative = path.relative(root, candidate)
481
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
482
+ }
483
+
484
+ function isSpecSourceFile(absolutePath, specRootAbsolute) {
485
+ if (!isSubpath(absolutePath, specRootAbsolute)) {
486
+ return false
487
+ }
488
+
489
+ return absolutePath.endsWith(".spec.ts")
490
+ || absolutePath.endsWith(".spec.tsx")
491
+ }
492
+
493
+ async function findSpecSourceFiles(projectRoot) {
494
+ const patterns = ["spec/**/*.spec.ts", "spec/**/*.spec.tsx"]
495
+ const matches = await fg(patterns, { cwd: projectRoot, absolute: true, onlyFiles: true })
496
+ return matches.sort()
497
+ }
498
+
499
+ function resolvePlaywrightSpecFile(specProjectPath) {
500
+ const buildSpecRoot = path.join(process.cwd(), "build", "spec")
501
+ const isBuildSpecProject = fs.existsSync(buildSpecRoot)
502
+
503
+ if (!isBuildSpecProject) {
504
+ return specProjectPath
505
+ }
506
+
507
+ const builtCandidate = normalizeBuiltSpecPath(path.join("build", specProjectPath))
508
+ const builtAbsolute = path.resolve(process.cwd(), builtCandidate)
509
+ if (!fs.existsSync(builtAbsolute)) {
510
+ throw new Error(`[specs-map] Missing built spec file: ${builtCandidate}`)
511
+ }
512
+
513
+ return builtCandidate
514
+ }
515
+
516
+ function normalizeBuiltSpecPath(filePath) {
517
+ if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
518
+ return `${filePath.replace(/\.tsx?$/, "")}.js`
519
+ }
520
+ return filePath
521
+ }
522
+
523
+ function toPosixPath(input) {
524
+ return String(input ?? "").split(path.sep).join("/")
525
+ }
526
+
527
+ function findWorkspaceRoot(projectRoot) {
528
+ let dir = path.resolve(projectRoot)
529
+
530
+ while (true) {
531
+ const pkgPath = path.join(dir, "package.json")
532
+ try {
533
+ if (fs.existsSync(pkgPath)) {
534
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
535
+ if (parsed && typeof parsed === "object" && parsed.workspaces) {
536
+ return dir
537
+ }
538
+ }
539
+ } catch {
540
+ //
541
+ }
542
+
543
+ const parent = path.dirname(dir)
544
+ if (parent === dir) {
545
+ return path.resolve(projectRoot)
546
+ }
547
+ dir = parent
548
+ }
549
+ }
550
+
551
+ function runPlaywright(userArgs, { disableCoverage = false } = {}) {
108
552
  // Determine config file path
109
553
  const configPath = fs.existsSync(
110
554
  path.join(process.cwd(), "playwright.config.ts"),
@@ -132,6 +576,9 @@ function runPlaywright(userArgs) {
132
576
  const launcher = resolvePlaywrightLauncher()
133
577
  const env = withRegisterShim(process.env)
134
578
  env[COMBINED_COVERAGE_ENV_VAR] = "1"
579
+ if (disableCoverage) {
580
+ env.RB_DISABLE_COVERAGE = "1"
581
+ }
135
582
 
136
583
  return spawnWithLogs({
137
584
  name: "Playwright",
@@ -9,8 +9,6 @@ import { build } from "esbuild"
9
9
 
10
10
  const DEFAULT_COVERAGE_CANDIDATES = [
11
11
  "spec/coverage.config.ts",
12
- "spec/coverage.config.mts",
13
- "spec/coverage.config.cts",
14
12
  "spec/coverage.config.js",
15
13
  "spec/coverage.config.mjs",
16
14
  "spec/coverage.config.cjs",
@@ -19,8 +17,6 @@ const DEFAULT_COVERAGE_CANDIDATES = [
19
17
 
20
18
  const LEGACY_COVERAGE_CANDIDATES = [
21
19
  "spec/coverage.ts",
22
- "spec/coverage.mts",
23
- "spec/coverage.cts",
24
20
  "spec/coverage.js",
25
21
  "spec/coverage.mjs",
26
22
  "spec/coverage.cjs",
@@ -78,7 +74,7 @@ async function importCoverageModule(filePath) {
78
74
  return JSON.parse(raw)
79
75
  }
80
76
 
81
- if ([".ts", ".mts", ".cts"].includes(ext)) {
77
+ if (ext === ".ts") {
82
78
  const compiledUrl = await compileTsModule(filePath)
83
79
  return loadModule(compiledUrl)
84
80
  }
@@ -120,7 +116,7 @@ async function loadModule(url) {
120
116
  return imported
121
117
  }
122
118
 
123
- const DEFAULT_COLLECT_COVERAGE_EXTENSIONS = "ts,tsx,js,jsx,mts,cts,mjs,cjs"
119
+ const DEFAULT_COLLECT_COVERAGE_EXTENSIONS = "ts,tsx,js,jsx,mjs,cjs"
124
120
  const DEFAULT_COLLECT_COVERAGE_TEST_EXCLUDE = `!**/*.test.{${DEFAULT_COLLECT_COVERAGE_EXTENSIONS}}`
125
121
 
126
122
  async function resolveCollectCoverageFrom(rawPatterns, rootDir) {
@@ -91,6 +91,30 @@ export async function generateCoverageReport(config) {
91
91
  }
92
92
  }
93
93
 
94
+ export async function collectCoveredFiles(config) {
95
+ const coverageFiles = await findCoverageFiles(config)
96
+ if (coverageFiles.length === 0) {
97
+ return []
98
+ }
99
+
100
+ const coverageLib = resolveCoverageLib()
101
+ const coverageMap = coverageLib.createCoverageMap({})
102
+ const matchesCollectCoverageFrom = createCollectCoverageMatcher(config.collectCoverageFrom, config.rootDir)
103
+
104
+ for (const file of coverageFiles) {
105
+ const payload = await readCoverageFile(file)
106
+ if (!payload) {
107
+ continue
108
+ }
109
+
110
+ for (const script of payload.scripts) {
111
+ await mergeScriptCoverage(coverageMap, script, config, matchesCollectCoverageFrom)
112
+ }
113
+ }
114
+
115
+ return coverageMap.files().sort()
116
+ }
117
+
94
118
  async function mergeScriptCoverage(coverageMap, script, config, matchesCollectCoverageFrom) {
95
119
  const scriptPath = script.absolutePath
96
120
  if (!scriptPath) {