Skip to content
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

Transpile in the main process #945

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var AvaFiles = require('ava-files');
var AvaError = require('./lib/ava-error');
var fork = require('./lib/fork');
var CachingPrecompiler = require('./lib/caching-precompiler');
var Precompiler = require('./lib/precompiler');
var RunStatus = require('./lib/run-status');

function Api(options) {
Expand Down Expand Up @@ -48,8 +49,15 @@ module.exports = Api;

Api.prototype._runFile = function (file, runStatus) {
var hash = this.precompiler.precompileFile(file);
var precompiled = {};
precompiled[file] = hash;

var precompiled;

if (this.options.precompile) {
precompiled = runStatus.precompiler.createHash(file, hash, this.precompiler.getDetectiveMetadata(hash));
} else {
precompiled = {};
precompiled[file] = hash;
}

var options = objectAssign({}, this.options, {
precompiled: precompiled
Expand Down Expand Up @@ -122,6 +130,10 @@ Api.prototype._run = function (files, _options) {
self.precompiler = new CachingPrecompiler(cacheDir, self.options.babelConfig);
self.fileCount = files.length;

if (self.options.precompile) {
runStatus.precompiler = new Precompiler(cacheDir);
}

var overwatch;
if (this.options.concurrency > 0) {
overwatch = this._runLimitedPool(files, runStatus, self.options.serial ? 1 : this.options.concurrency);
Expand Down
8 changes: 6 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ var cli = meow([
' --source, -S Pattern to match source files so tests can be re-run (Can be repeated)',
' --timeout, -T Set global timeout',
' --concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL)',
' --precompile, -p Precompile sources in the main thread (EXPERIMENTAL)',
'',
'Examples',
' ava',
Expand All @@ -91,7 +92,8 @@ var cli = meow([
'verbose',
'serial',
'tap',
'watch'
'watch',
'precompile'
],
default: conf,
alias: {
Expand All @@ -103,7 +105,8 @@ var cli = meow([
w: 'watch',
S: 'source',
T: 'timeout',
c: 'concurrency'
c: 'concurrency',
p: 'precompile'
}
});

Expand All @@ -125,6 +128,7 @@ if (
var api = new Api({
failFast: cli.flags.failFast,
serial: cli.flags.serial,
precompile: cli.flags.precompile,
require: arrify(cli.flags.require),
cacheEnabled: cli.flags.cache !== false,
explicitTitles: cli.flags.watch,
Expand Down
3 changes: 2 additions & 1 deletion lib/babel-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ var defaultPlugins = lazy(function () {
espowerPlugin(),
require('babel-plugin-ava-throws-helper'),
rewritePlugin(),
require('babel-plugin-transform-runtime')
require('babel-plugin-transform-runtime'),
require('babel-plugin-detective')
];
});

Expand Down
29 changes: 28 additions & 1 deletion lib/caching-precompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function CachingPrecompiler(cacheDirPath, babelConfig) {
this.babelConfig = babelConfigHelper.validate(babelConfig);
this.cacheDirPath = cacheDirPath;
this.fileHashes = {};
this.detectiveMetadataResults = {};

Object.keys(CachingPrecompiler.prototype).forEach(function (name) {
this[name] = this[name].bind(this);
Expand Down Expand Up @@ -45,6 +46,7 @@ CachingPrecompiler.prototype._factory = function () {

CachingPrecompiler.prototype._init = function () {
this.babel = require('babel-core');
this.detective = require('babel-plugin-detective');
};

CachingPrecompiler.prototype._transform = function (code, filePath, hash) {
Expand All @@ -54,9 +56,19 @@ CachingPrecompiler.prototype._transform = function (code, filePath, hash) {
var result = this.babel.transform(code, options);

// save source map
var mapPath = path.join(this.cacheDirPath, hash + '.js.map');
var mapPath = this.cachePath(hash + '.js.map');
fs.writeFileSync(mapPath, JSON.stringify(result.map));

// save detective results
var detectiveMetadata = this.detective.metadata(result) || {};
var serializableDetectiveMetadata = {
strings: detectiveMetadata.strings || [],
expressions: Boolean(detectiveMetadata.expressions && detectiveMetadata.expressions.length)
};
var detectiveResultsPath = this.cachePath(hash + '.detective.json');
fs.writeFileSync(detectiveResultsPath, JSON.stringify(serializableDetectiveMetadata));
this.detectiveMetadataResults[hash] = serializableDetectiveMetadata;

// When loading the test file, test workers intercept the require call and
// load the cached code instead. Libraries like nyc may also be intercepting
// require calls, however they won't know that different code was loaded.
Expand All @@ -73,6 +85,10 @@ CachingPrecompiler.prototype._transform = function (code, filePath, hash) {
return result.code + '\n' + comment;
};

CachingPrecompiler.prototype.cachePath = function (filename) {
return path.join(this.cacheDirPath, filename);
};

CachingPrecompiler.prototype._createTransform = function () {
var salt = packageHash.sync(
[require.resolve('../package.json')].concat(babelConfigHelper.pluginPackages),
Expand All @@ -94,3 +110,14 @@ CachingPrecompiler.prototype._generateHash = function (code, filePath, salt) {

return hash;
};

CachingPrecompiler.prototype.getDetectiveMetadata = function (hash) {
var metadata = this.detectiveMetadataResults[hash];

if (!metadata) {
metadata = JSON.parse(fs.readFileSync(this.cachePath(hash + '.detective.json'), 'utf8'));
this.detectiveMetadataResults[hash] = metadata;
}

return metadata;
};
111 changes: 111 additions & 0 deletions lib/precompiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
var path = require('path');
var resolveFrom = require('resolve-from');
var resolve = require('resolve');
var md5hex = require('md5-hex');
var detective = require('babel-plugin-detective');
var fs = require('graceful-fs');

var base = path.join(__dirname, '..', 'index.js');

var _babelCore;

function babelCore() {
if (!_babelCore) {
_babelCore = require('babel-core');
}
return _babelCore;
}

function Precompiler(cacheDir) {
this._cacheDir = cacheDir;
this._cache = {};
}

Precompiler.prototype.cachePath = function (filename) {
return path.join(this._cacheDir, filename);
};

Precompiler.prototype.buildEntry = function (filename) {
if (this._cache[filename]) {
return;
}

var entry = this._cache[filename] = {};

var result = babelCore().transformFileSync(filename, {
plugins: [detective]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you don't merge this with the babelConfig already defined, this will implicitly be merged with the .babelrc.
Which is totally cool if the babel option is set to "inherit". Otherwise, metadata generation might fail.

var config = objectAssign({}, this.babelConfig, {
   plugins: this.babelConfig.plugins.concat(detective)
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this repo failing with this config

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will implicitly be merged with the .babelrc.

Yes. That's the idea. That is only precompiling your sources. We already precompile your tests in caching-precompiler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});

var hash = md5hex(result.code);
fs.writeFileSync(this.cachePath(hash + '.js'), result.code);

var metadata = detective.metadata(result);

var dependencies = this.normalizeDependencies(filename, metadata);

entry.hash = hash;
entry.dependencies = dependencies;

dependencies.forEach(this.buildEntry, this);
};

Precompiler.prototype.normalizeDependencies = function (filename, metadata) {
if (metadata && metadata.expressions && (metadata.expressions === true || metadata.expressions.length)) {
console.warn(filename + ' has a dynamic require - precompilation may not work');
}

if (!metadata || !metadata.strings || !metadata.strings.length) {
return [];
}

var dir = path.dirname(filename);

return metadata.strings
.filter(function (dep) {
return !resolve.isCore(dep);
})
.map(function (dep) {
return resolveFrom(dir, dep);
})
.filter(Boolean)
.filter(this.shouldTranspile, this);
};

Precompiler.prototype.shouldTranspile = function (filename) {
return (
(filename !== base) &&
!/[\/\\]node_modules[\/\\]/.test(filename) &&
/\.js$/.test(filename)
);
};

Precompiler.prototype.createHash = function (filename, hash, metadata) {
var hashMap = {};
hashMap[filename] = hash;

var dependencies = this.normalizeDependencies(filename, metadata);

dependencies.forEach(this.buildEntry, this);

dependencies.forEach(function (filename) {
this.attach(filename, hashMap);
}, this);

return hashMap;
};

Precompiler.prototype.attach = function (filename, hashMap) {
if (hashMap[filename] || !this._cache[filename]) {
return;
}

var entry = this._cache[filename];

hashMap[filename] = entry.hash;

entry.dependencies.forEach(function (filename) {
this.attach(filename, hashMap);
}, this);
};

module.exports = Precompiler;
12 changes: 8 additions & 4 deletions lib/test-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ sourceMapSupport.install({
handleUncaughtExceptions: false,
retrieveSourceMap: function (source) {
if (sourceMapCache[source]) {
return {
url: source,
map: fs.readFileSync(sourceMapCache[source], 'utf8')
};
try {
return {
url: source,
map: fs.readFileSync(sourceMapCache[source], 'utf8')
};
} catch (err) {
return null;
}
}
}
});
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"find-cache-dir": "^0.1.1",
"fn-name": "^2.0.0",
"globby": "^5.0.0",
"graceful-fs": "^4.1.4",
"has-flag": "^2.0.0",
"ignore-by-default": "^1.0.0",
"is-ci": "^1.0.7",
Expand Down Expand Up @@ -141,7 +142,9 @@
"pretty-ms": "^2.0.0",
"repeating": "^2.0.0",
"require-precompiled": "^0.1.0",
"resolve": "^1.1.7",
"resolve-cwd": "^1.0.0",
"resolve-from": "^2.0.0",
"set-immediate-shim": "^1.0.1",
"slash": "^1.0.0",
"source-map-support": "^0.4.0",
Expand Down Expand Up @@ -182,7 +185,9 @@
},
"overrides": [
{
"files": ["test/**/*.js"],
"files": [
"test/**/*.js"
],
"rules": {
"max-lines": 0
}
Expand Down
9 changes: 7 additions & 2 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,16 +683,17 @@ function generateTests(prefix, apiCreator) {
});

test(prefix + 'caching is enabled by default', function (t) {
t.plan(3);
t.plan(4);
rimraf.sync(path.join(__dirname, 'fixture/caching/node_modules'));
var api = apiCreator();

api.run([path.join(__dirname, 'fixture/caching/test.js')])
.then(function () {
var files = fs.readdirSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava'));
t.is(files.length, 2);
t.is(files.length, 3);
t.is(files.filter(endsWithJs).length, 1);
t.is(files.filter(endsWithMap).length, 1);
t.is(files.filter(endsWithJson).length, 1);
t.end();
});

Expand All @@ -703,6 +704,10 @@ function generateTests(prefix, apiCreator) {
function endsWithMap(filename) {
return /\.map$/.test(filename);
}

function endsWithJson(filename) {
return /\.json/.test(filename);
}
});

test(prefix + 'caching can be disabled', function (t) {
Expand Down
3 changes: 2 additions & 1 deletion test/babel-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ var sinon = require('sinon');
var proxyquire = require('proxyquire').noCallThru();
var throwsHelper = require('babel-plugin-ava-throws-helper');
var transformRuntime = require('babel-plugin-transform-runtime');
var detective = require('babel-plugin-detective');

function fixture(name) {
return path.join(__dirname, 'fixture', name);
Expand Down Expand Up @@ -45,6 +46,6 @@ test('uses babelConfig for babel options when babelConfig is an object', functio
t.true('inputSourceMap' in options);
t.false(options.babelrc);
t.strictDeepEqual(options.presets, ['stage-2', 'es2015']);
t.strictDeepEqual(options.plugins, [customPlugin, powerAssert, throwsHelper, rewrite, transformRuntime]);
t.strictDeepEqual(options.plugins, [customPlugin, powerAssert, throwsHelper, rewrite, transformRuntime, detective]);
t.end();
});
Loading