-
-
Notifications
You must be signed in to change notification settings - Fork 268
/
index.ts
123 lines (107 loc) · 3.93 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { extname, join, isAbsolute, sep, posix } from 'path';
import { CoverageMapData } from 'istanbul-lib-coverage';
import v8toIstanbulLib from 'v8-to-istanbul';
import { TestRunnerCoreConfig, fetchSourceMap } from '@web/test-runner-core';
import { Profiler } from 'inspector';
import picoMatch from 'picomatch';
import LruCache from 'lru-cache';
import { readFile } from 'node:fs/promises';
import { toFilePath } from './utils';
type V8Coverage = Profiler.ScriptCoverage;
type Matcher = (test: string) => boolean;
type IstanbulSource = Required<Parameters<typeof v8toIstanbulLib>>[2];
const cachedMatchers = new Map<string, Matcher>();
// Cache the sourcemap/source objects to avoid repeatedly having to load
// them from disk per call
const cachedSources = new LruCache<string, IstanbulSource>({
maxSize: 1024 * 1024 * 50,
sizeCalculation: n => n.source.length,
});
// coverage base dir must be separated with "/"
const coverageBaseDir = process.cwd().split(sep).join('/');
function hasOriginalSource(source: IstanbulSource): boolean {
return (
'sourceMap' in source &&
source.sourceMap !== undefined &&
typeof source.sourceMap.sourcemap === 'object' &&
source.sourceMap.sourcemap !== null &&
Array.isArray(source.sourceMap.sourcemap.sourcesContent) &&
source.sourceMap.sourcemap.sourcesContent.length > 0
);
}
function getMatcher(patterns?: string[]) {
if (!patterns || patterns.length === 0) {
return () => true;
}
const key = patterns.join('');
let matcher = cachedMatchers.get(key);
if (!matcher) {
const resolvedPatterns = patterns.map(pattern =>
!isAbsolute(pattern) && !pattern.startsWith('*')
? posix.join(coverageBaseDir, pattern)
: pattern,
);
matcher = picoMatch(resolvedPatterns);
cachedMatchers.set(key, matcher);
}
return matcher;
}
export async function v8ToIstanbul(
config: TestRunnerCoreConfig,
testFiles: string[],
coverage: V8Coverage[],
userAgent?: string,
) {
const included = getMatcher(config?.coverageConfig?.include);
const excluded = getMatcher(config?.coverageConfig?.exclude);
const istanbulCoverage: CoverageMapData = {};
for (const entry of coverage) {
const url = new URL(entry.url);
const path = url.pathname;
if (
// ignore non-http protocols (for exmaple webpack://)
url.protocol.startsWith('http') &&
// ignore external urls
url.hostname === config.hostname &&
url.port === `${config.port}` &&
// ignore non-files
!!extname(path) &&
// ignore virtual files
!path.startsWith('/__web-test-runner') &&
!path.startsWith('/__web-dev-server')
) {
try {
const filePath = join(config.rootDir, toFilePath(path));
if (!testFiles.includes(filePath) && included(filePath) && !excluded(filePath)) {
const browserUrl = `${url.pathname}${url.search}${url.hash}`;
const cachedSource = cachedSources.get(browserUrl);
const sources =
cachedSource ??
((await fetchSourceMap({
protocol: config.protocol,
host: config.hostname,
port: config.port,
browserUrl,
userAgent,
})) as IstanbulSource);
if (!cachedSource) {
if (!hasOriginalSource(sources)) {
const contents = await readFile(filePath, 'utf8');
(sources as IstanbulSource & { originalSource: string }).originalSource = contents;
}
cachedSources.set(browserUrl, sources);
}
const converter = v8toIstanbulLib(filePath, 0, sources);
await converter.load();
converter.applyCoverage(entry.functions);
Object.assign(istanbulCoverage, converter.toIstanbul());
}
} catch (error) {
console.error(`Error while generating code coverage for ${entry.url}.`);
console.error(error);
}
}
}
return istanbulCoverage;
}
export { V8Coverage };