Skip to content

Commit

Permalink
feat(config): add support for configuring a macro (#36)
Browse files Browse the repository at this point in the history
Closes #26
  • Loading branch information
Kent C. Dodds committed Oct 15, 2017
1 parent fb5fe93 commit ab0e6c9
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 14 deletions.
43 changes: 42 additions & 1 deletion other/docs/author.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
A macro is a JavaScript module that exports a function. Here's a simple example:

```javascript
const {create: createMacro} = require('babel-macros')
const {createMacro} = require('babel-macros')

// `createMacros` is simply a function that ensures your macro is only
// called in the context of a babel transpilation and will throw an
Expand Down Expand Up @@ -146,6 +146,46 @@ you're given. For that check out [the babel handbook][babel-handbook].
> One other thing to note is that after your macro has run, babel-macros will
> remove the import/require statement for you.
#### config (EXPERIMENTAL!)

There is an experimental feature that allows users to configure your macro. We
use [`cosmiconfig`][cosmiconfig] to read a `babel-macros` configuration which
can be located in any of the following files up the directories from the
importing file:

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

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

```javascript
const {createMacro} = require('babel-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`
}
```

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

```javascript
// babel-macros.config.js
module.exports = {
taggedTranslations: {
someConfig: {}
}
}
```

And the `config` object you would receive would be: `{someConfig: {}}`.

## Throwing Helpful Errors

Debugging stuff that transpiles your code is the worst, especially for
Expand Down Expand Up @@ -222,3 +262,4 @@ Contributions to improve this experience are definitely welcome!
[tester]: https://github.com/babel-utils/babel-plugin-tester
[keyword]: https://docs.npmjs.com/files/package.json#keywords
[npm-babel-macros]: https://www.npmjs.com/browse/keyword/babel-macros
[cosmiconfig]: https://www.npmjs.com/package/cosmiconfig
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
],
"author": "Kent C. Dodds <kent@doddsfamily.us> (http://kentcdodds.com/)",
"license": "MIT",
"dependencies": {},
"dependencies": {
"cosmiconfig": "3.1.0"
},
"devDependencies": {
"ast-pretty-print": "2.0.1",
"babel-core": "^6.25.0",
Expand Down
5 changes: 5 additions & 0 deletions src/__tests__/__snapshots__/create-macros.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`attempting to create a macros with the configName of options throws an error 1`] = `"You cannot use the configName \\"options\\". It is reserved for babel-macros."`;

exports[`throws error if it is not transpiled 1`] = `"The macro you imported from \\"untranspiled.macro\\" is being executed outside the context of compilation with babel-macros. This indicates that you don't have the babel plugin \\"babel-macros\\" configured correctly. Please see the documentation for how to configure babel-macros properly: https://github.com/kentcdodds/babel-macros/blob/master/other/docs/user.md"`;
42 changes: 41 additions & 1 deletion src/__tests__/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ MacroError: <PROJECT_ROOT>/src/__tests__/index.js: very helpful
`;
exports[`macros can set their configName and get their config 1`] = `
import configured from './configurable.macro'
configured\`stuff\`
↓ ↓ ↓ ↓ ↓ ↓
configured\`stuff\`;
`;
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.,
]
`;
exports[`prepends the relative path for errors thrown by the macro 1`] = `
import errorThrower from './fixtures/error-thrower.macro'
Expand Down Expand Up @@ -108,7 +126,29 @@ Error: <PROJECT_ROOT>/src/__tests__/index.js: The macro imported from "./fixture
`;
exports[`throws error if it is not transpiled 1`] = `The macro you imported from "untranspiled.macro" is being executed outside the context of compilation with babel-macros. This indicates that you don't have the babel plugin "babel-macros" configured correctly. Please see the documentation for how to configure babel-macros properly: https://github.com/kentcdodds/babel-macros/blob/master/other/docs/user.md`;
exports[`when there is an error reading the config, a helpful message is logged 1`] = `
import configured from './configurable.macro'
configured\`stuff\`
↓ ↓ ↓ ↓ ↓ ↓
Error: <PROJECT_ROOT>/src/__tests__/fixtures/config/code.js: this is a cosmiconfig error
`;
exports[`when there is no config to load, then no config is passed 1`] = `
import configured from './configurable.macro'
configured\`stuff\`
↓ ↓ ↓ ↓ ↓ ↓
configured\`stuff\`;
`;
exports[`works with function calls 1`] = `
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/create-macros.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const {createMacro} = require('../')

test('throws error if it is not transpiled', () => {
const untranspiledMacro = createMacro(() => {})
expect(() =>
untranspiledMacro({source: 'untranspiled.macro'}),
).toThrowErrorMatchingSnapshot()
})

test('attempting to create a macros with the configName of options throws an error', () => {
expect(() =>
createMacro(() => {}, {configName: 'options'}),
).toThrowErrorMatchingSnapshot()
})
5 changes: 5 additions & 0 deletions src/__tests__/fixtures/config/babel-macros.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
configurableMacro: {
someConfig: true,
},
}
3 changes: 3 additions & 0 deletions src/__tests__/fixtures/config/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import configured from './configurable.macro'

configured`stuff`
10 changes: 10 additions & 0 deletions src/__tests__/fixtures/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,
})
61 changes: 54 additions & 7 deletions src/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/* eslint no-console:0 */
import path from 'path'
import cosmiconfigMock from 'cosmiconfig'
import cpy from 'cpy'
import babel from 'babel-core'
import pluginTester from 'babel-plugin-tester'
import plugin from '../'

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

jest.mock('cosmiconfig', () => jest.fn(require.requireActual('cosmiconfig')))

beforeAll(() => {
// copy our mock modules to the node_modules directory
// so we can test how things work when importing a macro
Expand Down Expand Up @@ -157,12 +161,55 @@ pluginTester({
errorThrower('hi')
`,
},
{
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-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()
},
},
{
title:
'when there is an error reading the config, a helpful message is logged',
error: true,
fixture: path.join(__dirname, 'fixtures/config/code.js'),
setup() {
cosmiconfigMock.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
}
},
},
{
title: 'when there is no config to load, then no config is passed',
fixture: path.join(__dirname, 'fixtures/config/code.js'),
setup() {
cosmiconfigMock.mockImplementationOnce(() => ({load: () => null}))
return function teardown() {
const configurableMacro = require('./fixtures/config/configurable.macro')
expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1)
expect(configurableMacro.realMacro).not.toHaveBeenCalledWith(
expect.objectContaining({
config: expect.any,
}),
)
configurableMacro.realMacro.mockClear()
}
},
},
],
})

