Skip to content

Commit

Permalink
feat(config): allow macro config to come from plugin options in babel…
Browse files Browse the repository at this point in the history
… config. (#113)

* Allow macro config to come from plugin options in babel config.

* Respond to review feedback

* Ensure failing test output is readable

* Ensure cosmiconfig conf is cached

* Prioritize file config over plugin options config
  • Loading branch information
conartist6 authored and Kent C. Dodds committed May 30, 2019
1 parent e0ebca7 commit 796c189
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 92 deletions.
66 changes: 38 additions & 28 deletions other/docs/author.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
Is this your first time working with ASTs? Here are some resources:

* [Writing custom Babel and ESLint plugins with ASTs](https://youtu.be/VBscbcm2Mok?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf): A 53 minute talk by [@kentcdodds](https://twitter.com/kentcdodds)
* [babel-handbook](https://github.com/thejameskyle/babel-handbook): A guided handbook on how to use Babel and how to create plugins for Babel by [@thejameskyle](https://twitter.com/thejameskyle)
* [Code Transformation and Linting](https://kentcdodds.com/workshops/#code-transformation-and-linting): A workshop (recording available on Frontend Masters) with exercises of making custom Babel and ESLint plugins
- [Writing custom Babel and ESLint plugins with ASTs](https://youtu.be/VBscbcm2Mok?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf): A 53 minute talk by [@kentcdodds](https://twitter.com/kentcdodds)
- [babel-handbook](https://github.com/thejameskyle/babel-handbook): A guided handbook on how to use Babel and how to create plugins for Babel by [@thejameskyle](https://twitter.com/thejameskyle)
- [Code Transformation and Linting](https://kentcdodds.com/workshops/#code-transformation-and-linting): A workshop (recording available on Frontend Masters) with exercises of making custom Babel and ESLint plugins

## Writing a macro

Expand Down Expand Up @@ -164,43 +164,53 @@ This is a string used as import declaration's source - i.e. `'./my.macro'`.

#### config (EXPERIMENTAL!)

There is an experimental feature that allows users to configure your macro. We
use [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which
There is an experimental feature that allows users to configure your macro.

To specify that your plugin is configurable, you pass a `configName` to `createMacro`.

A configuration is created from data combined from two sources:
We use [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which
can be located in any of the following files up the directories from the
importing file:

* `.babel-plugin-macrosrc`
* `.babel-plugin-macrosrc.json`
* `.babel-plugin-macrosrc.yaml`
* `.babel-plugin-macrosrc.yml`
* `.babel-plugin-macrosrc.js`
* `babel-plugin-macros.config.js`
* `babelMacros` in `package.json`
- `.babel-plugin-macrosrc`
- `.babel-plugin-macrosrc.json`
- `.babel-plugin-macrosrc.yaml`
- `.babel-plugin-macrosrc.yml`
- `.babel-plugin-macrosrc.js`
- `babel-plugin-macros.config.js`
- `babelMacros` in `package.json`

To specify that your plugin is configurable, you pass a `configName` to
`createMacro`:
The content of the config will be merged with the content of the babel macros plugin
options. Config options take priority.

All together specifying and using the config might look like this:

```javascript
const {createMacro} = require('babel-plugin-macros')
const configName = 'taggedTranslations'
module.exports = createMacro(taggedTranslationsMacro, {configName})
function taggedTranslationsMacro({references, state, babel, config}) {
// config would be taggedTranslations portion of the config as loaded from `cosmiconfig`
// .babel-plugin-macros.config.js
module.exports = {
taggedTranslations: { locale: 'en_US' }
}
```

Then to configure this, users would do something like this:

```javascript
// babel-plugin-macros.config.js
// .babel.config.js
module.exports = {
taggedTranslations: {
someConfig: {},
},
plugins: [
['macros': {
taggedTranslations: { locale: 'en_GB' }
}]
]
}

// taggedTranslations.macro.js
const {createMacro} = require('babel-plugin-macros')
module.exports = createMacro(taggedTranslationsMacro, {configName: 'taggedTranslations'})
function taggedTranslationsMacro({references, state, babel, config}) {
const { locale = 'en' } = config;
}
```

And the `config` object you would receive would be: `{someConfig: {}}`.
Note that in the above example if both files were sepcified the final locale value would
be `en_US`, since that is the value in the plugin config file.

### Keeping imports

Expand Down
41 changes: 41 additions & 0 deletions src/__tests__/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,47 @@ global.result = result;
`;
exports[`macros when configuration is specified in plugin options: when configuration is specified in plugin options 1`] = `
import configured from './configurable.macro'
// eslint-disable-next-line babel/no-unused-expressions
configured\`stuff\`
↓ ↓ ↓ ↓ ↓ ↓
// eslint-disable-next-line babel/no-unused-expressions
configured\`stuff\`;
`;
exports[`macros when configuration is specified incorrectly in plugin options: when configuration is specified incorrectly in plugin options 1`] = `
import configured from './configurable.macro'
// eslint-disable-next-line babel/no-unused-expressions
configured\`stuff\`
↓ ↓ ↓ ↓ ↓ ↓
// eslint-disable-next-line babel/no-unused-expressions
configured\`stuff\`;
`;
exports[`macros when plugin options configuration cannot be merged with file configuration: when plugin options configuration cannot be merged with file configuration 1`] = `
import configured from './configurable.macro'
// eslint-disable-next-line babel/no-unused-expressions
configured\`stuff\`
↓ ↓ ↓ ↓ ↓ ↓
Error: <PROJECT_ROOT>/src/__tests__/fixtures/primitive-config/babel-plugin-macros.config.js specified a configurableMacro config of type object, but the the macros plugin's options.configurableMacro did contain an object. Both configs must contain objects for their options to be mergeable.
`;
exports[`macros when there is an error reading the config, a helpful message is logged 1`] = `
Array [
There was an error trying to load the config "configurableMacro" for the macro imported from "./configurable.macro. Please see the error thrown for more information.,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
configurableMacro: {
fileConfig: true,
someConfig: true,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
configurableMacro: 4,
}
4 changes: 4 additions & 0 deletions src/__tests__/fixtures/primitive-config/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import configured from './configurable.macro'

// eslint-disable-next-line babel/no-unused-expressions
configured`stuff`
10 changes: 10 additions & 0 deletions src/__tests__/fixtures/primitive-config/configurable.macro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const {createMacro} = require('../../../../')

const configName = 'configurableMacro'
const realMacro = jest.fn()
module.exports = createMacro(realMacro, {configName})
// for testing purposes only
Object.assign(module.exports, {
realMacro,
configName,
})
159 changes: 127 additions & 32 deletions src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,30 @@ import plugin from '../'

const projectRoot = path.join(__dirname, '../../')

jest.mock('cosmiconfig', () => jest.fn(require.requireActual('cosmiconfig')))
jest.mock('cosmiconfig', () => {
const mockSearchSync = jest.fn()
Object.assign(mockSearchSync, {
mockReset() {
return mockSearchSync.mockImplementation(
(filename, configuredCosmiconfig) =>
configuredCosmiconfig.searchSync(filename),
)
},
})

mockSearchSync.mockReset()

const _cosmiconfigMock = (...args) => ({
searchSync(filename) {
return mockSearchSync(
filename,
require.requireActual('cosmiconfig')(...args),
)
},
})

return Object.assign(_cosmiconfigMock, {mockSearchSync})
})

beforeAll(() => {
// copy our mock modules to the node_modules directory
Expand All @@ -23,6 +46,7 @@ beforeAll(() => {
afterEach(() => {
// eslint-disable-next-line
require('babel-plugin-macros-test-fake/macro').innerFn.mockClear()
cosmiconfigMock.mockSearchSync.mockReset()
})

expect.addSnapshotSerializer({
Expand Down Expand Up @@ -169,21 +193,26 @@ pluginTester({
fakeMacro('hi')
`,
teardown() {
// kinda abusing the babel-plugin-tester API here
// to make an extra assertion
// eslint-disable-next-line
const fakeMacro = require('babel-plugin-macros-test-fake/macro')
expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1)
expect(fakeMacro.innerFn).toHaveBeenCalledWith({
references: expect.any(Object),
source: expect.stringContaining(
'babel-plugin-macros-test-fake/macro',
),
state: expect.any(Object),
babel: expect.any(Object),
isBabelMacrosCall: true,
})
expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel)
try {
// kinda abusing the babel-plugin-tester API here
// to make an extra assertion
// eslint-disable-next-line
const fakeMacro = require('babel-plugin-macros-test-fake/macro')
expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1)
expect(fakeMacro.innerFn).toHaveBeenCalledWith({
references: expect.any(Object),
source: expect.stringContaining(
'babel-plugin-macros-test-fake/macro',
),
state: expect.any(Object),
babel: expect.any(Object),
isBabelMacrosCall: true,
})
expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel)
} catch (e) {
console.error(e)
throw e
}
},
},
{
Expand Down Expand Up @@ -258,15 +287,19 @@ pluginTester({
title: 'macros can set their configName and get their config',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
teardown() {
const babelMacrosConfig = require('./fixtures/config/babel-plugin-macros.config')
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro).toHaveBeenCalledWith(
expect.objectContaining({
config: babelMacrosConfig[configurableMacro.configName],
}),
)
configurableMacro.realMacro.mockClear()
try {
const babelMacrosConfig = require('./fixtures/config/babel-plugin-macros.config')
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual(
babelMacrosConfig[configurableMacro.configName],
)

configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
Expand All @@ -275,26 +308,76 @@ pluginTester({
error: true,
fixture: path.join(__dirname, 'fixtures/config/code.js'),
setup() {
cosmiconfigMock.mockImplementationOnce(() => {
cosmiconfigMock.mockSearchSync.mockImplementationOnce(() => {
throw new Error('this is a cosmiconfig error')
})
const originalError = console.error
console.error = jest.fn()
return function teardown() {
expect(console.error).toHaveBeenCalledTimes(1)
expect(console.error.mock.calls[0]).toMatchSnapshot()
console.error = originalError
try {
expect(console.error).toHaveBeenCalledTimes(1)
expect(console.error.mock.calls[0]).toMatchSnapshot()
console.error = originalError
} catch (e) {
console.error(e)
throw e
}
}
},
},
{
title: 'when there is no config to load, then no config is passed',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
setup() {
cosmiconfigMock.mockImplementationOnce(() => ({
searchSync: () => null,
}))
cosmiconfigMock.mockSearchSync.mockImplementationOnce(() => null)
return function teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual(
{},
)
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
}
},
},
{
title: 'when configuration is specified in plugin options',
pluginOptions: {
configurableMacro: {
someConfig: false,
somePluginConfig: true,
},
},
fixture: path.join(__dirname, 'fixtures/config/code.js'),
teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({
fileConfig: true,
someConfig: true,
somePluginConfig: true,
})
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
title: 'when configuration is specified incorrectly in plugin options',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
pluginOptions: {
configurableMacro: 2,
},
teardown() {
try {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro).not.toHaveBeenCalledWith(
Expand All @@ -303,9 +386,21 @@ pluginTester({
}),
)
configurableMacro.realMacro.mockClear()
} catch (e) {
console.error(e)
throw e
}
},
},
{
title:
'when plugin options configuration cannot be merged with file configuration',
error: true,
fixture: path.join(__dirname, 'fixtures/primitive-config/code.js'),
pluginOptions: {
configurableMacro: {},
},
},
{
title:
'when a plugin that replaces paths is used, macros still work properly',
Expand Down

0 comments on commit 796c189

Please sign in to comment.