Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow macro config to come from plugin options in babel config. #113

Merged
merged 5 commits into from
May 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why it did this. Maybe you hadn't run the newest version of your kcd-scripts formatter on this file?

- [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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer babel.config.js and babel-plugin-macros.config.js rather than .babel.config.js and .babel-plugin-macros.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
Loading