test('throws error if it is not transpiled', () => {
const untranspiledMacro = plugin.createMacro(() => {})
expect(() =>
untranspiledMacro({source: 'untranspiled.macro'}),
).toThrowErrorMatchingSnapshot()
})
47 changes: 43 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@ class MacroError extends Error {
}
}

function createMacro(macro) {
function createMacro(macro, options = {}) {
if (options.configName === 'options') {
throw new Error(
`You cannot use the configName "options". It is reserved for babel-macros.`,
)
}
macroWrapper.isBabelMacro = true
macroWrapper.options = options
return macroWrapper

function macroWrapper(options) {
const {source, isBabelMacrosCall} = options
function macroWrapper(args) {
const {source, isBabelMacrosCall} = args
if (!isBabelMacrosCall) {
throw new MacroError(
`The macro you imported from "${source}" is being executed outside the context of compilation with babel-macros. ` +
Expand All @@ -31,7 +37,7 @@ function createMacro(macro) {
'https://github.com/kentcdodds/babel-macros/blob/master/other/docs/user.md',
)
}
return macro(options)
return macro(args)
}
}

Expand Down Expand Up @@ -127,11 +133,13 @@ function applyMacros({path, imports, source, state, babel}) {
`Please refer to the documentation to see how to do this properly: https://github.com/kentcdodds/babel-macros/blob/master/other/docs/author.md#writing-a-macro`,
)
}
const config = getConfig(macro, filename, source)
try {
macro({
references: referencePathsByImportName,
state,
babel,
config,
isBabelMacrosCall: true,
})
} catch (error) {
Expand All @@ -149,6 +157,37 @@ function applyMacros({path, imports, source, state, babel}) {
}
}

// eslint-disable-next-line consistent-return
function getConfig(macro, filename, source) {
if (macro.configName) {
try {
// lazy-loading it here to avoid perf issues of loading it up front.
// No I did not measure. Yes I'm a bad person.
// FWIW, this thing told me that cosmiconfig is 227.1 kb of minified JS
// so that's probably significant... https://bundlephobia.com/result?p=cosmiconfig@3.1.0
// Note that cosmiconfig will cache the babel-macros config 👍
const loaded = require('cosmiconfig')('babel-macros', {
packageProp: 'babelMacros',
rc: '.babel-macrosrc',
js: 'babel-macros.config.js',
rcExtensions: true,
sync: true,
}).load(filename)
if (loaded) {
return loaded.config[macro.configName]
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
`There was an error trying to load the config "${macro.configName}" ` +
`for the macro imported from "${source}. ` +
`Please see the error thrown for more information.`,
)
throw error
}
}
}

/*
istanbul ignore next
because this is hard to test
Expand Down

0 comments on commit ab0e6c9

Please sign in to comment.