From 7537b0ac71f9ddcacfab0ed3e12dcf9462750393 Mon Sep 17 00:00:00 2001 From: Benjamin Stepp Date: Sun, 8 May 2016 11:30:26 -0500 Subject: [PATCH] Support user provided babel configs This change allows the user to provide a babelrc or a babel section in their package.json. It also allows gatsby to be used without having to specifiy a babelrc if the user doesn't need anything past what babel provides as a default. * It resolves all of the paths to become absolute such that the user can crawl upwards as in #239. * It allows the custom usage of other babel plugins so that decorators work as in #129. --- lib/utils/babel-config.js | 120 ++++++++++++++++++ lib/utils/build.js | 1 - lib/utils/develop.js | 12 -- lib/utils/webpack.config.js | 10 +- package.json | 2 +- .../site-with-babelpackage/package.json | 5 + .../site-with-invalid-babelrc/.babelrc | 2 + .../site-with-unresolvable-babelrc/.babelrc | 8 ++ .../fixtures/site-with-valid-babelrc/.babelrc | 6 + test/utils/babel-config.js | 61 +++++++++ 10 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 lib/utils/babel-config.js create mode 100644 test/fixtures/site-with-babelpackage/package.json create mode 100644 test/fixtures/site-with-invalid-babelrc/.babelrc create mode 100644 test/fixtures/site-with-unresolvable-babelrc/.babelrc create mode 100644 test/fixtures/site-with-valid-babelrc/.babelrc create mode 100644 test/utils/babel-config.js diff --git a/lib/utils/babel-config.js b/lib/utils/babel-config.js new file mode 100644 index 0000000000000..6b0fe61261160 --- /dev/null +++ b/lib/utils/babel-config.js @@ -0,0 +1,120 @@ +import resolve from 'babel-core/lib/helpers/resolve' +import fs from 'fs' +import path from 'path' +import json5 from 'json5' +import startsWith from 'lodash/startswith' +import invariant from 'invariant' + +const DEFAULT_BABEL_CONFIG = { + presets: ['react', 'es2015', 'stage-0'], + plugins: ['add-module-exports'], +} + +/** + * Uses babel-core helpers to resolve the plugin given it's name. It + * resolves plugins in the following order: + * + * 1. Adding babel-type prefix and checking user's local modules + * 2. Adding babel-type prefix and checking gatsby's modules + * 3. Checking users's modules without prefix + * 4. Checking gatsby's modules without prefix + * + */ +function resolvePlugin (pluginName, directory, type) { + const plugin = resolve(`babel-${type}-${pluginName}`, directory) || + resolve(`babel-${type}-${pluginName}`) || + resolve(pluginName, directory) || + resolve(pluginName) + + const name = !startsWith(pluginName, 'babel') ? pluginName : `babel-${type}-${pluginName}` + const pluginInvariantMessage = ` + You are trying to use a babel plugin which gatsby cannot find. You + can install it using "npm install --save ${name}". + + You can use any of the gatsby provided plugins without installing them: + - babel-plugin-add-module-exports + - babel-preset-es2015 + - babel-preset-react + - babel-preset-stage-0 + ` + + invariant(plugin !== null, pluginInvariantMessage) + return plugin +} + +/** + * Normalizes a babel config object to include only absolute paths. + * This way babel-loader will correctly resolve babel plugins + * regardless of where they are located. + */ +function normalizeConfig (config, directory) { + const normalizedConfig = { + presets: [], + plugins: [], + } + + const presets = config.presets || [] + presets.forEach(preset => { + normalizedConfig.presets.push(resolvePlugin(preset, directory, 'preset')) + }) + + const plugins = config.plugins || [] + plugins.forEach(plugin => { + normalizedConfig.plugins.push(resolvePlugin(plugin, directory, 'plugin')) + }) + + return normalizedConfig +} + +/** + * Locates a babelrc in the gatsby site root directory. Parses it using + * json5 (what babel uses). It throws an error if the users's babelrc is + * not parseable. + */ +function findBabelrc (directory) { + try { + const babelrc = fs.readFileSync(path.join(directory, '.babelrc'), 'utf-8') + return json5.parse(babelrc) + } catch (error) { + if (error.code === 'ENOENT') { + return null + } else { + throw error + } + } +} + +/** + * Reads the user's package.json and returns the babel section. It will + * return undefined when the babel section does not exist. + */ +function findBabelPackage (directory) { + try { + const packageJson = require(path.join(directory, 'package.json')) + return packageJson.babel + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + return null + } else { + throw error + } + } +} + +/** + * Returns a normalized babel config to use with babel-loader. All of + * the paths will be absolute so that babel behaves as expected. + */ +export default function babelConfig (program, stage) { + const { directory } = program + + const babelrc = findBabelrc(directory) || + findBabelPackage(directory) || + DEFAULT_BABEL_CONFIG + + if (stage === 'develop') { + babelrc.presets.unshift('react-hmre') + } + + return normalizeConfig(babelrc, directory) +} diff --git a/lib/utils/build.js b/lib/utils/build.js index 43c56c0fabd5c..90802034f9ee8 100644 --- a/lib/utils/build.js +++ b/lib/utils/build.js @@ -6,7 +6,6 @@ import globPages from './glob-pages' import toml from 'toml' import fs from 'fs' - function customPost (program, callback) { const directory = program.directory let customPostBuild diff --git a/lib/utils/develop.js b/lib/utils/develop.js index 3a0f361b75739..8ecefd80e7870 100644 --- a/lib/utils/develop.js +++ b/lib/utils/develop.js @@ -33,18 +33,6 @@ module.exports = (program) => { } const htmlCompilerConfig = webpackConfig(program, directory, 'develop', program.port) - // Remove react-transform option from Babel as redbox-react doesn't work - // on the server. - htmlCompilerConfig.removeLoader('js') - htmlCompilerConfig.loader('js', { - test: /\.jsx?$/, // Accept either .js or .jsx files. - exclude: /(node_modules|bower_components)/, - loader: 'babel', - query: { - presets: ['react', 'es2015', 'stage-1'], - plugins: ['add-module-exports'], - }, - }) webpackRequire(htmlCompilerConfig.resolve(), require.resolve(HTMLPath), (error, factory) => { if (error) { diff --git a/lib/utils/webpack.config.js b/lib/utils/webpack.config.js index 6c99c3e69dd6c..d86fac37839a5 100644 --- a/lib/utils/webpack.config.js +++ b/lib/utils/webpack.config.js @@ -4,6 +4,7 @@ import ExtractTextPlugin from 'extract-text-webpack-plugin' import Config from 'webpack-configurator' const debug = require('debug')('gatsby:webpack-config') import path from 'path' +import babelConfig from './babel-config' let modifyWebpackConfig try { const gatsbyNodeConfig = path.resolve(process.cwd(), './gatsby-node.js') @@ -182,9 +183,7 @@ module.exports = (program, directory, stage, webpackPort = 1500, routes = []) => test: /\.jsx?$/, // Accept either .js or .jsx files. exclude: /(node_modules|bower_components)/, loader: 'babel', - query: { - plugins: ['add-module-exports'], - }, + query: babelConfig(program, stage), }) config.loader('coffee', { test: /\.coffee$/, @@ -287,10 +286,7 @@ module.exports = (program, directory, stage, webpackPort = 1500, routes = []) => test: /\.jsx?$/, // Accept either .js or .jsx files. exclude: /(node_modules|bower_components)/, loader: 'babel', - query: { - presets: ['react-hmre', 'react', 'es2015', 'stage-1'], - plugins: ['add-module-exports'], - }, + query: babelConfig(program, stage), }) return config diff --git a/package.json b/package.json index 7c943a6a768b7..e753ce3fb348b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "babel-core": "^6.8.0", "babel-loader": "^6.2.4", "babel-plugin-add-module-exports": "^0.2.0", - "babel-plugin-transform-object-rest-spread": "^6.8.0", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "babel-preset-react-hmre": "^1.1.1", @@ -40,6 +39,7 @@ "invariant": "^2.2.1", "json-loader": "^0.5.2", "less": "^2.7.0", + "json5": "^0.5.0", "less-loader": "^2.2.0", "loader-utils": "^0.2.14", "lodash": "^4.11.2", diff --git a/test/fixtures/site-with-babelpackage/package.json b/test/fixtures/site-with-babelpackage/package.json new file mode 100644 index 0000000000000..43e652bd03659 --- /dev/null +++ b/test/fixtures/site-with-babelpackage/package.json @@ -0,0 +1,5 @@ +{ + "babel": { + "presets": ["react", "es2015", "stage-0"] + } +} diff --git a/test/fixtures/site-with-invalid-babelrc/.babelrc b/test/fixtures/site-with-invalid-babelrc/.babelrc new file mode 100644 index 0000000000000..93528edbe3fe7 --- /dev/null +++ b/test/fixtures/site-with-invalid-babelrc/.babelrc @@ -0,0 +1,2 @@ +{ + presets: ['react'] diff --git a/test/fixtures/site-with-unresolvable-babelrc/.babelrc b/test/fixtures/site-with-unresolvable-babelrc/.babelrc new file mode 100644 index 0000000000000..fb83380910a28 --- /dev/null +++ b/test/fixtures/site-with-unresolvable-babelrc/.babelrc @@ -0,0 +1,8 @@ +{ + plugins: [ + "transform-decorators-legacy", + "transform-async-to-generator", + "transform-es2015-modules-commonjs", + "transform-export-extensions", + ] +} diff --git a/test/fixtures/site-with-valid-babelrc/.babelrc b/test/fixtures/site-with-valid-babelrc/.babelrc new file mode 100644 index 0000000000000..66701ea4a9d34 --- /dev/null +++ b/test/fixtures/site-with-valid-babelrc/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": ['react', 'es2015', 'stage-0'], + "plugins": [ + 'transform-object-rest-spread' + ] +} diff --git a/test/utils/babel-config.js b/test/utils/babel-config.js new file mode 100644 index 0000000000000..c38bb6cb0b4b4 --- /dev/null +++ b/test/utils/babel-config.js @@ -0,0 +1,61 @@ +import test from 'ava' +import path from 'path' +import includes from 'lodash/includes' +import babelConfig from '../../lib/utils/babel-config' + +function programStub (fixture) { + const directory = path.resolve('..', 'fixtures', fixture) + return { directory } +} + +test('it returns a default babel config for babel-loader query', t => { + const program = programStub('site-without-babelrc') + const config = babelConfig(program) + + t.true(typeof config === 'object') + t.truthy(config.presets.length) + t.truthy(config.plugins.length) +}) + +test('all plugins are absolute paths to avoid babel lookups', t => { + const program = programStub('site-without-babelrc') + const config = babelConfig(program) + + config.presets.forEach(preset => t.true(path.resolve(preset) === preset)) + config.plugins.forEach(plugin => t.true(path.resolve(plugin) === plugin)) +}) + +test('fixture can resolve plugins in gatsby directory (crawling up)', t => { + const program = programStub('site-with-valid-babelrc') + + const config = babelConfig(program) + t.truthy(config.presets.length) + t.truthy(config.plugins.length) +}) + +test('throws error when babelrc is not parseable', t => { + const program = programStub('site-with-invalid-babelrc') + + t.throws(() => babelConfig(program)) +}) + +test('can read babel from packagejson', t => { + const program = programStub('site-with-valid-babelpackage') + + const config = babelConfig(program) + t.truthy(config.presets.length) +}) + +test('when in development has hmre', t => { + const program = programStub('site-without-babelrc') + const config = babelConfig(program, 'develop') + + // regex matches: babel followed by any amount of hyphen or word characters + const presetNames = config.presets.map(p => p.match(/babel[-|\w]+/)[0]) + t.true(includes(presetNames, 'babel-preset-react-hmre')) +}) + +test('throws when a plugin is not available', t => { + const program = programStub('site-with-unresolvable-babelrc') + t.throws(() => babelConfig(program)) +})