-
-
Notifications
You must be signed in to change notification settings - Fork 359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Caching #101
Caching #101
Changes from 10 commits
f4be86f
7eec3e0
ed1a9ee
84fce89
3f9952c
1d625e8
9b014bb
1343d4a
ca01f07
f7694b4
f4b9f24
dcef2dc
03de5e7
1c08d5b
0f73158
afedbfb
0319449
b71f8c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/* global __coverage__ */ | ||
var _ = require('lodash') | ||
var lazyRequire = require('lazy-req')(require) | ||
var fs = require('fs') | ||
var glob = require('glob') | ||
var micromatch = require('micromatch') | ||
|
@@ -9,47 +9,49 @@ var path = require('path') | |
var rimraf = require('rimraf') | ||
var onExit = require('signal-exit') | ||
var stripBom = require('strip-bom') | ||
var SourceMapCache = require('./lib/source-map-cache') | ||
var resolveFrom = require('resolve-from') | ||
var md5 = require('md5-hex') | ||
var arrify = require('arrify') | ||
var SourceMapCache = lazyRequire('./lib/source-map-cache') | ||
var convertSourceMap = require('convert-source-map') | ||
var istanbul = lazyRequire('istanbul') | ||
|
||
/* istanbul ignore next */ | ||
if (/index\.covered\.js$/.test(__filename)) { | ||
require('./lib/self-coverage-helper') | ||
} | ||
|
||
function NYC (opts) { | ||
_.extend(this, { | ||
subprocessBin: path.resolve( | ||
__dirname, | ||
'./bin/nyc.js' | ||
), | ||
tempDirectory: './.nyc_output', | ||
cwd: process.env.NYC_CWD || process.cwd(), | ||
reporter: 'text', | ||
istanbul: require('istanbul'), | ||
sourceMapCache: new SourceMapCache(), | ||
require: [] | ||
}, opts) | ||
|
||
if (!Array.isArray(this.reporter)) this.reporter = [this.reporter] | ||
opts = opts || {} | ||
|
||
this._istanbul = opts.istanbul | ||
this.subprocessBin = opts.subprocessBin || path.resolve(__dirname, './bin/nyc.js') | ||
this._tempDirectory = opts.tempDirectory || './.nyc_output' | ||
this._cacheDirectory = opts.cacheDirectory || './node_modules/.cache/nyc' | ||
this.cwd = opts.cwd || process.env.NYC_CWD || process.cwd() | ||
this.reporter = arrify(opts.reporter || 'text') | ||
|
||
// you can specify config in the nyc stanza of package.json. | ||
var config = require(path.resolve(this.cwd, './package.json')).config || {} | ||
config = config.nyc || {} | ||
|
||
// load exclude stanza from config. | ||
this.include = config.include || ['**'] | ||
this.include = this._prepGlobPatterns(this.include) | ||
this.include = false | ||
if (config.include) { | ||
this.include = this._prepGlobPatterns(arrify(config.include)) | ||
} | ||
|
||
this.exclude = ['**/node_modules/**'].concat(config.exclude || ['test/**', 'test{,-*}.js']) | ||
if (!Array.isArray(this.exclude)) this.exclude = [this.exclude] | ||
this.exclude = this._prepGlobPatterns(this.exclude) | ||
this.exclude = this._prepGlobPatterns( | ||
['**/node_modules/**'].concat(arrify(config.exclude || ['test/**', 'test{,-*}.js'])) | ||
) | ||
|
||
// require extensions can be provided as config in package.json. | ||
this.require = config.require ? config.require : this.require | ||
this.require = arrify(config.require || opts.require) | ||
|
||
this.instrumenter = this._createInstrumenter() | ||
this._createOutputDirectory() | ||
|
||
this.hashCache = {} | ||
this.loadedMaps = null | ||
} | ||
|
||
NYC.prototype._loadAdditionalModules = function () { | ||
|
@@ -67,14 +69,20 @@ NYC.prototype._loadAdditionalModules = function () { | |
}) | ||
} | ||
|
||
NYC.prototype.instrumenter = function () { | ||
return this._instrumenter || (this._instrumenter = this._createInstrumenter()) | ||
} | ||
|
||
NYC.prototype._createInstrumenter = function () { | ||
var configFile = path.resolve(this.cwd, './.istanbul.yml') | ||
|
||
if (!fs.existsSync(configFile)) configFile = undefined | ||
|
||
var instrumenterConfig = this.istanbul.config.loadFile(configFile).instrumentation.config | ||
var istanbul = this.istanbul() | ||
|
||
var instrumenterConfig = istanbul.config.loadFile(configFile).instrumentation.config | ||
|
||
return new this.istanbul.Instrumenter({ | ||
return new istanbul.Instrumenter({ | ||
coverageVariable: '__coverage__', | ||
embedSource: instrumenterConfig['embed-source'], | ||
noCompact: !instrumenterConfig.compact, | ||
|
@@ -85,41 +93,41 @@ NYC.prototype._createInstrumenter = function () { | |
NYC.prototype._prepGlobPatterns = function (patterns) { | ||
if (!patterns) return patterns | ||
|
||
var directories = [] | ||
patterns = _.map(patterns, function (pattern) { | ||
var result = [] | ||
|
||
function add (pattern) { | ||
if (result.indexOf(pattern) === -1) { | ||
result.push(pattern) | ||
} | ||
} | ||
|
||
patterns.forEach(function (pattern) { | ||
// Allow gitignore style of directory exclusion | ||
if (!_.endsWith(pattern, '/**')) { | ||
directories.push(pattern.replace(/\/$/, '').concat('/**')) | ||
if (!/\/\*\*$/.test(pattern)) { | ||
add(pattern.replace(/\/$/, '') + '/**') | ||
} | ||
|
||
return pattern | ||
add(pattern) | ||
}) | ||
return _.union(patterns, directories) | ||
|
||
return result | ||
} | ||
|
||
NYC.prototype.addFile = function (filename, returnImmediately) { | ||
NYC.prototype.addFile = function (filename) { | ||
var relFile = path.relative(this.cwd, filename) | ||
var instrument = this.shouldInstrumentFile(filename, relFile) | ||
var content = stripBom(fs.readFileSync(filename, 'utf8')) | ||
|
||
if (instrument) { | ||
this.sourceMapCache.add(filename, content) | ||
content = this.instrumenter.instrumentSync(content, './' + relFile) | ||
} else if (returnImmediately) { | ||
return {} | ||
} | ||
|
||
var source = stripBom(fs.readFileSync(filename, 'utf8')) | ||
var instrumentedSource = this._maybeInstrumentSource(source, filename, relFile) | ||
return { | ||
instrument: instrument, | ||
content: content, | ||
relFile: relFile | ||
instrument: !!instrumentedSource, | ||
relFile: relFile, | ||
content: instrumentedSource || source | ||
} | ||
} | ||
|
||
NYC.prototype.shouldInstrumentFile = function (filename, relFile) { | ||
relFile = relFile.replace(/^\.\//, '') // remove leading './'. | ||
|
||
return (micromatch.any(filename, this.include) || micromatch.any(relFile, this.include)) && | ||
return (!this.include || micromatch.any(filename, this.include) || micromatch.any(relFile, this.include)) && | ||
!(micromatch.any(filename, this.exclude) || micromatch.any(relFile, this.exclude)) | ||
} | ||
|
||
|
@@ -132,7 +140,7 @@ NYC.prototype.addAllFiles = function () { | |
var obj = _this.addFile(filename, true) | ||
if (obj.instrument) { | ||
module._compile( | ||
_this.instrumenter.getPreamble(obj.content, obj.relFile), | ||
_this.instrumenter().getPreamble(obj.content, obj.relFile), | ||
filename | ||
) | ||
} | ||
|
@@ -141,29 +149,46 @@ NYC.prototype.addAllFiles = function () { | |
this.writeCoverageFile() | ||
} | ||
|
||
NYC.prototype._maybeInstrumentSource = function (code, filename, relFile) { | ||
var instrument = this.shouldInstrumentFile(filename, relFile) | ||
|
||
if (!instrument) { | ||
return null | ||
} | ||
|
||
var hash = md5(code) | ||
this.hashCache['./' + relFile] = hash | ||
var cacheFilePath = path.join(this.cacheDirectory(), hash + '.js') | ||
|
||
try { | ||
return fs.readFileSync(cacheFilePath, 'utf8') | ||
} catch (e) { | ||
var sourceMap = convertSourceMap.fromSource(code) || convertSourceMap.fromMapFileSource(code, path.dirname(filename)) | ||
if (sourceMap) { | ||
var mapPath = path.join(this.cacheDirectory(), hash + '.map') | ||
fs.writeFileSync(mapPath, sourceMap.toJSON()) | ||
} | ||
var instrumented = this.instrumenter().instrumentSync(code, './' + relFile) | ||
fs.writeFileSync(cacheFilePath, instrumented) | ||
return instrumented | ||
} | ||
} | ||
|
||
NYC.prototype._wrapRequire = function () { | ||
var _this = this | ||
appendTransform(function (code, filename) { | ||
var relFile = path.relative(_this.cwd, filename) | ||
var instrument = _this.shouldInstrumentFile(filename, relFile) | ||
|
||
if (!instrument) { | ||
return code | ||
} | ||
|
||
_this.sourceMapCache.add(filename, code) | ||
|
||
// now instrument the compiled code. | ||
return _this.instrumenter.instrumentSync(code, './' + relFile) | ||
return _this._maybeInstrumentSource(code, filename, relFile) || code | ||
}) | ||
} | ||
|
||
NYC.prototype.cleanup = function () { | ||
if (!process.env.NYC_CWD) rimraf.sync(this.tmpDirectory()) | ||
if (!process.env.NYC_CWD) rimraf.sync(this.tempDirectory()) | ||
} | ||
|
||
NYC.prototype._createOutputDirectory = function () { | ||
mkdirp.sync(this.tmpDirectory()) | ||
mkdirp.sync(this.tempDirectory()) | ||
mkdirp.sync(this.cacheDirectory()) | ||
} | ||
|
||
NYC.prototype._wrapExit = function () { | ||
|
@@ -188,18 +213,29 @@ NYC.prototype.writeCoverageFile = function () { | |
if (typeof __coverage__ === 'object') coverage = __coverage__ | ||
if (!coverage) return | ||
|
||
Object.keys(coverage).forEach(function (relFile) { | ||
if (this.hashCache[relFile] && coverage[relFile]) { | ||
coverage[relFile].contentHash = this.hashCache[relFile] | ||
} | ||
}, this) | ||
|
||
fs.writeFileSync( | ||
path.resolve(this.tmpDirectory(), './', process.pid + '.json'), | ||
JSON.stringify(this.sourceMapCache.applySourceMaps(coverage)), | ||
path.resolve(this.tempDirectory(), './', process.pid + '.json'), | ||
JSON.stringify(coverage), | ||
'utf-8' | ||
) | ||
} | ||
|
||
NYC.prototype.istanbul = function () { | ||
return this._istanbul || (this._istanbul = istanbul()) | ||
} | ||
|
||
NYC.prototype.report = function (cb, _collector, _reporter) { | ||
cb = cb || function () {} | ||
|
||
var collector = _collector || new this.istanbul.Collector() | ||
var reporter = _reporter || new this.istanbul.Reporter() | ||
var istanbul = this.istanbul() | ||
var collector = _collector || new istanbul.Collector() | ||
var reporter = _reporter || new istanbul.Reporter() | ||
|
||
this._loadReports().forEach(function (report) { | ||
collector.add(report) | ||
|
@@ -214,22 +250,54 @@ NYC.prototype.report = function (cb, _collector, _reporter) { | |
|
||
NYC.prototype._loadReports = function () { | ||
var _this = this | ||
var files = fs.readdirSync(this.tmpDirectory()) | ||
var files = fs.readdirSync(this.tempDirectory()) | ||
|
||
var sourceMapCache = SourceMapCache()() | ||
|
||
var cacheDir = _this.cacheDirectory() | ||
|
||
return _.map(files, function (f) { | ||
var loadedMaps = this.loadedMaps || (this.loadedMaps = {}) | ||
|
||
return files.map(function (f) { | ||
var report | ||
try { | ||
return JSON.parse(fs.readFileSync( | ||
path.resolve(_this.tmpDirectory(), './', f), | ||
report = JSON.parse(fs.readFileSync( | ||
path.resolve(_this.tempDirectory(), './', f), | ||
'utf-8' | ||
)) | ||
} catch (e) { // handle corrupt JSON output. | ||
return {} | ||
} | ||
|
||
Object.keys(report).forEach(function (relFile) { | ||
var fileReport = report[relFile] | ||
if (fileReport && fileReport.contentHash) { | ||
var hash = fileReport.contentHash | ||
if (!(hash in loadedMaps)) { | ||
try { | ||
var mapPath = path.join(cacheDir, hash + '.map') | ||
loadedMaps[hash] = fs.readFileSync(mapPath, 'utf8') | ||
} catch (e) { | ||
// set to false to avoid repeatedly trying to load the map | ||
loadedMaps[hash] = false | ||
} | ||
} | ||
if (loadedMaps[hash]) { | ||
sourceMapCache.addMap(relFile, loadedMaps[hash]) | ||
} | ||
} | ||
}) | ||
report = sourceMapCache.applySourceMaps(report) | ||
return report | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could just return the result of |
||
}) | ||
} | ||
|
||
NYC.prototype.tmpDirectory = function () { | ||
return path.resolve(this.cwd, './', this.tempDirectory) | ||
NYC.prototype.tempDirectory = function () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 dropping |
||
return path.resolve(this.cwd, './', this._tempDirectory) | ||
} | ||
|
||
NYC.prototype.cacheDirectory = function () { | ||
return path.resolve(this.cwd, './', this._cacheDirectory) | ||
} | ||
|
||
NYC.prototype.mungeArgs = function (yargv) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,9 @@ var convertSourceMap = require('convert-source-map') | |
var SourceMapConsumer = require('source-map').SourceMapConsumer | ||
|
||
function SourceMapCache (opts) { | ||
if (!(this instanceof SourceMapCache)) { | ||
return new SourceMapCache(opts) | ||
} | ||
_.extend(this, { | ||
cache: {}, | ||
cwd: process.env.NYC_CWD || process.cwd() | ||
|
@@ -15,6 +18,10 @@ SourceMapCache.prototype.add = function (filename, source) { | |
if (sourceMap) this.cache['./' + path.relative(this.cwd, filename)] = new SourceMapConsumer(sourceMap.sourcemap) | ||
} | ||
|
||
SourceMapCache.prototype.addMap = function (relFile, mapJson) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer need There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Except in your tests I think. |
||
this.cache[relFile] = new SourceMapConsumer(JSON.parse(mapJson)) | ||
} | ||
|
||
SourceMapCache.prototype.applySourceMaps = function (coverage) { | ||
var _this = this | ||
var mappedCoverage = _.cloneDeep(coverage) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of a misnomer now.