Skip to content

Commit c6d6a37

Browse files
committed
wip
1 parent 5ae1288 commit c6d6a37

File tree

9 files changed

+274
-116
lines changed

9 files changed

+274
-116
lines changed

LICENSE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Copyright (c) 2021 Lefty
1111
Copyright (c) 2019 Cypress.io <https://www.cypress.io>
1212
<https://github.com/cypress-io/code-coverage>
1313

14+
Copyright JS Foundation and other contributors
15+
<https://github.com/webpack-contrib/source-map-loader>
16+
1417
Permission is hereby granted, free of charge, to any person
1518
obtaining a copy of this software and associated documentation
1619
files (the "Software"), to deal in the Software without

src/lib/common/common-utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1+
const fs = require('fs/promises')
2+
const path = require('path')
13
const debug = require('debug')('code-coverage')
24

5+
const cacheDir = path.join(__dirname, '..', '..', '..', '.cache')
6+
7+
/**
8+
* @param {string} filename
9+
*/
10+
function exists(filename) {
11+
return fs
12+
.access(filename, fs.constants.F_OK)
13+
.then(() => true)
14+
.catch(() => false)
15+
}
16+
317
function stringToArray(prop, obj) {
418
if (typeof obj[prop] === 'string') {
519
obj[prop] = [obj[prop]]
@@ -66,6 +80,8 @@ const removePlaceholders = (coverage) => {
6680

6781
module.exports = {
6882
debug,
83+
cacheDir,
84+
exists,
6985
combineNycOptions,
7086
defaultNycOptions,
7187
fileCoveragePlaceholder,

src/lib/common/sourceMap.js

Lines changed: 164 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
const fs = require('fs/promises')
12
const { readFileSync } = require('fs')
3+
const { fileURLToPath } = require('url')
24
const { findSourceMap } = require('module')
3-
const { debug } = require('./common-utils')
5+
const { debug, cacheDir, exists } = require('./common-utils')
6+
const path = require('path')
47

58
/**
69
* @param {string} f
@@ -18,60 +21,178 @@ const lineLengths = (f) =>
1821
* }} SourceMap
1922
*/
2023

24+
// Matches only the last occurrence of sourceMappingURL
25+
const innerRegex = /\s*[#@]\s*sourceMappingURL\s*=\s*([^\s'"]*)\s*/
26+
27+
const sourceMappingURLRegex = RegExp(
28+
'(?:' +
29+
'/\\*' +
30+
'(?:\\s*\r?\n(?://)?)?' +
31+
'(?:' +
32+
innerRegex.source +
33+
')' +
34+
'\\s*' +
35+
'\\*/' +
36+
'|' +
37+
'//(?:' +
38+
innerRegex.source +
39+
')' +
40+
')' +
41+
'\\s*'
42+
)
43+
44+
/**
45+
* @see https://github.com/webpack-contrib/source-map-loader/blob/996368547e47a1a840f0d348b556084eb8442301/src/utils.js#L77
46+
* @param {string} code
47+
*/
48+
function getSourceMappingURL(code) {
49+
const lines = code.split(/^/m)
50+
let match
51+
52+
for (let i = lines.length - 1; i >= 0; i--) {
53+
match = lines[i].match(sourceMappingURLRegex)
54+
if (match) {
55+
break
56+
}
57+
}
58+
59+
const sourceMappingURL = match ? match[1] || match[2] || '' : null
60+
61+
return {
62+
sourceMappingURL: sourceMappingURL
63+
? decodeURI(sourceMappingURL)
64+
: sourceMappingURL,
65+
replacementString: match ? match[0] : null
66+
}
67+
}
68+
69+
/**
70+
* @param {string} url
71+
* @returns
72+
*/
73+
async function getContentFromUrl(url) {
74+
let code
75+
let filePath
76+
if (/^file:/.test(url)) {
77+
filePath = fileURLToPath(url)
78+
code = await (await fs.readFile(filePath)).toString('utf-8')
79+
} else if (/^https?:/.test(url)) {
80+
const parsedUrl = new URL(url)
81+
code = await (await fetch(url)).text()
82+
filePath = path.join(cacheDir, parsedUrl.pathname)
83+
await fs.writeFile(filePath, code)
84+
} else {
85+
return undefined
86+
}
87+
return { code, filePath }
88+
}
89+
90+
const cwd = process.cwd()
91+
2192
/**
22-
* @param {string} filePath
2393
* @param {string} url
24-
* @param {Record<string, SourceMap>} sourceMapCache
94+
* @param {Record<string, string>} hostToProjectMap
95+
* @param {Record<string, {filePath: string, sources: SourceMap}>} sourceMapCache
2596
* @returns
2697
*/
27-
function getSources(filePath, url, sourceMapCache = {}) {
98+
async function getSources(url, hostToProjectMap, sourceMapCache = {}) {
2899
if (sourceMapCache[url]) {
29100
return sourceMapCache[url]
30101
}
31-
// debug(`SOURCE MAP: ${url}`)
32-
// see if it has a source map
33-
const s = findSourceMap(filePath)
34-
35-
if (url.includes('.next/')) {
36-
console.warn(
37-
{
38-
s,
39-
f: filePath,
40-
url
41-
},
42-
'SOURCE MAP'
102+
let projectDir = cwd
103+
104+
let filePath
105+
let code
106+
let sourceMap
107+
if (/^file:/.test(url)) {
108+
filePath = fileURLToPath(url)
109+
code = (await fs.readFile(filePath)).toString('utf-8')
110+
111+
let { sourceMappingURL } = getSourceMappingURL(code)
112+
if (!sourceMappingURL) {
113+
return
114+
}
115+
sourceMappingURL = path.join(path.dirname(filePath), sourceMappingURL)
116+
sourceMap = JSON.parse(
117+
(await fs.readFile(sourceMappingURL)).toString('utf-8')
118+
)
119+
} else if (/^https?:/.test(url)) {
120+
code = await (await fetch(url)).text()
121+
const parsedUrl = new URL(url)
122+
123+
projectDir =
124+
hostToProjectMap?.[parsedUrl.hostname] ??
125+
hostToProjectMap?.[parsedUrl.host] ??
126+
hostToProjectMap?.[parsedUrl.origin] ??
127+
projectDir
128+
129+
filePath = path.resolve(
130+
path.join(cacheDir, parsedUrl.hostname, parsedUrl.pathname)
43131
)
132+
if (!(await exists(path.dirname(filePath)))) {
133+
await fs.mkdir(path.dirname(filePath), { recursive: true })
134+
}
135+
await fs.writeFile(filePath, code)
136+
137+
let { sourceMappingURL } = getSourceMappingURL(code)
138+
if (!sourceMappingURL) {
139+
return
140+
}
141+
if (!sourceMappingURL.startsWith('http')) {
142+
sourceMappingURL = new URL(sourceMappingURL, parsedUrl).href
143+
}
144+
const sourceMapString = await (await fetch(sourceMappingURL)).text()
145+
sourceMap = JSON.parse(sourceMapString)
146+
147+
await fs.writeFile(`${filePath}.map`, sourceMapString)
148+
} else {
149+
return undefined
44150
}
45-
if (s) {
46-
const { payload } = s
47-
/**
48-
* @type {SourceMap}
49-
*/
50-
const sources = { source: '' }
51-
/**
52-
* @type {{data: import('module').SourceMapPayload, lineLengths: number[]}}
53-
*/
54-
let sourceMapAndLineLengths = Object.assign(Object.create(null), {
55-
lineLengths: lineLengths(filePath),
56-
data: payload
57-
})
151+
const fileLineLengths = lineLengths(filePath)
152+
/**
153+
* @type {import('module').SourceMapPayload}
154+
*/
155+
const sourcemap = Object.assign(Object.create(null), {
156+
...sourceMap
157+
})
58158

59-
// See: https://github.com/nodejs/node/pull/34305
60-
if (sourceMapAndLineLengths?.data) {
61-
sources.sourceMap = {
62-
sourcemap: sourceMapAndLineLengths.data
63-
}
64-
if (sourceMapAndLineLengths.lineLengths) {
65-
let source = ''
66-
sourceMapAndLineLengths.lineLengths.forEach((length) => {
67-
source += `${''.padEnd(length, '.')}\n`
68-
})
69-
sources.source = source
159+
let modified = false
160+
161+
Object.assign(sourcemap, {
162+
sources: sourceMap.sources.map((sourceFile) => {
163+
if (sourceFile.startsWith('webpack://')) {
164+
// example: 'webpack://@pepper/order-management/./src/pages/index.page.tsx'
165+
const res = sourceFile.replace(
166+
/^webpack:\/\/[^.]+\/\./,
167+
`${projectDir}/.`
168+
)
169+
if (res !== sourceFile) {
170+
modified = true
171+
return res
172+
}
70173
}
71-
}
72-
sourceMapCache[url] = sources
73-
return sources
174+
return sourceFile
175+
})
176+
})
177+
178+
let source = ''
179+
if (fileLineLengths) {
180+
fileLineLengths.forEach((length) => {
181+
source += `${''.padEnd(length, '.')}\n`
182+
})
74183
}
184+
/**
185+
* @type {SourceMap}
186+
*/
187+
const sources = {
188+
sourceMap: {
189+
sourcemap: sourceMap
190+
},
191+
source
192+
}
193+
194+
sourceMapCache[url] = { filePath, sources }
195+
return { filePath, sources }
75196
}
76197

77198
module.exports = {

src/lib/common/v8ToIstanbul.js

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,71 @@
11
const fs = require('fs/promises')
2-
const { fileURLToPath } = require('url')
32
const libCoverage = require('istanbul-lib-coverage')
43
const v8toIstanbul = require('v8-to-istanbul')
54
const { getSources } = require('./sourceMap')
6-
const { debug } = require('./common-utils');
5+
const { debug, exists, cacheDir } = require('./common-utils')
76

87
/**
9-
* @param {import('devtools-protocol').Protocol.Profiler.TakePreciseCoverageResponse['result'][number]} obj
8+
* @param {import('devtools-protocol').Protocol.Profiler.TakePreciseCoverageResponse['result'][number]} obj
109
*/
11-
async function convertToIstanbul(obj, sourceMapCache = {}) {
12-
let filePath;
13-
let sources;
14-
if (/^file:/.test(obj.url)) {
15-
filePath = fileURLToPath(obj.url)
16-
sources = getSources(filePath, obj.url, sourceMapCache)
17-
}
18-
else if (/^https?:/.test(obj.url)) {
19-
20-
}
21-
22-
if (!filePath) {
23-
return null;
10+
async function convertToIstanbul(obj, hostToProjectMap, sourceMapCache = {}) {
11+
let res = await getSources(obj.url, hostToProjectMap, sourceMapCache)
12+
if (!res) {
13+
return null
2414
}
25-
26-
const converter = v8toIstanbul(filePath, undefined, sources)
15+
const { filePath, sources } = res
16+
const converter = v8toIstanbul(filePath, undefined, sources, (path) => {
17+
if (
18+
path.includes('/node_modules/') ||
19+
path.includes('/__cypress/') ||
20+
path.includes('/__/assets/')
21+
) {
22+
return true
23+
}
24+
return false
25+
})
2726
await converter.load()
2827
converter.applyCoverage(obj.functions)
2928
const coverage = converter.toIstanbul()
3029
converter.destroy()
3130
return coverage
3231
}
3332

34-
/**
35-
* @param {string} filename
36-
*/
37-
function exists(filename) {
38-
return fs
39-
.access(filename, fs.constants.F_OK)
40-
.then(() => true)
41-
.catch(() => false)
42-
}
43-
4433
/**
4534
* @see https://github.com/bcoe/c8/issues/376
4635
* @see https://github.com/tapjs/processinfo/blob/33c72e547139630cde35a4126bb4575ad7157065/lib/register-coverage.cjs
4736
* @param {Omit<import('devtools-protocol').Protocol.Profiler.TakePreciseCoverageResponse, 'timestamp'>} cov
37+
* @param {Record<string, string>} hostToProjectMap
4838
*/
49-
async function convertProfileCoverageToIstanbul(cov) {
50-
const map = libCoverage.createCoverageMap()
39+
async function convertProfileCoverageToIstanbul(cov, hostToProjectMap = {}) {
5140
// @ts-ignore
5241
const sourceMapCache = (cov['source-map-cache'] = {})
5342

43+
if (!(await exists(cacheDir))) {
44+
await fs.mkdir(cacheDir, { recursive: true })
45+
}
46+
5447
const coverages = await Promise.all(
5548
cov.result.map(async (obj) => {
56-
if (!/^file:/.test(obj.url)) {
57-
if (obj.url.includes('/__cypress/') || obj.url.includes('/__/assets/')) {
58-
return false
59-
}
49+
if (!/^file:/.test(obj.url) && !/^https?:/.test(obj.url)) {
50+
return null
6051
}
61-
// TODO
62-
if (obj.url.includes('/node_modules/')) {
63-
return false
52+
if (
53+
obj.url.includes('/node_modules/') ||
54+
obj.url.includes('/__cypress/') ||
55+
obj.url.includes('/__/assets/')
56+
) {
57+
return null
6458
}
65-
return convertToIstanbul(obj, sourceMapCache)
59+
return convertToIstanbul(obj, hostToProjectMap, sourceMapCache).catch(
60+
(err) => {
61+
console.error(err, `could not convert to istanbul - ${obj.url}`)
62+
return null
63+
}
64+
)
6665
})
6766
)
6867

68+
const map = libCoverage.createCoverageMap()
6969
coverages.reduce((_, coverage) => {
7070
if (coverage) {
7171
map.merge(coverage)
@@ -81,6 +81,5 @@ async function convertProfileCoverageToIstanbul(cov) {
8181
}
8282

8383
module.exports = {
84-
convertToIstanbul,
8584
convertProfileCoverageToIstanbul
8685
}

0 commit comments

Comments
 (0)