UNPKG

orionsoft-react-scripts

Version:

Orionsoft Configuration and scripts for Create React App.

573 lines (518 loc) 22.2 kB
/* Copyright (c) 2012, Yahoo! Inc. All rights reserved. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*jshint maxlen: 300 */ var handlebars = require('handlebars').create(), defaults = require('./common/defaults'), path = require('path'), fs = require('fs'), util = require('util'), FileWriter = require('../util/file-writer'), Report = require('./index'), Store = require('../store'), InsertionText = require('../util/insertion-text'), TreeSummarizer = require('../util/tree-summarizer'), utils = require('../object-utils'), templateFor = function (name) { return handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates', name + '.txt'), 'utf8')); }, headerTemplate = templateFor('head'), footerTemplate = templateFor('foot'), detailTemplate = handlebars.compile([ '<tr>', '<td class="line-count quiet">{{#show_lines}}{{maxLines}}{{/show_lines}}</td>', '<td class="line-coverage quiet">{{#show_line_execution_counts fileCoverage}}{{maxLines}}{{/show_line_execution_counts}}</td>', '<td class="text"><pre class="prettyprint lang-js">{{#show_code structured}}{{/show_code}}</pre></td>', '</tr>\n' ].join('')), summaryTableHeader = [ '<div class="pad1">', '<table class="coverage-summary">', '<thead>', '<tr>', ' <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>', ' <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>', ' <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>', ' <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>', ' <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>', ' <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>', ' <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>', ' <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>', ' <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>', ' <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>', '</tr>', '</thead>', '<tbody>' ].join('\n'), summaryLineTemplate = handlebars.compile([ '<tr>', '<td class="file {{reportClasses.statements}}" data-value="{{file}}"><a href="{{output}}">{{file}}</a></td>', '<td data-value="{{metrics.statements.pct}}" class="pic {{reportClasses.statements}}"><div class="chart">{{#show_picture}}{{metrics.statements.pct}}{{/show_picture}}</div></td>', '<td data-value="{{metrics.statements.pct}}" class="pct {{reportClasses.statements}}">{{metrics.statements.pct}}%</td>', '<td data-value="{{metrics.statements.total}}" class="abs {{reportClasses.statements}}">{{metrics.statements.covered}}/{{metrics.statements.total}}</td>', '<td data-value="{{metrics.branches.pct}}" class="pct {{reportClasses.branches}}">{{metrics.branches.pct}}%</td>', '<td data-value="{{metrics.branches.total}}" class="abs {{reportClasses.branches}}">{{metrics.branches.covered}}/{{metrics.branches.total}}</td>', '<td data-value="{{metrics.functions.pct}}" class="pct {{reportClasses.functions}}">{{metrics.functions.pct}}%</td>', '<td data-value="{{metrics.functions.total}}" class="abs {{reportClasses.functions}}">{{metrics.functions.covered}}/{{metrics.functions.total}}</td>', '<td data-value="{{metrics.lines.pct}}" class="pct {{reportClasses.lines}}">{{metrics.lines.pct}}%</td>', '<td data-value="{{metrics.lines.total}}" class="abs {{reportClasses.lines}}">{{metrics.lines.covered}}/{{metrics.lines.total}}</td>', '</tr>\n' ].join('\n\t')), summaryTableFooter = [ '</tbody>', '</table>', '</div>' ].join('\n'), lt = '\u0001', gt = '\u0002', RE_LT = /</g, RE_GT = />/g, RE_AMP = /&/g, RE_lt = /\u0001/g, RE_gt = /\u0002/g; handlebars.registerHelper('show_picture', function (opts) { var num = Number(opts.fn(this)), rest, cls = ''; if (isFinite(num)) { if (num === 100) { cls = ' cover-full'; } num = Math.floor(num); rest = 100 - num; return '<div class="cover-fill' + cls + '" style="width: ' + num + '%;"></div>' + '<div class="cover-empty" style="width:' + rest + '%;"></div>'; } else { return ''; } }); handlebars.registerHelper('if_has_ignores', function (metrics, opts) { return (metrics.statements.skipped + metrics.functions.skipped + metrics.branches.skipped) === 0 ? '' : opts.fn(this); }); handlebars.registerHelper('show_ignores', function (metrics) { var statements = metrics.statements.skipped, functions = metrics.functions.skipped, branches = metrics.branches.skipped, result; if (statements === 0 && functions === 0 && branches === 0) { return '<span class="ignore-none">none</span>'; } result = []; if (statements >0) { result.push(statements === 1 ? '1 statement': statements + ' statements'); } if (functions >0) { result.push(functions === 1 ? '1 function' : functions + ' functions'); } if (branches >0) { result.push(branches === 1 ? '1 branch' : branches + ' branches'); } return result.join(', '); }); handlebars.registerHelper('show_lines', function (opts) { var maxLines = Number(opts.fn(this)), i, array = []; for (i = 0; i < maxLines; i += 1) { array[i] = i + 1; } return array.join('\n'); }); handlebars.registerHelper('show_line_execution_counts', function (context, opts) { var lines = context.l, maxLines = Number(opts.fn(this)), i, lineNumber, array = [], covered, value = ''; for (i = 0; i < maxLines; i += 1) { lineNumber = i + 1; value = '&nbsp;'; covered = 'neutral'; if (lines.hasOwnProperty(lineNumber)) { if (lines[lineNumber] > 0) { covered = 'yes'; value = lines[lineNumber] + '×'; } else { covered = 'no'; } } array.push('<span class="cline-any cline-' + covered + '">' + value + '</span>'); } return array.join('\n'); }); function customEscape(text) { text = text.toString(); return text.replace(RE_AMP, '&amp;') .replace(RE_LT, '&lt;') .replace(RE_GT, '&gt;') .replace(RE_lt, '<') .replace(RE_gt, '>'); } handlebars.registerHelper('show_code', function (context /*, opts */) { var array = []; context.forEach(function (item) { array.push(customEscape(item.text) || '&nbsp;'); }); return array.join('\n'); }); function title(str) { return ' title="' + str + '" '; } function annotateLines(fileCoverage, structuredText) { var lineStats = fileCoverage.l; if (!lineStats) { return; } Object.keys(lineStats).forEach(function (lineNumber) { var count = lineStats[lineNumber]; if (structuredText[lineNumber]) { structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no'; } }); structuredText.forEach(function (item) { if (item.covered === null) { item.covered = 'neutral'; } }); } function annotateStatements(fileCoverage, structuredText) { var statementStats = fileCoverage.s, statementMeta = fileCoverage.statementMap; Object.keys(statementStats).forEach(function (stName) { var count = statementStats[stName], meta = statementMeta[stName], type = count > 0 ? 'yes' : 'no', startCol = meta.start.column, endCol = meta.end.column + 1, startLine = meta.start.line, endLine = meta.end.line, openSpan = lt + 'span class="' + (meta.skip ? 'cstat-skip' : 'cstat-no') + '"' + title('statement not covered') + gt, closeSpan = lt + '/span' + gt, text; if (type === 'no') { if (endLine !== startLine) { endLine = startLine; endCol = structuredText[startLine].text.originalLength(); } text = structuredText[startLine].text; text.wrap(startCol, openSpan, startLine === endLine ? endCol : text.originalLength(), closeSpan); } }); } function annotateFunctions(fileCoverage, structuredText) { var fnStats = fileCoverage.f, fnMeta = fileCoverage.fnMap; if (!fnStats) { return; } Object.keys(fnStats).forEach(function (fName) { var count = fnStats[fName], meta = fnMeta[fName], type = count > 0 ? 'yes' : 'no', startCol = meta.loc.start.column, endCol = meta.loc.end.column + 1, startLine = meta.loc.start.line, endLine = meta.loc.end.line, openSpan = lt + 'span class="' + (meta.skip ? 'fstat-skip' : 'fstat-no') + '"' + title('function not covered') + gt, closeSpan = lt + '/span' + gt, text; if (type === 'no') { if (endLine !== startLine) { endLine = startLine; endCol = structuredText[startLine].text.originalLength(); } text = structuredText[startLine].text; text.wrap(startCol, openSpan, startLine === endLine ? endCol : text.originalLength(), closeSpan); } }); } function annotateBranches(fileCoverage, structuredText) { var branchStats = fileCoverage.b, branchMeta = fileCoverage.branchMap; if (!branchStats) { return; } Object.keys(branchStats).forEach(function (branchName) { var branchArray = branchStats[branchName], sumCount = branchArray.reduce(function (p, n) { return p + n; }, 0), metaArray = branchMeta[branchName].locations, i, count, meta, type, startCol, endCol, startLine, endLine, openSpan, closeSpan, text; if (sumCount > 0) { //only highlight if partial branches are missing for (i = 0; i < branchArray.length; i += 1) { count = branchArray[i]; meta = metaArray[i]; type = count > 0 ? 'yes' : 'no'; startCol = meta.start.column; endCol = meta.end.column + 1; startLine = meta.start.line; endLine = meta.end.line; openSpan = lt + 'span class="branch-' + i + ' ' + (meta.skip ? 'cbranch-skip' : 'cbranch-no') + '"' + title('branch not covered') + gt; closeSpan = lt + '/span' + gt; if (count === 0) { //skip branches taken if (endLine !== startLine) { endLine = startLine; endCol = structuredText[startLine].text.originalLength(); } text = structuredText[startLine].text; if (branchMeta[branchName].type === 'if') { // and 'if' is a special case since the else branch might not be visible, being non-existent text.insertAt(startCol, lt + 'span class="' + (meta.skip ? 'skip-if-branch' : 'missing-if-branch') + '"' + title((i === 0 ? 'if' : 'else') + ' path not taken') + gt + (i === 0 ? 'I' : 'E') + lt + '/span' + gt, true, false); } else { text.wrap(startCol, openSpan, startLine === endLine ? endCol : text.originalLength(), closeSpan); } } } } }); } function getReportClass(stats, watermark) { var coveragePct = stats.pct, identity = 1; if (coveragePct * identity === coveragePct) { return coveragePct >= watermark[1] ? 'high' : coveragePct >= watermark[0] ? 'medium' : 'low'; } else { return ''; } } function cleanPath(name) { var SEP = path.sep || '/'; return (SEP !== '/') ? name.split(SEP).join('/') : name; } function isEmptySourceStore(sourceStore) { if (!sourceStore) { return true; } var cache = sourceStore.sourceCache; return cache && !Object.keys(cache).length; } /** * a `Report` implementation that produces HTML coverage reports. * * Usage * ----- * * var report = require('istanbul').Report.create('html'); * * * @class HtmlReport * @extends Report * @module report * @constructor * @param {Object} opts optional * @param {String} [opts.dir] the directory in which to generate reports. Defaults to `./html-report` */ function HtmlReport(opts) { Report.call(this); this.opts = opts || {}; this.opts.dir = this.opts.dir || path.resolve(process.cwd(), 'html-report'); this.opts.sourceStore = isEmptySourceStore(this.opts.sourceStore) ? Store.create('fslookup') : this.opts.sourceStore; this.opts.linkMapper = this.opts.linkMapper || this.standardLinkMapper(); this.opts.writer = this.opts.writer || null; this.opts.templateData = { datetime: Date() }; this.opts.watermarks = this.opts.watermarks || defaults.watermarks(); } HtmlReport.TYPE = 'html'; util.inherits(HtmlReport, Report); Report.mix(HtmlReport, { synopsis: function () { return 'Navigable HTML coverage report for every file and directory'; }, getPathHtml: function (node, linkMapper) { var parent = node.parent, nodePath = [], linkPath = [], i; while (parent) { nodePath.push(parent); parent = parent.parent; } for (i = 0; i < nodePath.length; i += 1) { linkPath.push('<a href="' + linkMapper.ancestor(node, i + 1) + '">' + (cleanPath(nodePath[i].relativeName) || 'all files') + '</a>'); } linkPath.reverse(); return linkPath.length > 0 ? linkPath.join(' / ') + ' ' + cleanPath(node.displayShortName()) : '/'; }, fillTemplate: function (node, templateData) { var opts = this.opts, linkMapper = opts.linkMapper; templateData.entity = node.name || 'All files'; templateData.metrics = node.metrics; templateData.reportClass = getReportClass(node.metrics.statements, opts.watermarks.statements); templateData.pathHtml = this.getPathHtml(node, linkMapper); templateData.base = { css: linkMapper.asset(node, 'base.css') }; templateData.sorter = { js: linkMapper.asset(node, 'sorter.js'), image: linkMapper.asset(node, 'sort-arrow-sprite.png') }; templateData.prettify = { js: linkMapper.asset(node, 'prettify.js'), css: linkMapper.asset(node, 'prettify.css') }; }, writeDetailPage: function (writer, node, fileCoverage) { var opts = this.opts, sourceStore = opts.sourceStore, templateData = opts.templateData, sourceText = fileCoverage.code && Array.isArray(fileCoverage.code) ? fileCoverage.code.join('\n') + '\n' : sourceStore.get(fileCoverage.path), code = sourceText.split(/(?:\r?\n)|\r/), count = 0, structured = code.map(function (str) { count += 1; return { line: count, covered: null, text: new InsertionText(str, true) }; }), context; structured.unshift({ line: 0, covered: null, text: new InsertionText("") }); this.fillTemplate(node, templateData); writer.write(headerTemplate(templateData)); writer.write('<pre><table class="coverage">\n'); annotateLines(fileCoverage, structured); //note: order is important, since statements typically result in spanning the whole line and doing branches late //causes mismatched tags annotateBranches(fileCoverage, structured); annotateFunctions(fileCoverage, structured); annotateStatements(fileCoverage, structured); structured.shift(); context = { structured: structured, maxLines: structured.length, fileCoverage: fileCoverage }; writer.write(detailTemplate(context)); writer.write('</table></pre>\n'); writer.write(footerTemplate(templateData)); }, writeIndexPage: function (writer, node) { var linkMapper = this.opts.linkMapper, templateData = this.opts.templateData, children = Array.prototype.slice.apply(node.children), watermarks = this.opts.watermarks; children.sort(function (a, b) { return a.name < b.name ? -1 : 1; }); this.fillTemplate(node, templateData); writer.write(headerTemplate(templateData)); writer.write(summaryTableHeader); children.forEach(function (child) { var metrics = child.metrics, reportClasses = { statements: getReportClass(metrics.statements, watermarks.statements), lines: getReportClass(metrics.lines, watermarks.lines), functions: getReportClass(metrics.functions, watermarks.functions), branches: getReportClass(metrics.branches, watermarks.branches) }, data = { metrics: metrics, reportClasses: reportClasses, file: cleanPath(child.displayShortName()), output: linkMapper.fromParent(child) }; writer.write(summaryLineTemplate(data) + '\n'); }); writer.write(summaryTableFooter); writer.write(footerTemplate(templateData)); }, writeFiles: function (writer, node, dir, collector) { var that = this, indexFile = path.resolve(dir, 'index.html'), childFile; if (this.opts.verbose) { console.error('Writing ' + indexFile); } writer.writeFile(indexFile, function (contentWriter) { that.writeIndexPage(contentWriter, node); }); node.children.forEach(function (child) { if (child.kind === 'dir') { that.writeFiles(writer, child, path.resolve(dir, child.relativeName), collector); } else { childFile = path.resolve(dir, child.relativeName + '.html'); if (that.opts.verbose) { console.error('Writing ' + childFile); } writer.writeFile(childFile, function (contentWriter) { that.writeDetailPage(contentWriter, child, collector.fileCoverageFor(child.fullPath())); }); } }); }, standardLinkMapper: function () { return { fromParent: function (node) { var relativeName = cleanPath(node.relativeName); return node.kind === 'dir' ? relativeName + 'index.html' : relativeName + '.html'; }, ancestorHref: function (node, num) { var href = '', notDot = function(part) { return part !== '.'; }, separated, levels, i, j; for (i = 0; i < num; i += 1) { separated = cleanPath(node.relativeName).split('/').filter(notDot); levels = separated.length - 1; for (j = 0; j < levels; j += 1) { href += '../'; } node = node.parent; } return href; }, ancestor: function (node, num) { return this.ancestorHref(node, num) + 'index.html'; }, asset: function (node, name) { var i = 0, parent = node.parent; while (parent) { i += 1; parent = parent.parent; } return this.ancestorHref(node, i) + name; } }; }, writeReport: function (collector, sync) { var opts = this.opts, dir = opts.dir, summarizer = new TreeSummarizer(), writer = opts.writer || new FileWriter(sync), that = this, tree, copyAssets = function (subdir) { var srcDir = path.resolve(__dirname, '..', 'assets', subdir); fs.readdirSync(srcDir).forEach(function (f) { var resolvedSource = path.resolve(srcDir, f), resolvedDestination = path.resolve(dir, f), stat = fs.statSync(resolvedSource); if (stat.isFile()) { if (opts.verbose) { console.log('Write asset: ' + resolvedDestination); } writer.copyFile(resolvedSource, resolvedDestination); } }); }; collector.files().forEach(function (key) { summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key))); }); tree = summarizer.getTreeSummary(); [ '.', 'vendor'].forEach(function (subdir) { copyAssets(subdir); }); writer.on('done', function () { that.emit('done'); }); //console.log(JSON.stringify(tree.root, undefined, 4)); this.writeFiles(writer, tree.root, dir, collector); writer.done(); } }); module.exports = HtmlReport;