|
2 | 2 |
|
3 | 3 | process.bin = process.title = 'css-coverage'; |
4 | 4 |
|
5 | | -var fs = require('fs'); |
6 | | -var path = require('path'); |
7 | | -var childProcess = require('child_process'); |
8 | | -var phantomjs = require('phantomjs-prebuilt'); |
9 | | -var commander = require('commander'); |
10 | | -var SourceMapConsumer = require('source-map').SourceMapConsumer; |
11 | | -var cssTree = require('css-tree'); |
12 | | - |
13 | | - |
14 | | -function parseFileName(filePath) { |
15 | | - return path.resolve(process.cwd(), filePath); |
16 | | -} |
17 | | - |
18 | | -commander |
19 | | - // .usage('[options]') |
20 | | - .description('Generate coverage info for a CSS file against an HTML file. This supports loading sourcemaps by using the sourceMappingURL=FILENAME.map CSS comment') |
21 | | - .option('--html [path/to/file.html]', 'path to a local HTML file', parseFileName) // TODO: Support multiple |
22 | | - .option('--css [path/to/file.css]', 'path to a local CSS file', parseFileName) |
23 | | - .option('--lcov [path/to/output.lcov]', 'the LCOV output file', parseFileName) |
24 | | - .option('--verbose', 'verbose/debugging output') |
25 | | - .option('--ignore-source-map', 'disable loading the sourcemap if one is found') |
26 | | - .option('--cover-declarations', 'try to cover CSS declarations as well as selectors (best-effort, difficult with sourcemaps)') |
27 | | - .parse(process.argv); |
28 | | - |
29 | | -// Validate args |
30 | | -if (!commander.html && !commander.css) { |
31 | | - commander.help(); |
32 | | -} |
33 | | -if (commander.html) { |
34 | | - if (!fs.statSync(commander.html).isFile()) { |
35 | | - console.error('ERROR: Invalid argument. HTML file not found at ' + commander.html); |
36 | | - process.exit(1); |
37 | | - } |
38 | | -} else { |
39 | | - console.error('ERROR: Missing argument. At least 1 HTML file must be specified'); |
40 | | - process.exit(1); |
41 | | -} |
42 | | -if (commander.css) { |
43 | | - if (!fs.statSync(commander.css).isFile()) { |
44 | | - console.error('ERROR: Invalid argument. CSS file not found at ' + commander.css); |
45 | | - process.exit(1); |
46 | | - } |
47 | | -} else { |
48 | | - console.error('ERROR: Missing argument. A CSS file must be specified'); |
49 | | - process.exit(1); |
50 | | -} |
51 | | - |
52 | | -var CSS_STR = fs.readFileSync(commander.css, 'utf8'); |
53 | | -var ast; |
54 | | -try { |
55 | | - ast = cssTree.parse(CSS_STR, { filename: commander.css, positions: true }); |
56 | | -} catch (e) { |
57 | | - // CssSyntaxError |
58 | | - console.error('CssSyntaxError: ' + e.message + ' @ ' + e.line + ':' + e.column); |
59 | | - throw e; |
60 | | -} |
61 | | - |
62 | | -var cssForPhantom = []; |
63 | | -cssTree.walkRules(ast, (rule) => { |
64 | | - if ('Atrule' === rule.type) { |
65 | | - // ignore |
66 | | - } else if ('Rule' === rule.type) { |
67 | | - var converted = rule.prelude.children.map((selector) => { |
68 | | - return cssTree.translate(selector) |
69 | | - }) |
70 | | - cssForPhantom.push(converted) |
71 | | - } else { |
72 | | - throw new Error('BUG: Forgot to handle this rule subtype: ' + rule.type) |
73 | | - } |
74 | | -}) |
75 | | - |
76 | | -// Check if there is a sourceMappingURL |
77 | | -var sourceMapConsumer = null; |
78 | | -if (!commander.ignoreSourceMap && /sourceMappingURL=([^\ ]*)/.exec(CSS_STR)) { |
79 | | - var sourceMapPath = /sourceMappingURL=([^\ ]*)/.exec(CSS_STR)[1]; |
80 | | - sourceMapPath = path.resolve(path.dirname(commander.css), sourceMapPath); |
81 | | - if (commander.verbose) { |
82 | | - console.error('Using sourceMappingURL at ' + sourceMapPath); |
83 | | - } |
84 | | - var sourceMapStr = fs.readFileSync(sourceMapPath); |
85 | | - var sourceMap = JSON.parse(sourceMapStr); |
86 | | - sourceMapConsumer = new SourceMapConsumer(sourceMap); |
87 | | - |
88 | | - // sourceMapConsumer.eachMapping(function (m) { console.log(m.generatedLine, m.source); }); |
89 | | -} |
90 | | - |
91 | | - |
92 | | -var phantomCSSJSON = JSON.stringify(cssForPhantom); |
93 | | - |
94 | | -var coverageOutput = []; |
95 | | -var program = phantomjs.exec(path.resolve(__dirname, '../phantom-coverage.js'), require.resolve('sizzle'), commander.html, phantomCSSJSON); |
96 | | -program.stderr.pipe(process.stderr); |
97 | | -if (commander.verbose) { |
98 | | - program.stdout.pipe(process.stderr); |
99 | | -} |
100 | | -// Collect the coverage info that is written by the phantom script. |
101 | | -program.stdout.on('data', function(data) { |
102 | | - data.toString().split('\n').forEach(function (entry) { |
103 | | - if (entry.trim()) { |
104 | | - try { |
105 | | - var parsedJSON = JSON.parse(entry); |
106 | | - coverageOutput.push(parsedJSON); |
107 | | - } catch (e) { |
108 | | - console.warn(entry); |
109 | | - } |
110 | | - } |
111 | | - }); |
112 | | -}); |
113 | | -program.on('exit', function(code) { |
114 | | - // if success, then write out the LCOV file |
115 | | - if (code === 0) { |
116 | | - |
117 | | - var lcovStr = generateLcovStr(coverageOutput); |
118 | | - if (commander.lcov) { |
119 | | - fs.writeFileSync(commander.lcov, lcovStr); |
120 | | - } else { |
121 | | - console.log(lcovStr); |
122 | | - } |
123 | | - } |
124 | | - // do something on end |
125 | | - process.exit(code); |
126 | | -}); |
127 | | - |
128 | | - |
129 | | -function generateLcovStr(coverageOutput) { |
130 | | - // coverageOutput is of the form: |
131 | | - // [[1, ['body']], [400, ['div.foo']]] |
132 | | - // where each entry is a pair of count, selectors |
133 | | - var expected = cssForPhantom.length; |
134 | | - var actual = coverageOutput.length; |
135 | | - if (expected !== actual) { |
136 | | - throw new Error('BUG: count lengths do not match. Expected: ' + expected + ' Actual: ' + actual); |
137 | | - } |
138 | | - |
139 | | - var files = {}; // key is filename, value is [{startLine, endLine, count}] |
140 | | - var ret = []; // each line in the lcov file. Joined at the end of the function |
141 | | - |
142 | | - var cssLines = CSS_STR.split('\n'); |
143 | | - |
144 | | - function addCoverage(fileName, count, startLine, endLine) { |
145 | | - // add it to the files |
146 | | - if (!files[fileName]) { |
147 | | - files[fileName] = []; |
148 | | - } |
149 | | - files[fileName].push({startLine: startLine, endLine: endLine, count: count}); |
150 | | - } |
151 | | - |
152 | | - var i = -1; |
153 | | - cssTree.walkRules(ast, (rule, item, list) => { |
154 | | - if ('Rule' !== rule.type) { |
155 | | - return // Skip AtRules |
156 | | - } |
157 | | - |
158 | | - i += 1; |
159 | | - |
160 | | - var count = coverageOutput[i][0]; |
161 | | - var fileName; |
162 | | - var startLine; |
163 | | - var endLine; |
164 | | - // Look up the source map (if available) |
165 | | - if (sourceMapConsumer) { |
166 | | - // From https://github.com/mozilla/source-map#sourcemapconsumerprototypeoriginalpositionforgeneratedposition |
167 | | - // Could have been {line: rule.position.start.line, column: rule.positoin.start.column} |
168 | | - var origStart = rule.loc.start; |
169 | | - var origEnd = rule.loc.end; |
170 | | - |
171 | | - if (commander.coverDeclarations) { |
172 | | - |
173 | | - // Loop over every character between origStart and origEnd to make sure they are covered |
174 | | - // TODO: Do not duplicate-count lines just because this code runs character-by-character |
175 | | - var parseColumn = origStart.column; |
176 | | - for (var parseLine=origStart.line; parseLine <= origEnd.line; parseLine++) { |
177 | | - var curLineText = cssLines[parseLine - 1]; |
178 | | - for (var curColumn=parseColumn-1; curColumn < curLineText.length; curColumn++) { |
179 | | - var info = sourceMapConsumer.originalPositionFor({line: parseLine, column: curColumn}); |
180 | | - // stop processing when we hit origEnd |
181 | | - if (parseLine === origEnd.line && curColumn >= origEnd.column) { |
182 | | - break; |
183 | | - } |
184 | | - if (/\s/.test(curLineText[curColumn])) { |
185 | | - continue; |
186 | | - } |
187 | | - // console.error('PHIL ', curLineText[curColumn], {line: parseLine, column: curColumn}, info); |
188 | | - if (info.source) { |
189 | | - addCoverage(info.source, count, info.line, info.line); |
190 | | - } else { |
191 | | - if (commander.verbose) { |
192 | | - console.error('BUG: Could not look up source for this range:'); |
193 | | - console.error('origStart', origStart); |
194 | | - console.error('origEnd', origEnd); |
195 | | - console.error('currIndexes', {line: parseLine, column: curColumn}); |
196 | | - } |
197 | | - } |
198 | | - } |
199 | | - parseColumn = 1; |
200 | | - } |
201 | | - |
202 | | - |
203 | | - } else { |
204 | | - // Just cover the selectors |
205 | | - var startInfo = sourceMapConsumer.originalPositionFor({line: origStart.line, column: origStart.column-1}); |
206 | | - var endInfo = sourceMapConsumer.originalPositionFor({line: origEnd.line, column: origEnd.column-2}); |
207 | | - |
208 | | - // When there is no match, startInfo.source is null |
209 | | - if (!startInfo.source /*|| startInfo.source !== endInfo.source*/) { |
210 | | - console.error('cssStart', JSON.stringify(origStart)); |
211 | | - console.error('cssEnd', JSON.stringify(origEnd)); |
212 | | - // console.error('sourceStart', JSON.stringify(startInfo)); |
213 | | - // console.error('sourceEnd', JSON.stringify(endInfo)); |
214 | | - throw new Error('BUG: sourcemap might be invalid. Maybe try regenerating it?'); |
215 | | - } else { |
216 | | - if (commander.verbose) { |
217 | | - console.error('DEBUG: MATCHED this one', JSON.stringify(startInfo)); |
218 | | - } |
219 | | - } |
220 | | - |
221 | | - addCoverage(startInfo.source, count, startInfo.line, startInfo.line); |
222 | | - } |
223 | | - |
224 | | - |
225 | | - } else { |
226 | | - // No sourceMap available |
227 | | - fileName = commander.css; |
228 | | - startLine = rule.loc.start.line; |
229 | | - if (commander.coverDeclarations) { |
230 | | - endLine = rule.loc.end.line; |
231 | | - } else { |
232 | | - endLine = startLine; // Just do the selector (startLine) |
233 | | - } |
234 | | - addCoverage(fileName, count, startLine, endLine); |
235 | | - } |
236 | | - |
237 | | - }); |
238 | | - |
239 | | - for (var fileName in files) { |
240 | | - |
241 | | - var nonZero = 0; // For summary info |
242 | | - var allCounter = 0; |
243 | | - var fileNamePrefix = sourceMapPath ? path.dirname(sourceMapPath) : ''; |
244 | | - ret.push('SF:' + path.resolve(fileNamePrefix, fileName)); |
245 | | - |
246 | | - files[fileName].forEach(function(entry) { |
247 | | - var startLine = entry.startLine; |
248 | | - var endLine = entry.endLine; |
249 | | - var count = entry.count; |
250 | | - |
251 | | - for (var line=startLine; line <= endLine; line++) { |
252 | | - ret.push('DA:' + line + ',' + count); |
253 | | - if (count > 0) { |
254 | | - nonZero += 1; |
255 | | - } |
256 | | - allCounter += 1; |
257 | | - } |
258 | | - |
259 | | - }); |
260 | | - |
261 | | - // Include summary info for the file |
262 | | - ret.push('LH:' + nonZero); |
263 | | - ret.push('LF:' + allCounter); |
264 | | - ret.push('end_of_record'); |
265 | | - } |
266 | | - |
267 | | - return ret.join('\n'); |
268 | | -} |
| 5 | +require('../src/runCoverage') |
0 commit comments