-
-
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 9 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,4 @@ | ||
/* global __coverage__ */ | ||
var _ = require('lodash') | ||
var fs = require('fs') | ||
var glob = require('glob') | ||
var micromatch = require('micromatch') | ||
|
@@ -9,46 +8,43 @@ 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 convertSourceMap = require('convert-source-map') | ||
var endsWith = require('ends-with') | ||
|
||
/* 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 = ['**/node_modules/**'].concat(arrify(config.exclude || ['test/**', 'test{,-*}.js'])) | ||
this.exclude = this._prepGlobPatterns(this.exclude) | ||
|
||
// 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() | ||
} | ||
|
||
|
@@ -67,14 +63,18 @@ 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 instrumenterConfig = this.istanbul().config.loadFile(configFile).instrumentation.config | ||
|
||
return new this.istanbul.Instrumenter({ | ||
return new (this.istanbul()).Instrumenter({ | ||
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. Assign |
||
coverageVariable: '__coverage__', | ||
embedSource: instrumenterConfig['embed-source'], | ||
noCompact: !instrumenterConfig.compact, | ||
|
@@ -85,41 +85,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 (!endsWith(pattern, '/**')) { | ||
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. Why not |
||
add(pattern.replace(/\/$/, '').concat('/**')) | ||
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. Apparently Also, apparently it exists! 😉 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. (And yes you didn't write |
||
} | ||
|
||
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 content = this._addSource(source, filename, relFile) | ||
return { | ||
instrument: instrument, | ||
content: content, | ||
relFile: relFile | ||
instrument: !!content, | ||
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. This is pretty subtle (as is |
||
relFile: relFile, | ||
content: content || 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 +132,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,20 +141,38 @@ NYC.prototype.addAllFiles = function () { | |
this.writeCoverageFile() | ||
} | ||
|
||
var hashCache = {} | ||
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. I suppose it doesn't matter in practice but may be better to have this on the instance? |
||
|
||
NYC.prototype._addSource = function (code, filename, relFile) { | ||
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. If you rename this to |
||
var instrument = this.shouldInstrumentFile(filename, relFile) | ||
|
||
if (!instrument) { | ||
return null | ||
} | ||
|
||
var hash = md5(code) | ||
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._addSource(code, filename, relFile) || code | ||
}) | ||
} | ||
|
||
|
@@ -163,7 +181,8 @@ NYC.prototype.cleanup = function () { | |
} | ||
|
||
NYC.prototype._createOutputDirectory = 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. This is a bit of a misnomer now. |
||
mkdirp.sync(this.tmpDirectory()) | ||
mkdirp.sync(this.tempDirectory()) | ||
mkdirp.sync(this.cacheDirectory()) | ||
} | ||
|
||
NYC.prototype._wrapExit = function () { | ||
|
@@ -188,18 +207,28 @@ NYC.prototype.writeCoverageFile = function () { | |
if (typeof __coverage__ === 'object') coverage = __coverage__ | ||
if (!coverage) return | ||
|
||
Object.keys(coverage).forEach(function (relFile) { | ||
if (hashCache[relFile] && coverage[relFile]) { | ||
coverage[relFile].contentHash = hashCache[relFile] | ||
} | ||
}) | ||
|
||
fs.writeFileSync( | ||
path.resolve(this.tmpDirectory(), './', process.pid + '.json'), | ||
JSON.stringify(this.sourceMapCache.applySourceMaps(coverage)), | ||
JSON.stringify(coverage), | ||
'utf-8' | ||
) | ||
} | ||
|
||
NYC.prototype.istanbul = function () { | ||
return this._istanbul || (this._istanbul = require('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 collector = _collector || new (this.istanbul()).Collector() | ||
var reporter = _reporter || new (this.istanbul()).Reporter() | ||
|
||
this._loadReports().forEach(function (report) { | ||
collector.add(report) | ||
|
@@ -212,24 +241,58 @@ NYC.prototype.report = function (cb, _collector, _reporter) { | |
reporter.write(collector, true, cb) | ||
} | ||
|
||
var loadedMaps = {} | ||
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. Same concern here as for |
||
|
||
NYC.prototype._loadReports = function () { | ||
var _this = this | ||
var files = fs.readdirSync(this.tmpDirectory()) | ||
|
||
return _.map(files, function (f) { | ||
var SourceMapCache = require('./lib/source-map-cache') | ||
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. Did you move the require statement down here because 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. yep |
||
var sourceMapCache = new SourceMapCache() | ||
|
||
var cacheDir = _this.cacheDirectory() | ||
|
||
return files.map(function (f) { | ||
var report | ||
try { | ||
return JSON.parse(fs.readFileSync( | ||
report = JSON.parse(fs.readFileSync( | ||
path.resolve(_this.tmpDirectory(), './', f), | ||
'utf-8' | ||
)) | ||
} catch (e) { // handle corrupt JSON output. | ||
return {} | ||
} | ||
|
||
if (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. This condition seems unnecessary. If the file no longer exists, or did not contain valid JSON, we'll already have returned. The other option is that it contains valid JSON but it's not an object. There's plenty other ways the report could be invalid for the code that follows. |
||
Object.keys(report).forEach(function (relFile) { | ||
var fileReport = report[relFile] | ||
if (fileReport && fileReport.contentHash) { | ||
var hash = fileReport.contentHash | ||
if (!(loadedMaps[hash] || (loadedMaps[hash] === false))) { | ||
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. Why the second 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. Oh wait that prevents multiple attempts to read the source map. Maybe rewrite to 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. Or just |
||
try { | ||
var mapPath = path.join(cacheDir, hash + '.map') | ||
loadedMaps[hash] = fs.readFileSync(mapPath, 'utf8') | ||
} catch (e) { | ||
loadedMaps[hash] = false | ||
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 add a comment "set to false to avoid repeatedly trying to load the map" |
||
} | ||
} | ||
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.tmpDirectory = 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.
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.
|
||
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 |
---|---|---|
|
@@ -15,6 +15,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.
Could call
this._prepGlobPatterns()
here rather than reassigningthis.exclude
?