diff --git a/.babelrc b/.babelrc index 66701ea4a9d34..007ca4aaa4ce1 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,4 @@ { "presets": ['react', 'es2015', 'stage-0'], - "plugins": [ - 'transform-object-rest-spread' - ] + "plugins": [] } diff --git a/README.md b/README.md index 171dd88f48bfa..209c63adb3054 100644 --- a/README.md +++ b/README.md @@ -365,10 +365,28 @@ export.postBuild = function(pages, callback) { ### Configuring Babel -For **Webpack loaded code** you can't modify Babel's behavior as normal -by modifying the .babelrc in your site's root directory. - -Instead you'll need to modify the Webpack babel loader as [described +You can modify Babel's behavior as normal by either providing a `.babelrc` in +your site's root directory or by adding a "babel" section in your site's +`package.json`. You can find out more about how to configure babel +[here](https://babeljs.io/docs/usage/babelrc/). + +Gatsby by default will use your Babel configuration over the default if it can +find it. Gatsby will automatically add react-hmre to your Babel config during +development. + +Note that if you want to use babel-plugin that is not provided by Gatsby, you +will have to also add it to your package.json. You can use any babel-plugin +that Gatsby packs as a dependency without having to add it to your own +package.json: + +* babel-plugin-add-module-exports +* babel-plugin-transform-object-assign +* babel-preset-es2015 +* babel-preset-react +* babel-preset-stage-0 + +If you need to change the loader to be something completely custom. You will +have to define your own webpack loader by following the steps [described above](https://github.com/gatsbyjs/gatsby#how-to-use-your-own-webpack-loaders). ### Deploying to Github Pages (and other hosts where your site's links need prefixes) diff --git a/lib/utils/babel-config.js b/lib/utils/babel-config.js new file mode 100644 index 0000000000000..a270e385d064b --- /dev/null +++ b/lib/utils/babel-config.js @@ -0,0 +1,122 @@ +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', 'transform-object-assign'], +} + +/** + * 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 gatsbyPath = path.resolve('..', '..') + const plugin = resolve(`babel-${type}-${pluginName}`, directory) || + resolve(`babel-${type}-${pluginName}`, gatsbyPath) || + resolve(pluginName, directory) || + resolve(pluginName, gatsbyPath) + + 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-plugin-transform-object-assign + - 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 1c00d6955e5b8..e5d3b74d747f1 100644 --- a/lib/utils/build.js +++ b/lib/utils/build.js @@ -8,7 +8,6 @@ import buildProductionBundle from './build-javascript' import postBuild from './post-build' import globPages from './glob-pages' - 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 46909881bf126..ac1cebad768a7 100644 --- a/lib/utils/webpack.config.js +++ b/lib/utils/webpack.config.js @@ -6,6 +6,8 @@ const debug = require('debug')('gatsby:webpack-config') import path from 'path' import _ from 'lodash' +import babelConfig from './babel-config' + let modifyWebpackConfig try { const gatsbyNodeConfig = path.resolve(process.cwd(), './gatsby-node') @@ -185,9 +187,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$/, @@ -285,16 +285,6 @@ module.exports = (program, directory, stage, webpackPort = 1500, routes = []) => require('postcss-reporter'), ], }) - config.removeLoader('js') - config.loader('js', { - 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'], - }, - }) return config case 'build-css': diff --git a/package.json b/package.json index 5a42d926df573..e0ec9420d3f89 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "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-plugin-transform-object-assign": "^6.8.0", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "babel-preset-react-hmre": "^1.1.1", @@ -40,6 +40,7 @@ "invariant": "^2.2.1", "json-loader": "^0.5.2", "less": "^2.7.1", + "json5": "^0.5.0", "less-loader": "^2.2.0", "loader-utils": "^0.2.14", "lodash": "^4.12.0", 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..ed1dfd7029d02 --- /dev/null +++ b/test/fixtures/site-with-valid-babelrc/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ['react', 'es2015', 'stage-0'] +} diff --git a/test/utils/babel-config.js b/test/utils/babel-config.js new file mode 100644 index 0000000000000..72ae0bd5b63e8 --- /dev/null +++ b/test/utils/babel-config.js @@ -0,0 +1,60 @@ +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) +}) + +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)) +})