Skip to content
Permalink
Browse files

Refactor Babel integration to prepare for extraction

Prepare `lib/babel.js` which we'll extract into @ava/babel. There's an
integration protocol which will allow us to evolve AVA and @ava/babel
independently.

Refactor both the legacy and the no-babel-out-of-the-box flows to be
more cleanly separated, so we can more easily remove the legacy flows
and upgrade the experimental one.

Add support for the experimental flow in the ESLint helper and
`profile.js`.
  • Loading branch information...
novemberborn committed Oct 6, 2019
1 parent 7b54317 commit 20f86d683cb10b43d88681a1904ad2c57c0a856b
Showing with 433 additions and 296 deletions.
  1. +13 βˆ’7 eslint-plugin-helper.js
  2. +64 βˆ’61 lib/api.js
  3. +63 βˆ’0 lib/babel-manager.js
  4. +199 βˆ’118 lib/{babel-pipeline.js β†’ babel.js}
  5. +11 βˆ’8 lib/cli.js
  6. +46 βˆ’36 lib/extensions.js
  7. +0 βˆ’42 lib/worker/precompiler-hook.js
  8. +6 βˆ’2 lib/worker/subprocess.js
  9. +16 βˆ’13 profile.js
  10. +7 βˆ’6 test/api.js
  11. +8 βˆ’3 test/helper/report.js
@@ -1,5 +1,5 @@
'use strict';
const babelPipeline = require('./lib/babel-pipeline');
const babelManager = require('./lib/babel-manager');
const normalizeExtensions = require('./lib/extensions');
const {hasExtension, normalizeGlobs, classify} = require('./lib/globs');
const loadConfig = require('./lib/load-config');
@@ -14,25 +14,31 @@ function load(projectDir, overrides) {
}

let conf;
let babelConfig;
let babelProvider;
if (configCache.has(projectDir)) {
({conf, babelConfig} = configCache.get(projectDir));
({conf, babelProvider} = configCache.get(projectDir));
} else {
conf = loadConfig({resolveFrom: projectDir});
babelConfig = babelPipeline.validate(conf.babel);
configCache.set(projectDir, {conf, babelConfig});
const {nonSemVerExperiments: experiments} = conf;

if (!experiments.noBabelOutOfTheBox || conf.babel !== undefined) {
babelProvider = babelManager({experiments, projectDir});
babelProvider.validateConfig(conf.babel, conf.compileEnhancements !== false);
}

configCache.set(projectDir, {conf, babelProvider});
}

if (overrides) {
conf = {...conf, ...overrides};
if (overrides.extensions) {
// Ignore extensions from the Babel config. Assume all extensions are
// provided in the override.
babelConfig = null;
babelProvider = undefined;
}
}

const extensions = normalizeExtensions(conf.extensions || [], babelConfig);
const extensions = normalizeExtensions(conf.extensions, babelProvider, {experiments: conf.nonSemVerExperiments});
const globs = {cwd: projectDir, ...normalizeGlobs(conf.files, conf.helpers, conf.sources, extensions.all)};

