Skip to content

Commit

Permalink
Support user provided babel configs
Browse files Browse the repository at this point in the history
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 gatsbyjs#239.

* It allows the custom usage of other babel plugins so that decorators
work as in gatsbyjs#129.
  • Loading branch information
benstepp committed May 8, 2016
1 parent ceb44ce commit 7537b0a
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 21 deletions.
120 changes: 120 additions & 0 deletions lib/utils/babel-config.js
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 0 additions & 1 deletion lib/utils/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 0 additions & 12 deletions lib/utils/develop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 3 additions & 7 deletions lib/utils/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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$/,
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/site-with-babelpackage/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"babel": {
"presets": ["react", "es2015", "stage-0"]
}
}
2 changes: 2 additions & 0 deletions test/fixtures/site-with-invalid-babelrc/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
presets: ['react']
8 changes: 8 additions & 0 deletions test/fixtures/site-with-unresolvable-babelrc/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
plugins: [
"transform-decorators-legacy",
"transform-async-to-generator",
"transform-es2015-modules-commonjs",
"transform-export-extensions",
]
}
6 changes: 6 additions & 0 deletions test/fixtures/site-with-valid-babelrc/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": ['react', 'es2015', 'stage-0'],
"plugins": [
'transform-object-rest-spread'
]
}
61 changes: 61 additions & 0 deletions test/utils/babel-config.js
Original file line number Diff line number Diff line change
@@ -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))
})

0 comments on commit 7537b0a

Please sign in to comment.