Skip to content
Permalink
Browse files

Experimentally disable Babel out of the box

  • Loading branch information...
novemberborn committed Sep 22, 2019
1 parent ef39793 commit 1bcaaa4e6eca3cb9a31b7e4c7ce0a4ba40611e7d
Showing with 171 additions and 38 deletions.
  1. +50 −0 docs/recipes/babel.md
  2. +4 −4 lib/api.js
  3. +74 −18 lib/babel-pipeline.js
  4. +6 −2 lib/cli.js
  5. +36 −13 lib/extensions.js
  6. +1 −1 lib/load-config.js
@@ -138,6 +138,56 @@ You can completely disable AVA's use of Babel.
}
```

## Experimental feature: No Babel out of the box

A future release of AVA will not use Babel out of the box. You can opt in to this feature. **This feature may change or be removed at any time**:

**`package.json`:**

```js
{
"ava": {
"nonSemVerExperiments": {
"noBabelOutOfTheBox": true
}
}
}
```

This disables AVA's Babel pipeline, though you can still enable it by configuring it as described above.

`compileEnhancements` can no longer be configured on the top-level AVA configuration. It must be configured in AVA's `babel` configuration instead. It defaults to `true`.

**`package.json`:**

```json
{
"ava": {
"nonSemVerExperiments": {
"noBabelOutOfTheBox": true
},
"babel": {
"compileEnhancements": false
}
}
}
```

You can disable the non-enhancement compilation of your test files by setting `testOptions` to `false`.

```json
{
"ava": {
"nonSemVerExperiments": {
"noBabelOutOfTheBox": true
},
"babel": {
"testOptions": false
}
}
}
```

## Use Babel polyfills

AVA lets you write your tests using new JavaScript syntax, even on Node.js versions that otherwise wouldn't support it. However, it doesn't add or modify built-ins of your current environment. Using AVA would, for example, not provide modern features such as `Object.entries()` to an underlying Node.js 6 environment.
@@ -281,16 +281,16 @@ class Api extends Emittery {
// Ensure cacheDir exists
makeDir.sync(cacheDir);

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

let precompileEnhancementsOnly = () => null;
if (compileEnhancements) {
if (compileEnhancements && !experiments.noBabelOutOfTheBox) {
precompileEnhancementsOnly = this.options.extensions.enhancementsOnly.length > 0 ?
babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) :
filename => {
@@ -300,7 +300,7 @@ class Api extends Emittery {

this._precompiler = {
cacheDir,
enabled: babelConfig || compileEnhancements,
enabled: experiments.noBabelOutOfTheBox ? precompileFull !== null : babelConfig || compileEnhancements,
precompileEnhancementsOnly,
precompileFull
};
@@ -24,38 +24,79 @@ function getSourceMap(filePath, code) {
return sourceMap ? sourceMap.toObject() : undefined;
}

function hasValidKeys(conf) {
return Object.keys(conf).every(key => key === 'extensions' || key === 'testOptions');
function hasValidKeys(conf, {experiments}) {
return Object.keys(conf).every(key => {
if (key === 'compileEnhancements') {
// This key is only allowed when the no-Babel-out-of-the-box experiment has been opted into.
return experiments.noBabelOutOfTheBox === true;
}

return key === 'extensions' || key === 'testOptions';
});
}

function isValidExtensions(extensions) {
return Array.isArray(extensions) && extensions.every(ext => typeof ext === 'string' && ext !== '');
}

function validate(conf) {
function validate(conf, {experiments = {}} = {}) {
if (conf === false) {
return null;
}

const defaultOptions = {babelrc: true, configFile: true};

if (conf === undefined) {
// There is no default config when the no-Babel-out-of-the-box experiment has been opted into.
if (experiments.noBabelOutOfTheBox) {
return null;
}

return {testOptions: defaultOptions};
}

if (
!isPlainObject(conf) ||
!hasValidKeys(conf) ||
(conf.testOptions !== undefined && !isPlainObject(conf.testOptions)) ||
(conf.extensions !== undefined && !isValidExtensions(conf.extensions))
) {
let isValid = isPlainObject(conf) && hasValidKeys(conf, {experiments});

let compileEnhancements;
let extensions;
let testOptions;

if (isValid) {
({compileEnhancements, extensions, testOptions} = conf);

// If a valid key, `compileEnhancements` *must* be a boolean.
if (compileEnhancements !== undefined && typeof compileEnhancements !== 'boolean') {
isValid = false;
}

if (extensions !== undefined && !isValidExtensions(extensions)) {
isValid = false;
}

if (testOptions !== undefined) {
if (testOptions === false) {
if (!experiments.noBabelOutOfTheBox) {
isValid = false;
}
} else if (!isPlainObject(conf.testOptions)) {
isValid = false;
}
}
}

if (!isValid) {
throw new Error(`Unexpected Babel configuration for AVA. See https://github.com/avajs/ava/blob/v${pkg.version}/docs/recipes/babel.md for allowed values.`);
}

return {
extensions: conf.extensions,
testOptions: {...defaultOptions, ...conf.testOptions}
};
if (experiments.noBabelOutOfTheBox && compileEnhancements !== false) {
compileEnhancements = true;
}

if (testOptions !== false) {
testOptions = {...defaultOptions, ...testOptions};
}

return {compileEnhancements, extensions, testOptions};
}