const helper = Object.freeze({
@@ -15,7 +15,6 @@ const makeDir = require('make-dir');
const ms = require('ms');
const chunkd = require('chunkd');
const Emittery = require('emittery');
const babelPipeline = require('./babel-pipeline');
const globs = require('./globs');
const RunStatus = require('./run-status');
const fork = require('./fork');
@@ -41,16 +40,16 @@ class Api extends Emittery {
this.options.require = resolveModules(this.options.require);

this._allExtensions = this.options.extensions.all;
this._regexpFullExtensions = new RegExp(`\\.(${this.options.extensions.full.map(ext => escapeStringRegexp(ext)).join('|')})$`);
this._precompiler = null;
this._regexpBabelExtensions = new RegExp(`\\.(${this.options.extensions.babelOnly.map(ext => escapeStringRegexp(ext)).join('|')})$`);
this._cacheDir = null;
this._interruptHandler = () => {};

if (options.ranFromCli) {
process.on('SIGINT', () => this._interruptHandler());
}
}

async run(files = [], runtimeOptions = {}) {
async run(files = [], runtimeOptions = {}) { // eslint-disable-line complexity
let setupOrGlobError;
files = files.map(file => path.resolve(this.options.resolveTestsFrom, file));

@@ -107,19 +106,26 @@ class Api extends Emittery {
}
};

let precompiler;
let cacheDir;
let helpers;

const {babelProvider} = this.options;
const enabledBabelProvider = babelProvider !== undefined && (
babelProvider.isEnabled() || (babelProvider.legacy && babelProvider.compileEnhancements !== null)
) ?
babelProvider :
null;

try {
precompiler = await this._setupPrecompiler(); // eslint-disable-line require-atomic-updates
cacheDir = this._createCacheDir();
helpers = [];
if (files.length === 0 || precompiler.enabled) {
if (files.length === 0 || enabledBabelProvider !== null) {
let found;
if (precompiler.enabled) {
if (enabledBabelProvider === null) {
found = await globs.findTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
} else {
found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
helpers = found.helpers;
} else {
found = await globs.findTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
}

if (files.length === 0) {
@@ -186,31 +192,54 @@ class Api extends Emittery {
}
});

let precompilation = null;
if (precompiler.enabled) {
let babelState = null;
if (enabledBabelProvider) {
// Compile all test and helper files. Assumes the tests only load
// helpers from within the `resolveTestsFrom` directory. Without
// arguments this is the `projectDir`, else it's `process.cwd()`
// which may be nested too deeply.
precompilation = {
cacheDir: precompiler.cacheDir,
map: [...files, ...helpers].reduce((acc, file) => {
try {
const realpath = fs.realpathSync(file);
const filename = path.basename(realpath);
const cachePath = this._regexpFullExtensions.test(filename) ?
precompiler.precompileFull(realpath) :
precompiler.precompileEnhancementsOnly(realpath);
if (cachePath) {
acc[realpath] = cachePath;
}
} catch (error) {
throw Object.assign(error, {file});

const testFiles = files.map(file => fs.realpathSync(file));
const helperFiles = helpers.map(file => fs.realpathSync(file));

if (enabledBabelProvider.legacy) {
const full = {testFiles: [], helperFiles: []};
const enhancements = {testFiles: [], helperFiles: []};
for (const realpath of testFiles) {
if (this._regexpBabelExtensions.test(path.basename(realpath))) { // eslint-disable-line max-depth
full.testFiles.push(realpath);
} else {
enhancements.testFiles.push(realpath);
}
}

return acc;
}, {})
};
for (const realpath of helperFiles) {
if (this._regexpBabelExtensions.test(path.basename(realpath))) { // eslint-disable-line max-depth
full.helperFiles.push(realpath);
} else {
enhancements.helperFiles.push(realpath);
}
}

babelState = {
...enabledBabelProvider.isEnabled() && enabledBabelProvider.compile({
cacheDir,
...full
}),
...enabledBabelProvider.compileEnhancements !== null && enabledBabelProvider.compileEnhancements({
cacheDir,
...enhancements
})
};
} else {
babelState = {
...enabledBabelProvider.compile({
cacheDir,
testFiles: testFiles.filter(realpath => this._regexpBabelExtensions.test(path.basename(realpath))),
helperFiles: helperFiles.filter(realpath => this._regexpBabelExtensions.test(path.basename(realpath)))
})
};
}
}

// Resolve the correct concurrency value.
@@ -234,16 +263,11 @@ class Api extends Emittery {
const execArgv = await this._computeForkExecArgv();
const options = {
...apiOptions,
babelState,
recordNewSnapshots: !isCi,
// If we're looking for matches, run every single test process in exclusive-only mode
runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true
};
if (precompilation) {
options.cacheDir = precompilation.cacheDir;
options.precompiled = precompilation.map;
} else {
options.precompiled = {};
}

if (runtimeOptions.updateSnapshots) {
// Don't use in Object.assign() since it'll override options.updateSnapshots even when false.
@@ -269,9 +293,9 @@ class Api extends Emittery {
return runStatus;
}

_setupPrecompiler() {
if (this._precompiler) {
return this._precompiler;
_createCacheDir() {
if (this._cacheDir) {
return this._cacheDir;
}

const cacheDir = this.options.cacheEnabled === false ?
@@ -281,30 +305,9 @@ class Api extends Emittery {
// Ensure cacheDir exists
makeDir.sync(cacheDir);

const {projectDir, babelConfig, experiments} = this.options;
const compileEnhancements = this.options.compileEnhancements !== false;
const precompileFull = babelConfig ?
babelPipeline.build(projectDir, cacheDir, babelConfig, compileEnhancements, experiments) :
filename => {
throw new Error(`Cannot apply full precompilation, possible bad usage: ${filename}`);
};

let precompileEnhancementsOnly = () => null;
if (compileEnhancements && !experiments.noBabelOutOfTheBox) {
precompileEnhancementsOnly = this.options.extensions.enhancementsOnly.length > 0 ?
babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) :
filename => {
throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`);
};
}
this._cacheDir = cacheDir;

this._precompiler = {
cacheDir,
enabled: experiments.noBabelOutOfTheBox ? precompileFull !== null : babelConfig || compileEnhancements,
precompileEnhancementsOnly,
precompileFull
};
return this._precompiler;
return cacheDir;
}

async _computeForkExecArgv() {
@@ -0,0 +1,63 @@
const pkg = require('../package.json');

module.exports = ({experiments, projectDir}) => {
const ava = {version: pkg.version};
const makeProvider = require('./babel');

if (experiments.noBabelOutOfTheBox) {
let fatal;
const provider = makeProvider({
negotiateProtocol(identifiers) {
if (!identifiers.includes('noBabelOutOfTheBox')) {
fatal = new Error('TODO: Throw error when @ava/babel does not negotiate the expected protocol');
return null;
}

return {identifier: 'noBabelOutOfTheBox', ava, projectDir};
}
});

if (fatal) {
throw fatal;
}

return {
legacy: false,
...provider,
// Don't pass the legacy compileEnhancements value.
validateConfig: babelConfig => provider.validateConfig(babelConfig)
};
}

let fatal;
const negotiateProtocol = identifiers => {
if (!identifiers.includes('legacy')) {
fatal = new Error('TODO: Throw error when @ava/babel does not negotiate the expected protocol');
return null;
}

return {identifier: 'legacy', ava, projectDir};
};

const [full, enhancementsOnly] = [makeProvider({negotiateProtocol}), makeProvider({negotiateProtocol})];

if (fatal) {
throw fatal;
}

return {
legacy: true,
...full,
validateConfig(babelConfig, compileEnhancements) {
full.validateConfig(babelConfig, compileEnhancements, false);
enhancementsOnly.validateConfig(false, compileEnhancements, true);
},
get compileEnhancements() {
if (enhancementsOnly.isEnabled()) {
return enhancementsOnly.compile;
}

return null;
}
};
};

0 comments on commit 20f86d6

Please sign in to comment.
You can’t perform that action at this time.