function enableParserPlugins(api) {
@@ -98,11 +139,15 @@ function createConfigItem(ref, type, options = {}) {
// Ideally we'd detect the stage-4 preset anywhere in the configuration
// hierarchy, but Babel's loadPartialConfig() does not return disabled presets.
// See <https://github.com/babel/babel/issues/7920>.
function wantsStage4(userOptions, projectDir) {
function wantsStage4(userOptions, projectDir, {experiments}) {
if (!userOptions) {
return false;
}

if (experiments.noBabelOutOfTheBox && userOptions.testOptions === false) {
return false;
}

if (!userOptions.testOptions.presets) {
return true;
}
@@ -164,9 +209,20 @@ function hashPartialTestConfig({babelrc, config, options: {plugins, presets}}, p
return md5Hex(inputs);
}

function build(projectDir, cacheDir, userOptions, compileEnhancements) {
if (!userOptions && !compileEnhancements) {
return null;
function build(projectDir, cacheDir, userOptions, compileEnhancements, experiments = {}) { // eslint-disable-line max-params
if (experiments.noBabelOutOfTheBox) {
// Don't trust the `compileEnhancements` argument when the no-Babel-out-of-the-box experiment has been opted into.
compileEnhancements = userOptions.compileEnhancements === true;
}

if (!compileEnhancements) {
if (!userOptions) {
return null;
}

if (experiments.noBabelOutOfTheBox && userOptions.testOptions === false) {
return null;
}
}

// Note that Babel ignores empty string values, even for NODE_ENV. Here
@@ -192,7 +248,7 @@ function build(projectDir, cacheDir, userOptions, compileEnhancements) {
const partialCacheKey = md5Hex(seedInputs);
const pluginAndPresetHashes = new Map();

const ensureStage4 = wantsStage4(userOptions, projectDir);
const ensureStage4 = wantsStage4(userOptions, projectDir, {experiments});
const containsStage4 = makeValueChecker('../stage-4');
const containsTransformTestFiles = makeValueChecker('@ava/babel-preset-transform-test-files');

@@ -168,6 +168,10 @@ exports.run = async () => { // eslint-disable-line complexity
console.log(chalk.magenta(` ${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.`));
}

if (experiments.noBabelOutOfTheBox && typeof conf.compileEnhancements === 'boolean') {
exit('Enhancement compilation must be configured in AVA’s Babel options');
}

const ciParallelVars = require('ci-parallel-vars');
const Api = require('./api');
const VerboseReporter = require('./reporters/verbose');
@@ -181,7 +185,7 @@ exports.run = async () => { // eslint-disable-line complexity

let babelConfig = null;
try {
babelConfig = babelPipeline.validate(conf.babel);
babelConfig = babelPipeline.validate(conf.babel, {experiments});
} catch (error) {
exit(error.message);
}
@@ -195,7 +199,7 @@ exports.run = async () => { // eslint-disable-line complexity

let extensions;
try {
extensions = normalizeExtensions(conf.extensions || [], babelConfig);
extensions = normalizeExtensions(conf.extensions || [], babelConfig, {experiments});
} catch (error) {
exit(error.message);
}
@@ -1,10 +1,26 @@
module.exports = (enhancementsOnly, babelConfig) => {
const {extensions: full = []} = babelConfig || {};

module.exports = (configuredExtensions, babelConfig, {experiments = {}} = {}) => {
// Combine all extensions possible for testing. Remove duplicate extensions.
const duplicates = [];
const seen = new Set();
for (const ext of [...enhancementsOnly, ...full]) {

let all = [];
let enhancementsOnly = [];
let full = [];

if (experiments.noBabelOutOfTheBox) {
if (babelConfig) {
// When this experiment is opted into, the Babel pipeline takes care of
// compiling enhancements, so `enhancementsOnly` can be left empty.
({extensions: full = []} = babelConfig);
} else {
all = configuredExtensions;
}
} else {
enhancementsOnly = configuredExtensions;
({extensions: full = []} = babelConfig || {});
}

for (const ext of [...all, ...enhancementsOnly, ...full]) {
if (seen.has(ext)) {
duplicates.push(ext);
} else {
@@ -15,18 +31,25 @@ module.exports = (enhancementsOnly, babelConfig) => {
// Decide if and where to add the default `js` extension. Keep in mind it's not
// added if extensions have been explicitly given.
if (!seen.has('js')) {
if (babelConfig && full.length === 0) {
if (experiments.noBabelOutOfTheBox) {
seen.add('js');
full.push('js');
}
if (babelConfig && full.length === 0) {
full.push('js');
}
} else {
if (babelConfig && full.length === 0) {
seen.add('js');
full.push('js');
}

if (!babelConfig && enhancementsOnly.length === 0) {
seen.add('js');
enhancementsOnly.push('js');
if (!babelConfig && enhancementsOnly.length === 0) {
seen.add('js');
enhancementsOnly.push('js');
}
}
} else if (babelConfig && full.length === 0) {
} else if (!experiments.noBabelOutOfTheBox && babelConfig && full.length === 0) {
// If Babel is not disabled, and has the default extensions (or, explicitly,
// no configured extensions), thes the `js` extension must have come from
// no configured extensions), then the `js` extension must have come from
// the `enhancementsOnly` value. That's not allowed since it'd be a
// roundabout way of disabling Babel.
throw new Error('Cannot specify generic \'js\' extension without disabling AVA\'s Babel usage.');
@@ -36,6 +59,6 @@ module.exports = (enhancementsOnly, babelConfig) => {
throw new Error(`Unexpected duplicate extensions in options: '${duplicates.join('\', \'')}'.`);
}

const all = [...seen];
all = [...seen];
return {all, enhancementsOnly, full};
};
@@ -6,7 +6,7 @@ const pkgConf = require('pkg-conf');

const NO_SUCH_FILE = Symbol('no ava.config.js file');
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
const EXPERIMENTS = new Set(['tryAssertion']);
const EXPERIMENTS = new Set(['noBabelOutOfTheBox', 'tryAssertion']);

function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity
let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});

0 comments on commit 1bcaaa4

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