├── .npmrc ├── .gitattributes ├── src ├── __tests__ │ ├── fixtures │ │ ├── non-wrapped.macro.js │ │ ├── config │ │ │ ├── code.js │ │ │ ├── babel-plugin-macros.config.js │ │ │ └── configurable.macro.js │ │ ├── emotion-esm.macro.js │ │ ├── error-thrower.macro.js │ │ ├── macro-error-thrower.macro.js │ │ ├── emotion.macro.js │ │ └── eval.macro.js │ ├── create-macros.js │ ├── __snapshots__ │ │ ├── create-macros.js.snap │ │ └── index.js.snap │ └── index.js └── index.js ├── .prettierignore ├── .gitignore ├── CHANGELOG.md ├── other ├── mock-modules │ ├── babel-plugin-macros-test-error-thrower │ │ └── macro.js │ ├── babel-plugin-macros-test-error-thrower.macro │ │ └── index.js │ └── babel-plugin-macros-test-fake │ │ └── macro.js ├── babel-config.js ├── manual-releases.md ├── docs │ ├── macros.md │ ├── user.md │ └── author.md ├── MAINTAINING.md └── CODE_OF_CONDUCT.md ├── .travis.yml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── .all-contributorsrc └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/non-wrapped.macro.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | package.json 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .opt-in 5 | .opt-out 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/config/code.js: -------------------------------------------------------------------------------- 1 | import configured from './configurable.macro' 2 | 3 | configured`stuff` 4 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/config/babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configurableMacro: { 3 | someConfig: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release). 4 | You can see it on the [releases page](../../releases). 5 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emotion-esm.macro.js: -------------------------------------------------------------------------------- 1 | const {createMacro} = require('../../') 2 | 3 | export default createMacro(evalMacro) 4 | 5 | function evalMacro() { 6 | // we're lazy right now 7 | // we don't want to eval 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/error-thrower.macro.js: -------------------------------------------------------------------------------- 1 | // const printAST = require('ast-pretty-print') 2 | const {createMacro} = require('../../') 3 | 4 | module.exports = createMacro(evalMacro) 5 | 6 | function evalMacro() { 7 | throw new Error('very unhelpful') 8 | } 9 | -------------------------------------------------------------------------------- /other/mock-modules/babel-plugin-macros-test-error-thrower/macro.js: -------------------------------------------------------------------------------- 1 | // const printAST = require('ast-pretty-print') 2 | const {createMacro} = require('../../src') 3 | 4 | module.exports = createMacro(evalMacro) 5 | 6 | function evalMacro() { 7 | throw new Error('not helpful') 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/macro-error-thrower.macro.js: -------------------------------------------------------------------------------- 1 | // const printAST = require('ast-pretty-print') 2 | const {createMacro, MacroError} = require('../../') 3 | 4 | module.exports = createMacro(evalMacro) 5 | 6 | function evalMacro() { 7 | throw new MacroError('very helpful') 8 | } 9 | -------------------------------------------------------------------------------- /other/mock-modules/babel-plugin-macros-test-error-thrower.macro/index.js: -------------------------------------------------------------------------------- 1 | // const printAST = require('ast-pretty-print') 2 | const {createMacro} = require('../../src') 3 | 4 | module.exports = createMacro(evalMacro) 5 | 6 | function evalMacro() { 7 | throw new Error('not helpful') 8 | } 9 | -------------------------------------------------------------------------------- /other/mock-modules/babel-plugin-macros-test-fake/macro.js: -------------------------------------------------------------------------------- 1 | // this is used to make sure that you can require macro from node_modules 2 | const {createMacro} = require('../../src') 3 | 4 | const innerFn = jest.fn() 5 | module.exports = createMacro(innerFn) 6 | module.exports.innerFn = innerFn 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - ~/.npm 6 | notifications: 7 | email: false 8 | node_js: '8' 9 | install: npm install 10 | script: npm run validate 11 | after_success: kcd-scripts travis-after-success 12 | branches: 13 | only: master 14 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/config/configurable.macro.js: -------------------------------------------------------------------------------- 1 | const {createMacro} = require('../../../../') 2 | 3 | const configName = 'configurableMacro' 4 | const realMacro = jest.fn() 5 | module.exports = createMacro(realMacro, {configName}) 6 | // for testing purposes only 7 | Object.assign(module.exports, { 8 | realMacro, 9 | configName, 10 | }) 11 | -------------------------------------------------------------------------------- /other/babel-config.js: -------------------------------------------------------------------------------- 1 | const babelConfig = require('kcd-scripts/babel') 2 | 3 | // we don't want to use babel-macros inside babel-macros... 4 | // and until kcd-scripts gets upgraded to babel-plugin-macros 5 | // we'll have both of those referenced here... 6 | babelConfig.plugins = babelConfig.plugins.filter( 7 | pluginPath => 8 | !pluginPath.includes('babel-macros/') && 9 | !pluginPath.includes('babel-plugin-macros/'), 10 | ) 11 | module.exports = babelConfig 12 | -------------------------------------------------------------------------------- /src/__tests__/create-macros.js: -------------------------------------------------------------------------------- 1 | const {createMacro} = require('../') 2 | 3 | test('throws error if it is not transpiled', () => { 4 | const untranspiledMacro = createMacro(() => {}) 5 | expect(() => 6 | untranspiledMacro({source: 'untranspiled.macro'}), 7 | ).toThrowErrorMatchingSnapshot() 8 | }) 9 | 10 | test('attempting to create a macros with the configName of options throws an error', () => { 11 | expect(() => 12 | createMacro(() => {}, {configName: 'options'}), 13 | ).toThrowErrorMatchingSnapshot() 14 | }) 15 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/create-macros.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | 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-plugin-macros."`; 4 | 5 | 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-plugin-macros. This indicates that you don't have the babel plugin \\"babel-plugin-macros\\" configured correctly. Please see the documentation for how to configure babel-plugin-macros properly: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md"`; 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/emotion.macro.js: -------------------------------------------------------------------------------- 1 | // this is a fake version of emotion 2 | // const printAST = require('ast-pretty-print') 3 | const {createMacro} = require('../../') 4 | 5 | module.exports = createMacro(emotionMacro) 6 | 7 | function emotionMacro({references, babel}) { 8 | const {types: t} = babel 9 | references.css.forEach(cssRef => { 10 | if (cssRef.parentPath.type === 'TaggedTemplateExpression') { 11 | cssRef.parentPath.replaceWith( 12 | t.stringLiteral( 13 | cssRef.parentPath 14 | .get('quasi') 15 | .evaluate() 16 | .value.trim(), 17 | ), 18 | ) 19 | } 20 | }) 21 | references.styled.forEach(styledRef => { 22 | if (styledRef.parentPath.parentPath.type === 'TaggedTemplateExpression') { 23 | const quasi = styledRef.parentPath.parentPath.get('quasi') 24 | const val = quasi.evaluate().value.trim() 25 | const replacement = t.templateLiteral([t.templateElement(val)], []) 26 | quasi.replaceWith(replacement) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | * `babel-plugin-macros` version: 15 | * `node` version: 16 | * `npm` (or `yarn`) version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | ``` 22 | 23 | What you did: 24 | 25 | What happened: 26 | 27 | 28 | 29 | Reproduction repository: 30 | 31 | 35 | 36 | Problem description: 37 | 38 | Suggested solution: 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when there are 4 | useful changes in the code that justify a release. But sometimes things get messed up one way or another 5 | and we need to trigger the release ourselves. When this happens, simply bump the number below and commit 6 | that with the following commit message based on your needs: 7 | 8 | **Major** 9 | 10 | ``` 11 | fix(release): manually release a major version 12 | 13 | There was an issue with a major release, so this manual-releases.md 14 | change is to release a new major version. 15 | 16 | Reference: # 17 | 18 | BREAKING CHANGE: 19 | ``` 20 | 21 | **Minor** 22 | 23 | ``` 24 | feat(release): manually release a minor version 25 | 26 | There was an issue with a minor release, so this manual-releases.md 27 | change is to release a new minor version. 28 | 29 | Reference: # 30 | ``` 31 | 32 | **Patch** 33 | 34 | ``` 35 | fix(release): manually release a patch version 36 | 37 | There was an issue with a patch release, so this manual-releases.md 38 | change is to release a new patch version. 39 | 40 | Reference: # 41 | ``` 42 | 43 | The number of times we've had to do a manual release is: 0 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-macros", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Enables zero-config, importable babel plugins", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "add-contributor": "kcd-scripts contributors add", 8 | "build": "kcd-scripts build", 9 | "lint": "kcd-scripts lint", 10 | "test": "kcd-scripts test", 11 | "test:update": "npm test -- --updateSnapshot", 12 | "validate": "kcd-scripts validate", 13 | "setup": "npm install && npm run validate -s", 14 | "precommit": "kcd-scripts precommit" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "keywords": [ 20 | "babel-plugin", 21 | "macros", 22 | "macro", 23 | "babel-macro", 24 | "babel-plugin-macro" 25 | ], 26 | "author": "Kent C. Dodds (http://kentcdodds.com/)", 27 | "license": "MIT", 28 | "dependencies": { 29 | "cosmiconfig": "^4.0.0" 30 | }, 31 | "devDependencies": { 32 | "ast-pretty-print": "^2.0.1", 33 | "babel-core": "7.0.0-beta.3", 34 | "babel-plugin-tester": "^5.0.0", 35 | "babylon": "7.0.0-beta.34", 36 | "cpy": "^6.0.0", 37 | "kcd-scripts": "^0.32.1" 38 | }, 39 | "eslintConfig": { 40 | "extends": "./node_modules/kcd-scripts/eslint.js" 41 | }, 42 | "eslintIgnore": [ 43 | "node_modules", 44 | "coverage", 45 | "dist" 46 | ], 47 | "babel": { 48 | "presets": [ 49 | "./other/babel-config.js" 50 | ] 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/kentcdodds/babel-plugin-macros.git" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/kentcdodds/babel-plugin-macros/issues" 58 | }, 59 | "homepage": "https://github.com/kentcdodds/babel-plugin-macros#readme" 60 | } 61 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/eval.macro.js: -------------------------------------------------------------------------------- 1 | const babylon = require('babylon') 2 | // const printAST = require('ast-pretty-print') 3 | const {createMacro} = require('../../') 4 | 5 | module.exports = createMacro(evalMacro) 6 | 7 | function evalMacro({references, state}) { 8 | references.default.forEach(referencePath => { 9 | if (referencePath.parentPath.type === 'TaggedTemplateExpression') { 10 | asTag(referencePath.parentPath.get('quasi'), state) 11 | } else if (referencePath.parentPath.type === 'CallExpression') { 12 | asFunction(referencePath.parentPath.get('arguments'), state) 13 | } else if (referencePath.parentPath.type === 'JSXOpeningElement') { 14 | asJSX( 15 | { 16 | attributes: referencePath.parentPath.get('attributes'), 17 | children: referencePath.parentPath.parentPath.get('children'), 18 | }, 19 | state, 20 | ) 21 | } else { 22 | // TODO: throw a helpful error message 23 | } 24 | }) 25 | } 26 | 27 | function asTag(quasiPath) { 28 | const value = quasiPath.parentPath.get('quasi').evaluate().value 29 | quasiPath.parentPath.replaceWith(evalToAST(value)) 30 | } 31 | 32 | function asFunction(argumentsPaths) { 33 | const value = argumentsPaths[0].evaluate().value 34 | argumentsPaths[0].parentPath.replaceWith(evalToAST(value)) 35 | } 36 | 37 | // eslint-disable-next-line no-unused-vars 38 | function asJSX({attributes, children}) { 39 | // It's a shame you cannot use evaluate() with JSX 40 | const value = children[0].node.value 41 | children[0].parentPath.replaceWith(evalToAST(value)) 42 | } 43 | 44 | function evalToAST(value) { 45 | let x 46 | // eslint-disable-next-line 47 | eval(`x = ${value}`) 48 | return thingToAST(x) 49 | } 50 | 51 | function thingToAST(object) { 52 | const fileNode = babylon.parse(`var x = ${JSON.stringify(object)}`) 53 | return fileNode.program.body[0].declarations[0].init 54 | } 55 | -------------------------------------------------------------------------------- /other/docs/macros.md: -------------------------------------------------------------------------------- 1 | # macros 2 | 3 | `babel-plugin-macros` is only as useful as the `macros` you add to your project. 4 | You can make local macros for your project (`import a from './a.macro'`), but 5 | there's an ecosystem of existing macros out there that you might find interesting: 6 | 7 | * [`preval.macro`](https://www.npmjs.com/package/preval.macro): Pre-evaluate code at build-time 8 | * [`codegen.macro`](https://www.npmjs.com/package/codegen.macro): 💥 Generate code at build-time 9 | * [`import-all.macro`](https://www.npmjs.com/package/import-all.macro): A babel-macro that allows you to import all files that match a glob 10 | * [`tagged-translations`](https://www.npmjs.com/package/tagged-translations): A dead simple babel-plugin for translating texts in React applications. 11 | * [`traph.macro`](https://www.npmjs.com/package/traph.macro): JS Babel macro for Object transformation graph 12 | * [`param.macro`](https://www.npmjs.com/package/param.macro): Partial application syntax and lambda parameters for JavaScript, inspired by Scala's `_` & Kotlin's it. 13 | * [`ms.macro`](https://www.npmjs.com/package/ms.macro): Convert various time formats to milliseconds at build time in Babel. 14 | * [`react-emotion/macro`](https://github.com/emotion-js/emotion/tree/78fea2d2eb74269645b28fe12392ecc09882f55f/packages/babel-plugin-emotion#babel-macros): Babel plugin for the minification and optimization of emotion styles. 15 | * [`scope.macro`](https://www.npmjs.com/package/scope.macro): Adds useful build time console functions 16 | * [`graphql.macro`](https://github.com/evenchange4/graphql.macro): Compile GraphQL AST at build-time with babel-plugin-macros. 17 | * [`svgr.macro`](https://github.com/evenchange4/svgr.macro): Run svgr at build-time with babel-plugin-macros. 18 | * [`glamorous.macro`](https://github.com/kentcdodds/glamorous.macro): Give your glamorous components a nice `displayName` for React DevTools. 19 | * [`raw.macro`](https://github.com/pveyes/raw.macro): Webpack raw-loader implemented with babel-plugin-macros. 20 | * [`penv.macro`](https://github.com/chengjianhua/penv.macro): Pick specified value or branch according to the environment of building. 21 | 22 | Please note that macros are intended to be used as devDependencies. 23 | 24 | In addition, you can 25 | [search npm for `keyword:babel-plugin-macros`](https://www.npmjs.com/search?q=keywords:babel-plugin-macros) 26 | to find macros. 27 | -------------------------------------------------------------------------------- /other/docs/user.md: -------------------------------------------------------------------------------- 1 | # `babel-plugin-macros` Usage for users 2 | 3 | > See also: [the `author` docs](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md). 4 | 5 | ## Adding the plugin to your config 6 | 7 | ### Via `.babelrc` (Recommended) 8 | 9 | **.babelrc** 10 | 11 | ```json 12 | { 13 | "plugins": ["macros"] 14 | } 15 | ``` 16 | 17 | ### Via CLI 18 | 19 | ```shell 20 | babel --plugins babel-plugin-macros script.js 21 | ``` 22 | 23 | ### Via Node API 24 | 25 | ```js 26 | require('babel-core').transform('code', { 27 | plugins: ['macros'], 28 | }) 29 | ``` 30 | 31 | ## Using a macro 32 | 33 | With the `babel-plugin-macros` plugin added to your config, we can now use a macro 34 | that works with the `babel-plugin-macros` API. Let's assume we have such a module 35 | in our project called `eval.macro.js`. To use it, we `import` or `require` 36 | the macro module in our code like so: 37 | 38 | ```javascript 39 | import MyEval from './eval.macro' 40 | // or 41 | const MyEval = require('./eval.macro') 42 | ``` 43 | 44 | Then we use that variable however the documentation for the macro says. 45 | Incidentally, `eval.macro.js` actually exists in the tests for `babel-plugin-macros` 46 | [here][eval-macro] and you can see how it transforms our code in 47 | [the `babel-plugin-macros` snapshots][eval-snapshots]. 48 | 49 | > Note here that the real benefit is that we don't need to configure anything 50 | > for every macro you add. We simply configure `babel-plugin-macros`, then we can 51 | > use any macro available. This is part of the benefit of using `babel-plugin-macros`. 52 | 53 | [eval-macro]: https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/fixtures/eval.macro.js 54 | [eval-snapshots]: https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/__snapshots__/index.js.snap 55 | 56 | ### Using with create-react-app 57 | 58 | `babel-plugin-macros` is currently available to be used with the alpha version of react-scripts 2.0! This is awesome because it allows for babel to be configured in a nice way without having to eject from `create-react-app`! 59 | 60 | Before deciding to use this however you should be aware of a few things: 61 | 62 | 1. Features may be broken or not work as expected 63 | 2. There will be more breaking changes introduced before the final release ⚠️ 64 | 3. Documentation for new features is still sparse, so look through the pull requests for how they're expected to work 65 | 66 | With that being said you can use all the awesomeness of `babel-plugin-macros` inside `create-react-app` by running one of the following commands based on your situation. 67 | 68 | ``` 69 | $ # Create a new application 70 | $ npx create-react-app@next --scripts-version=2.0.0-next.47d2d941 71 | $ # Upgrade an existing application 72 | $ yarn upgrade react-scripts@2.0.0-next.47d2d941 73 | ``` 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ series 6 | [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `master` branch pointing at the original repository and make 15 | > pull requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/kentcdodds/babel-plugin-macros.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/master master 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," 24 | > Then fetch the git information from that remote, then set your local `master` 25 | > branch to use the upstream master branch whenever you run `git pull`. 26 | > Then you can make all of your pull request branches based on this `master` 27 | > branch. Whenever you want to update your version of `master`, do a regular 28 | > `git pull`. 29 | 30 | ## Add yourself as a contributor 31 | 32 | This project follows the [all contributors][all-contributors] specification. 33 | To add yourself to the table of contributors on the `README.md`, please use the 34 | automated script as part of your PR: 35 | 36 | ```console 37 | npm run add-contributor 38 | ``` 39 | 40 | Follow the prompt and commit `.all-contributorsrc` and `README.md` in the PR. 41 | If you've already added yourself to the list and are making 42 | a new type of contribution, you can run it again and select the added 43 | contribution type. 44 | 45 | ## Committing and Pushing changes 46 | 47 | Please make sure to run the tests before you commit your changes. You can run 48 | `npm run test:update` which will update any snapshots that need updating. 49 | Make sure to include those changes (if they exist) in your commit. 50 | 51 | ### opt into git hooks 52 | 53 | There are git hooks set up with this project that are automatically installed 54 | when you install dependencies. They're really handy, but are turned off by 55 | default (so as to not hinder new contributors). You can opt into these by 56 | creating a file called `.opt-in` at the root of the project and putting this 57 | inside: 58 | 59 | ``` 60 | pre-commit 61 | ``` 62 | 63 | ## Help needed 64 | 65 | Please checkout the [the open issues][issues] 66 | 67 | Also, please watch the repo and respond to questions/bug reports/feature 68 | requests! Thanks! 69 | 70 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 71 | [all-contributors]: https://github.com/kentcdodds/all-contributors 72 | [issues]: https://github.com/kentcdodds/babel-plugin-macros/issues 73 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | This is documentation for maintainers of this project. 4 | 5 | ## Code of Conduct 6 | 7 | Please review, understand, and be an example of it. Violations of the code of conduct are 8 | taken seriously, even (especially) for maintainers. 9 | 10 | ## Issues 11 | 12 | We want to support and build the community. We do that best by helping people learn to solve 13 | their own problems. We have an issue template and hopefully most folks follow it. If it's 14 | not clear what the issue is, invite them to create a minimal reproduction of what they're trying 15 | to accomplish or the bug they think they've found. 16 | 17 | Once it's determined that a code change is necessary, point people to 18 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a pull request. 19 | If they're the one who needs the feature, they're the one who can build it. If they need 20 | some hand holding and you have time to lend a hand, please do so. It's an investment into 21 | another human being, and an investment into a potential maintainer. 22 | 23 | Remember that this is open source, so the code is not yours, it's ours. If someone needs a change 24 | in the codebase, you don't have to make it happen yourself. Commit as much time to the project 25 | as you want/need to. Nobody can ask any more of you than that. 26 | 27 | ## Pull Requests 28 | 29 | As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either 30 | way is fine. 31 | 32 | When we receive a pull request, a travis build is kicked off automatically (see the `.travis.yml` 33 | for what runs in the travis build). We avoid merging anything that breaks the travis build. 34 | 35 | Please review PRs and focus on the code rather than the individual. You never know when this is 36 | someone's first ever PR and we want their experience to be as positive as possible, so be 37 | uplifting and constructive. 38 | 39 | When you merge the pull request, 99% of the time you should use the 40 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) feature. This keeps 41 | our git history clean, but more importantly, this allows us to make any necessary changes to the 42 | commit message so we release what we want to release. See the next section on Releases for more 43 | about that. 44 | 45 | ## Release 46 | 47 | Our releases are automatic. They happen whenever code lands into `master`. A travis build gets 48 | kicked off and if it's successful, a tool called 49 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is used to 50 | automatically publish a new release to npm as well as a changelog to GitHub. It is only able to 51 | determine the version and whether a release is necessary by the git commit messages. With this 52 | in mind, **please brush up on [the commit message convention][commit] which drives our releases.** 53 | 54 | > One important note about this: Please make sure that commit messages do NOT contain the words 55 | > "BREAKING CHANGE" in them unless we want to push a major version. I've been burned by this 56 | > more than once where someone will include "BREAKING CHANGE: None" and it will end up releasing 57 | > a new major version. Not a huge deal honestly, but kind of annoying... 58 | 59 | ## Thanks! 60 | 61 | Thank you so much for helping to maintain this project! 62 | 63 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 64 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kent+coc@doddsfamily.us. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "babel-plugin-macros", 3 | "projectOwner": "kentcdodds", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "contributors": [ 11 | { 12 | "login": "kentcdodds", 13 | "name": "Kent C. Dodds", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 15 | "profile": "https://kentcdodds.com", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "infra", 20 | "test" 21 | ] 22 | }, 23 | { 24 | "login": "threepointone", 25 | "name": "Sunil Pai", 26 | "avatar_url": "https://avatars1.githubusercontent.com/u/18808?v=3", 27 | "profile": "https://github.com/threepointone", 28 | "contributions": [ 29 | "ideas" 30 | ] 31 | }, 32 | { 33 | "login": "suchipi", 34 | "name": "Stephen Scott", 35 | "avatar_url": "https://avatars3.githubusercontent.com/u/1341513?v=3", 36 | "profile": "http://suchipi.com/", 37 | "contributions": [ 38 | "question", 39 | "doc" 40 | ] 41 | }, 42 | { 43 | "login": "dralletje", 44 | "name": "Michiel Dral", 45 | "avatar_url": "https://avatars1.githubusercontent.com/u/767261?v=4", 46 | "profile": "http://twitter.com/dralletje", 47 | "contributions": [ 48 | "ideas" 49 | ] 50 | }, 51 | { 52 | "login": "tkh44", 53 | "name": "Kye Hohenberger", 54 | "avatar_url": "https://avatars2.githubusercontent.com/u/662750?v=4", 55 | "profile": "https://github.com/tkh44", 56 | "contributions": [ 57 | "ideas" 58 | ] 59 | }, 60 | { 61 | "login": "mitchellhamilton", 62 | "name": "Mitchell Hamilton", 63 | "avatar_url": "https://avatars1.githubusercontent.com/u/11481355?v=4", 64 | "profile": "https://hamil.town", 65 | "contributions": [ 66 | "code", 67 | "test" 68 | ] 69 | }, 70 | { 71 | "login": "wKovacs64", 72 | "name": "Justin Hall", 73 | "avatar_url": "https://avatars1.githubusercontent.com/u/1288694?v=4", 74 | "profile": "https://github.com/wKovacs64", 75 | "contributions": [ 76 | "doc" 77 | ] 78 | }, 79 | { 80 | "login": "PiereDome", 81 | "name": "Brian Pedersen", 82 | "avatar_url": "https://avatars3.githubusercontent.com/u/1903016?v=4", 83 | "profile": "https://github.com/PiereDome", 84 | "contributions": [ 85 | "code", 86 | "doc" 87 | ] 88 | }, 89 | { 90 | "login": "apalm", 91 | "name": "Andrew Palm", 92 | "avatar_url": "https://avatars3.githubusercontent.com/u/4495237?v=4", 93 | "profile": "https://github.com/apalm", 94 | "contributions": [ 95 | "code" 96 | ] 97 | }, 98 | { 99 | "login": "evenchange4", 100 | "name": "Michael Hsu", 101 | "avatar_url": "https://avatars1.githubusercontent.com/u/1527371?v=4", 102 | "profile": "https://michaelhsu.tw/", 103 | "contributions": [ 104 | "doc", 105 | "plugin" 106 | ] 107 | }, 108 | { 109 | "login": "citycide", 110 | "name": "Bo Lingen", 111 | "avatar_url": "https://avatars2.githubusercontent.com/u/16605186?v=4", 112 | "profile": "https://github.com/citycide", 113 | "contributions": [ 114 | "code" 115 | ] 116 | }, 117 | { 118 | "login": "tylerthehaas", 119 | "name": "Tyler Haas", 120 | "avatar_url": "https://avatars1.githubusercontent.com/u/11150235?v=4", 121 | "profile": "https://github.com/tylerthehaas", 122 | "contributions": [ 123 | "doc" 124 | ] 125 | }, 126 | { 127 | "login": "FWeinb", 128 | "name": "FWeinb", 129 | "avatar_url": "https://avatars0.githubusercontent.com/u/1250430?v=4", 130 | "profile": "https://github.com/FWeinb", 131 | "contributions": [ 132 | "code" 133 | ] 134 | } 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`macros Supports named imports: Supports named imports 1`] = ` 4 | 5 | import {css as CSS, styled as STYLED} from './fixtures/emotion.macro' 6 | const red = CSS\` 7 | background-color: red; 8 | \` 9 | 10 | const Div = STYLED.div\` 11 | composes: \${red} 12 | color: blue; 13 | \` 14 | 15 | ↓ ↓ ↓ ↓ ↓ ↓ 16 | 17 | const red = "background-color: red;"; 18 | const Div = STYLED.div\`undefined\`; 19 | 20 | `; 21 | 22 | exports[`macros Works as a JSXElement: Works as a JSXElement 1`] = ` 23 | 24 | import MyEval from './fixtures/eval.macro' 25 | const x = 34 + 45 26 | 27 | ↓ ↓ ↓ ↓ ↓ ↓ 28 | 29 | const x = 79; 30 | 31 | `; 32 | 33 | exports[`macros appends the npm URL for errors thrown by node modules with a slash: appends the npm URL for errors thrown by node modules with a slash 1`] = ` 34 | 35 | import errorThrower from 'babel-plugin-macros-test-error-thrower/macro' 36 | errorThrower('hi') 37 | 38 | ↓ ↓ ↓ ↓ ↓ ↓ 39 | 40 | Error: babel-plugin-macros-test-error-thrower/macro: not helpful Learn more: https://www.npmjs.com/package/babel-plugin-macros-test-error-thrower 41 | 42 | `; 43 | 44 | exports[`macros appends the npm URL for errors thrown by node modules: appends the npm URL for errors thrown by node modules 1`] = ` 45 | 46 | import errorThrower from 'babel-plugin-macros-test-error-thrower.macro' 47 | errorThrower('hi') 48 | 49 | ↓ ↓ ↓ ↓ ↓ ↓ 50 | 51 | Error: babel-plugin-macros-test-error-thrower.macro: not helpful Learn more: https://www.npmjs.com/package/babel-plugin-macros-test-error-thrower.macro 52 | 53 | `; 54 | 55 | exports[`macros does nothing but remove macros if it is unused: does nothing but remove macros if it is unused 1`] = ` 56 | 57 | import foo from './some-macros-that-doesnt-even-need-to-exist.macro' 58 | export default 'something else' 59 | 60 | ↓ ↓ ↓ ↓ ↓ ↓ 61 | 62 | export default 'something else'; 63 | 64 | `; 65 | 66 | exports[`macros forwards MacroErrors thrown by the macro: forwards MacroErrors thrown by the macro 1`] = ` 67 | 68 | import errorThrower from './fixtures/macro-error-thrower.macro' 69 | errorThrower('hey') 70 | 71 | ↓ ↓ ↓ ↓ ↓ ↓ 72 | 73 | MacroError: very helpful 74 | 75 | `; 76 | 77 | exports[`macros macros can set their configName and get their config: macros can set their configName and get their config 1`] = ` 78 | 79 | import configured from './configurable.macro' 80 | 81 | configured\`stuff\` 82 | 83 | ↓ ↓ ↓ ↓ ↓ ↓ 84 | 85 | configured\`stuff\`; 86 | 87 | `; 88 | 89 | exports[`macros prepends the relative path for errors thrown by the macro: prepends the relative path for errors thrown by the macro 1`] = ` 90 | 91 | import errorThrower from './fixtures/error-thrower.macro' 92 | errorThrower('hey') 93 | 94 | ↓ ↓ ↓ ↓ ↓ ↓ 95 | 96 | Error: ./fixtures/error-thrower.macro: very unhelpful 97 | 98 | `; 99 | 100 | exports[`macros supports compiled macros (\`__esModule\` + \`export default\`): supports compiled macros (\`__esModule\` + \`export default\`) 1`] = ` 101 | 102 | import {css, styled} from './fixtures/emotion-esm.macro' 103 | const red = css\` 104 | background-color: red; 105 | \` 106 | 107 | const Div = styled.div\` 108 | composes: \${red} 109 | color: blue; 110 | \` 111 | 112 | ↓ ↓ ↓ ↓ ↓ ↓ 113 | 114 | const red = css\` 115 | background-color: red; 116 | \`; 117 | const Div = styled.div\` 118 | composes: \${red} 119 | color: blue; 120 | \`; 121 | 122 | `; 123 | 124 | exports[`macros supports macros from node_modules: supports macros from node_modules 1`] = ` 125 | 126 | import fakeMacro from 'babel-plugin-macros-test-fake/macro' 127 | fakeMacro('hi') 128 | 129 | ↓ ↓ ↓ ↓ ↓ ↓ 130 | 131 | fakeMacro('hi'); 132 | 133 | `; 134 | 135 | exports[`macros throws an error if the macro is not properly wrapped: throws an error if the macro is not properly wrapped 1`] = ` 136 | 137 | import unwrapped from './fixtures/non-wrapped.macro' 138 | unwrapped('hey') 139 | 140 | ↓ ↓ ↓ ↓ ↓ ↓ 141 | 142 | Error: The macro imported from "./fixtures/non-wrapped.macro" must be wrapped in "createMacro" which you can get from "babel-plugin-macros". Please refer to the documentation to see how to do this properly: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md#writing-a-macro 143 | 144 | `; 145 | 146 | exports[`macros when there is an error reading the config, a helpful message is logged 1`] = ` 147 | Array [ 148 | 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., 149 | ] 150 | `; 151 | 152 | exports[`macros when there is an error reading the config, a helpful message is logged: when there is an error reading the config, a helpful message is logged 1`] = ` 153 | 154 | import configured from './configurable.macro' 155 | 156 | configured\`stuff\` 157 | 158 | ↓ ↓ ↓ ↓ ↓ ↓ 159 | 160 | Error: this is a cosmiconfig error 161 | 162 | `; 163 | 164 | exports[`macros when there is no config to load, then no config is passed: when there is no config to load, then no config is passed 1`] = ` 165 | 166 | import configured from './configurable.macro' 167 | 168 | configured\`stuff\` 169 | 170 | ↓ ↓ ↓ ↓ ↓ ↓ 171 | 172 | configured\`stuff\`; 173 | 174 | `; 175 | 176 | exports[`macros works with function calls: works with function calls 1`] = ` 177 | 178 | import myEval from './fixtures/eval.macro' 179 | const x = myEval('34 + 45') 180 | 181 | ↓ ↓ ↓ ↓ ↓ ↓ 182 | 183 | const x = 79; 184 | 185 | `; 186 | 187 | exports[`macros works with import: works with import 1`] = ` 188 | 189 | import myEval from './fixtures/eval.macro' 190 | const x = myEval\`34 + 45\` 191 | 192 | ↓ ↓ ↓ ↓ ↓ ↓ 193 | 194 | const x = 79; 195 | 196 | `; 197 | 198 | exports[`macros works with require destructuring and aliasing: works with require destructuring and aliasing 1`] = ` 199 | 200 | const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro') 201 | const red = CSS\` 202 | background-color: red; 203 | \` 204 | 205 | const Div = STYLED.div\` 206 | composes: \${red} 207 | color: blue; 208 | \` 209 | 210 | ↓ ↓ ↓ ↓ ↓ ↓ 211 | 212 | const red = "background-color: red;"; 213 | const Div = STYLED.div\`undefined\`; 214 | 215 | `; 216 | 217 | exports[`macros works with require destructuring: works with require destructuring 1`] = ` 218 | 219 | const {css, styled} = require('./fixtures/emotion.macro') 220 | const red = css\` 221 | background-color: red; 222 | \` 223 | 224 | const Div = styled.div\` 225 | composes: \${red} 226 | color: blue; 227 | \` 228 | 229 | ↓ ↓ ↓ ↓ ↓ ↓ 230 | 231 | const red = "background-color: red;"; 232 | const Div = styled.div\`undefined\`; 233 | 234 | `; 235 | 236 | exports[`macros works with require: works with require 1`] = ` 237 | 238 | const evaler = require('./fixtures/eval.macro') 239 | const x = evaler\`34 + 45\` 240 | 241 | ↓ ↓ ↓ ↓ ↓ ↓ 242 | 243 | const x = 79; 244 | 245 | `; 246 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const p = require('path') 2 | // const printAST = require('ast-pretty-print') 3 | 4 | const macrosRegex = /[./]macro(\.js)?$/ 5 | 6 | // https://stackoverflow.com/a/32749533/971592 7 | class MacroError extends Error { 8 | constructor(message) { 9 | super(message) 10 | this.name = 'MacroError' 11 | /* istanbul ignore else */ 12 | if (typeof Error.captureStackTrace === 'function') { 13 | Error.captureStackTrace(this, this.constructor) 14 | } else if (!this.stack) { 15 | this.stack = new Error(message).stack 16 | } 17 | } 18 | } 19 | 20 | function createMacro(macro, options = {}) { 21 | if (options.configName === 'options') { 22 | throw new Error( 23 | `You cannot use the configName "options". It is reserved for babel-plugin-macros.`, 24 | ) 25 | } 26 | macroWrapper.isBabelMacro = true 27 | macroWrapper.options = options 28 | return macroWrapper 29 | 30 | function macroWrapper(args) { 31 | const {source, isBabelMacrosCall} = args 32 | if (!isBabelMacrosCall) { 33 | throw new MacroError( 34 | `The macro you imported from "${source}" is being executed outside the context of compilation with babel-plugin-macros. ` + 35 | `This indicates that you don't have the babel plugin "babel-plugin-macros" configured correctly. ` + 36 | `Please see the documentation for how to configure babel-plugin-macros properly: ` + 37 | 'https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md', 38 | ) 39 | } 40 | return macro(args) 41 | } 42 | } 43 | 44 | function macrosPlugin(babel, {require: _require = require} = {}) { 45 | function interopRequire(path) { 46 | // eslint-disable-next-line import/no-dynamic-require 47 | const o = _require(path) 48 | return o && o.__esModule && o.default ? o.default : o 49 | } 50 | 51 | return { 52 | name: 'macros', 53 | visitor: { 54 | ImportDeclaration(path, state) { 55 | const isMacros = looksLike(path, { 56 | node: { 57 | source: { 58 | value: v => macrosRegex.test(v), 59 | }, 60 | }, 61 | }) 62 | if (!isMacros) { 63 | return 64 | } 65 | const imports = path.node.specifiers.map(s => ({ 66 | localName: s.local.name, 67 | importedName: 68 | s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name, 69 | })) 70 | const source = path.node.source.value 71 | applyMacros({ 72 | path, 73 | imports, 74 | source, 75 | state, 76 | babel, 77 | interopRequire, 78 | }) 79 | path.remove() 80 | }, 81 | VariableDeclaration(path, state) { 82 | const isMacros = child => 83 | looksLike(child, { 84 | node: { 85 | init: { 86 | callee: { 87 | type: 'Identifier', 88 | name: 'require', 89 | }, 90 | arguments: args => 91 | args.length === 1 && macrosRegex.test(args[0].value), 92 | }, 93 | }, 94 | }) 95 | 96 | path 97 | .get('declarations') 98 | .filter(isMacros) 99 | .forEach(child => { 100 | const imports = child.node.id.name 101 | ? [{localName: child.node.id.name, importedName: 'default'}] 102 | : child.node.id.properties.map(property => ({ 103 | localName: property.value.name, 104 | importedName: property.key.name, 105 | })) 106 | 107 | const call = child.get('init') 108 | const source = call.node.arguments[0].value 109 | applyMacros({ 110 | path: call, 111 | imports, 112 | source, 113 | state, 114 | babel, 115 | interopRequire, 116 | }) 117 | 118 | child.remove() 119 | }) 120 | }, 121 | }, 122 | } 123 | } 124 | 125 | // eslint-disable-next-line complexity 126 | function applyMacros({path, imports, source, state, babel, interopRequire}) { 127 | const {file: {opts: {filename}}} = state 128 | let hasReferences = false 129 | const referencePathsByImportName = imports.reduce( 130 | (byName, {importedName, localName}) => { 131 | byName[importedName] = path.scope.getBinding(localName).referencePaths 132 | hasReferences = hasReferences || Boolean(byName[importedName].length) 133 | return byName 134 | }, 135 | {}, 136 | ) 137 | if (!hasReferences) { 138 | return 139 | } 140 | let requirePath = source 141 | const isRelative = source.indexOf('.') === 0 142 | if (isRelative) { 143 | requirePath = p.join(p.dirname(getFullFilename(filename)), source) 144 | } 145 | const macro = interopRequire(requirePath) 146 | if (!macro.isBabelMacro) { 147 | throw new Error( 148 | // eslint-disable-next-line prefer-template 149 | `The macro imported from "${source}" must be wrapped in "createMacro" ` + 150 | `which you can get from "babel-plugin-macros". ` + 151 | `Please refer to the documentation to see how to do this properly: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md#writing-a-macro`, 152 | ) 153 | } 154 | const config = getConfig(macro, filename, source) 155 | try { 156 | macro({ 157 | references: referencePathsByImportName, 158 | state, 159 | babel, 160 | config, 161 | isBabelMacrosCall: true, 162 | }) 163 | } catch (error) { 164 | if (error.name === 'MacroError') { 165 | throw error 166 | } 167 | error.message = `${source}: ${error.message}` 168 | if (!isRelative) { 169 | error.message = `${ 170 | error.message 171 | } Learn more: https://www.npmjs.com/package/${source.replace( 172 | /(\/.*)/g, 173 | '', 174 | )}` 175 | } 176 | throw error 177 | } 178 | } 179 | 180 | // eslint-disable-next-line consistent-return 181 | function getConfig(macro, filename, source) { 182 | if (macro.options.configName) { 183 | try { 184 | // lazy-loading it here to avoid perf issues of loading it up front. 185 | // No I did not measure. Yes I'm a bad person. 186 | // FWIW, this thing told me that cosmiconfig is 227.1 kb of minified JS 187 | // so that's probably significant... https://bundlephobia.com/result?p=cosmiconfig@3.1.0 188 | // Note that cosmiconfig will cache the babel-plugin-macros config 👍 189 | const loaded = require('cosmiconfig')('babel-plugin-macros', { 190 | packageProp: 'babelMacros', 191 | rc: '.babel-plugin-macrosrc', 192 | js: 'babel-plugin-macros.config.js', 193 | rcExtensions: true, 194 | sync: true, 195 | }).load(filename) 196 | if (loaded) { 197 | return loaded.config[macro.options.configName] 198 | } 199 | } catch (error) { 200 | // eslint-disable-next-line no-console 201 | console.error( 202 | `There was an error trying to load the config "${ 203 | macro.options.configName 204 | }" ` + 205 | `for the macro imported from "${source}. ` + 206 | `Please see the error thrown for more information.`, 207 | ) 208 | throw error 209 | } 210 | } 211 | } 212 | 213 | /* 214 | istanbul ignore next 215 | because this is hard to test 216 | and not worth it... 217 | */ 218 | function getFullFilename(filename) { 219 | if (p.isAbsolute(filename)) { 220 | return filename 221 | } 222 | return p.join(process.cwd(), filename) 223 | } 224 | 225 | function looksLike(a, b) { 226 | return ( 227 | a && 228 | b && 229 | Object.keys(b).every(bKey => { 230 | const bVal = b[bKey] 231 | const aVal = a[bKey] 232 | if (typeof bVal === 'function') { 233 | return bVal(aVal) 234 | } 235 | return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal) 236 | }) 237 | ) 238 | } 239 | 240 | function isPrimitive(val) { 241 | // eslint-disable-next-line 242 | return val == null || /^[sbn]/.test(typeof val) 243 | } 244 | 245 | module.exports = macrosPlugin 246 | Object.assign(module.exports, { 247 | createMacro, 248 | MacroError, 249 | }) 250 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import path from 'path' 3 | import cosmiconfigMock from 'cosmiconfig' 4 | import cpy from 'cpy' 5 | import babel from 'babel-core' 6 | import pluginTester from 'babel-plugin-tester' 7 | import plugin from '../' 8 | 9 | const projectRoot = path.join(__dirname, '../../') 10 | 11 | jest.mock('cosmiconfig', () => jest.fn(require.requireActual('cosmiconfig'))) 12 | 13 | beforeAll(() => { 14 | // copy our mock modules to the node_modules directory 15 | // so we can test how things work when importing a macro 16 | // from the node_modules directory. 17 | return cpy(['**/*.js'], path.join('..', '..', 'node_modules'), { 18 | parents: true, 19 | cwd: path.join(projectRoot, 'other', 'mock-modules'), 20 | }) 21 | }) 22 | 23 | afterEach(() => { 24 | // eslint-disable-next-line 25 | require('babel-plugin-macros-test-fake/macro').innerFn.mockClear() 26 | }) 27 | 28 | expect.addSnapshotSerializer({ 29 | print(val) { 30 | return val 31 | .split(projectRoot) 32 | .join('/') 33 | .replace(/\\/g, '/') 34 | }, 35 | test(val) { 36 | return typeof val === 'string' 37 | }, 38 | }) 39 | 40 | pluginTester({ 41 | plugin, 42 | snapshot: true, 43 | babelOptions: {filename: __filename, parserOpts: {plugins: ['jsx']}}, 44 | tests: [ 45 | { 46 | title: 'does nothing to code that does not import macro', 47 | snapshot: false, 48 | code: ` 49 | import foo from "./some-file-without-macro"; 50 | 51 | const bar = require("./some-other-file-without-macro"); 52 | `, 53 | }, 54 | { 55 | title: 'does nothing but remove macros if it is unused', 56 | code: ` 57 | import foo from './some-macros-that-doesnt-even-need-to-exist.macro' 58 | export default 'something else' 59 | `, 60 | }, 61 | { 62 | title: 'works with import', 63 | code: ` 64 | import myEval from './fixtures/eval.macro' 65 | const x = myEval\`34 + 45\` 66 | `, 67 | }, 68 | { 69 | title: 'works with require', 70 | code: ` 71 | const evaler = require('./fixtures/eval.macro') 72 | const x = evaler\`34 + 45\` 73 | `, 74 | }, 75 | { 76 | title: 'works with require destructuring', 77 | code: ` 78 | const {css, styled} = require('./fixtures/emotion.macro') 79 | const red = css\` 80 | background-color: red; 81 | \` 82 | 83 | const Div = styled.div\` 84 | composes: \${red} 85 | color: blue; 86 | \` 87 | `, 88 | }, 89 | { 90 | title: 'works with require destructuring and aliasing', 91 | code: ` 92 | const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro') 93 | const red = CSS\` 94 | background-color: red; 95 | \` 96 | 97 | const Div = STYLED.div\` 98 | composes: \${red} 99 | color: blue; 100 | \` 101 | `, 102 | }, 103 | { 104 | title: 'works with function calls', 105 | code: ` 106 | import myEval from './fixtures/eval.macro' 107 | const x = myEval('34 + 45') 108 | `, 109 | }, 110 | { 111 | title: 'Works as a JSXElement', 112 | code: ` 113 | import MyEval from './fixtures/eval.macro' 114 | const x = 34 + 45 115 | `, 116 | }, 117 | { 118 | title: 'Supports named imports', 119 | code: ` 120 | import {css as CSS, styled as STYLED} from './fixtures/emotion.macro' 121 | const red = CSS\` 122 | background-color: red; 123 | \` 124 | 125 | const Div = STYLED.div\` 126 | composes: \${red} 127 | color: blue; 128 | \` 129 | `, 130 | }, 131 | { 132 | title: 'supports compiled macros (`__esModule` + `export default`)', 133 | code: ` 134 | import {css, styled} from './fixtures/emotion-esm.macro' 135 | const red = css\` 136 | background-color: red; 137 | \` 138 | 139 | const Div = styled.div\` 140 | composes: \${red} 141 | color: blue; 142 | \` 143 | `, 144 | }, 145 | { 146 | title: 'supports macros from node_modules', 147 | code: ` 148 | import fakeMacro from 'babel-plugin-macros-test-fake/macro' 149 | fakeMacro('hi') 150 | `, 151 | teardown() { 152 | // kinda abusing the babel-plugin-tester API here 153 | // to make an extra assertion 154 | // eslint-disable-next-line 155 | const fakeMacro = require('babel-plugin-macros-test-fake/macro') 156 | expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1) 157 | expect(fakeMacro.innerFn).toHaveBeenCalledWith({ 158 | references: expect.any(Object), 159 | state: expect.any(Object), 160 | babel: expect.any(Object), 161 | isBabelMacrosCall: true, 162 | }) 163 | expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel) 164 | }, 165 | }, 166 | { 167 | title: 'throws an error if the macro is not properly wrapped', 168 | error: true, 169 | code: ` 170 | import unwrapped from './fixtures/non-wrapped.macro' 171 | unwrapped('hey') 172 | `, 173 | }, 174 | { 175 | title: 'forwards MacroErrors thrown by the macro', 176 | error: true, 177 | code: ` 178 | import errorThrower from './fixtures/macro-error-thrower.macro' 179 | errorThrower('hey') 180 | `, 181 | }, 182 | { 183 | title: 'prepends the relative path for errors thrown by the macro', 184 | error: true, 185 | code: ` 186 | import errorThrower from './fixtures/error-thrower.macro' 187 | errorThrower('hey') 188 | `, 189 | }, 190 | { 191 | title: 'appends the npm URL for errors thrown by node modules', 192 | error: true, 193 | code: ` 194 | import errorThrower from 'babel-plugin-macros-test-error-thrower.macro' 195 | errorThrower('hi') 196 | `, 197 | }, 198 | { 199 | title: 200 | 'appends the npm URL for errors thrown by node modules with a slash', 201 | error: true, 202 | code: ` 203 | import errorThrower from 'babel-plugin-macros-test-error-thrower/macro' 204 | errorThrower('hi') 205 | `, 206 | }, 207 | { 208 | title: 'macros can set their configName and get their config', 209 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 210 | teardown() { 211 | const babelMacrosConfig = require('./fixtures/config/babel-plugin-macros.config') 212 | const configurableMacro = require('./fixtures/config/configurable.macro') 213 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 214 | expect(configurableMacro.realMacro).toHaveBeenCalledWith( 215 | expect.objectContaining({ 216 | config: babelMacrosConfig[configurableMacro.configName], 217 | }), 218 | ) 219 | configurableMacro.realMacro.mockClear() 220 | }, 221 | }, 222 | { 223 | title: 224 | 'when there is an error reading the config, a helpful message is logged', 225 | error: true, 226 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 227 | setup() { 228 | cosmiconfigMock.mockImplementationOnce(() => { 229 | throw new Error('this is a cosmiconfig error') 230 | }) 231 | const originalError = console.error 232 | console.error = jest.fn() 233 | return function teardown() { 234 | expect(console.error).toHaveBeenCalledTimes(1) 235 | expect(console.error.mock.calls[0]).toMatchSnapshot() 236 | console.error = originalError 237 | } 238 | }, 239 | }, 240 | { 241 | title: 'when there is no config to load, then no config is passed', 242 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 243 | setup() { 244 | cosmiconfigMock.mockImplementationOnce(() => ({load: () => null})) 245 | return function teardown() { 246 | const configurableMacro = require('./fixtures/config/configurable.macro') 247 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 248 | expect(configurableMacro.realMacro).not.toHaveBeenCalledWith( 249 | expect.objectContaining({ 250 | config: expect.any, 251 | }), 252 | ) 253 | configurableMacro.realMacro.mockClear() 254 | } 255 | }, 256 | }, 257 | ], 258 | }) 259 | -------------------------------------------------------------------------------- /other/docs/author.md: -------------------------------------------------------------------------------- 1 | # `babel-plugin-macros` Usage for macros authors 2 | 3 | > See also: [the `user` docs](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md). 4 | 5 | Is this your first time working with ASTs? Here are some resources: 6 | 7 | * [Writing custom Babel and ESLint plugins with ASTs](https://youtu.be/VBscbcm2Mok?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf): A 53 minute talk by [@kentcdodds](https://twitter.com/kentcdodds) 8 | * [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) 9 | * [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 10 | 11 | ## Writing a macro 12 | 13 | A macro is a JavaScript module that exports a function. Here's a simple example: 14 | 15 | ```javascript 16 | const {createMacro} = require('babel-plugin-macros') 17 | 18 | // `createMacro` is simply a function that ensures your macro is only 19 | // called in the context of a babel transpilation and will throw an 20 | // error with a helpful message if someone does not have babel-plugin-macros 21 | // configured correctly 22 | module.exports = createMacro(myMacro) 23 | 24 | function myMacro({references, state, babel}) { 25 | // state is the second argument you're passed to a visitor in a 26 | // normal babel plugin. `babel` is the `babel-plugin-macros` module. 27 | // do whatever you like to the AST paths you find in `references` 28 | // read more below... 29 | } 30 | ``` 31 | 32 | It can be published to the npm registry (for generic macros, like a css-in-js 33 | library) or used locally (for domain-specific macros, like handling some special 34 | case for your company's localization efforts). 35 | 36 | > Before you write a custom macro, you might consider whether 37 | > [`babel-plugin-preval`][preval] help you do what you want as it's pretty 38 | > powerful. 39 | 40 | There are two parts to the `babel-plugin-macros` API: 41 | 42 | 1. The filename convention 43 | 2. The function you export 44 | 45 | ### Filename 46 | 47 | The way that `babel-plugin-macros` determines whether to run a macro is based on the 48 | source string of the `import` or `require` statement. It must match this regex: 49 | `/[./]macro(\.js)?$/` for example: 50 | 51 | _matches_: 52 | 53 | ``` 54 | 'my.macro' 55 | 'my.macro.js' 56 | 'my/macro' 57 | 'my/macro.js' 58 | ``` 59 | 60 | _does not match_: 61 | 62 | ``` 63 | 'my-macro' 64 | 'my.macro.is-sweet' 65 | 'my/macro/rocks' 66 | ``` 67 | 68 | > So long as your file can be required at a matching path, you're good. So you 69 | > could put it in: `my/macro/index.js` and people would: `require('my/macro')` 70 | > which would work fine. 71 | 72 | **If you're going to publish this to npm,** the most ergonomic thing would be to 73 | name it something that ends in `.macro`. If it's part of a larger package, 74 | then calling the file `macro.js` or placing it in `macro/index.js` is a great 75 | way to go as well. Then people could do: 76 | 77 | ```js 78 | import Nice from 'nice.macro' 79 | // or 80 | import Sweet from 'sweet/macro' 81 | ``` 82 | 83 | In addition, please publish your macro with the [`keyword`][keyword] of 84 | `babel-plugin-macros` (note the "s"). That way folks can easily find macros by 85 | searching for the [`babel-plugin-macros` keyword on npm][npm-babel-plugin-macros]. In 86 | addition, and you can add this badge to the top of your README: 87 | 88 | [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros) 89 | 90 | ``` 91 | [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros) 92 | ``` 93 | 94 | ### Function API 95 | 96 | The macro you create should export a function. That function accepts a single 97 | parameter which is an object with the following properties: 98 | 99 | #### state 100 | 101 | The state of the file being traversed. It's the second argument 102 | you receive in a visitor function in a normal babel plugin. 103 | 104 | #### babel 105 | 106 | This is the same thing you get as an argument to normal babel plugins. 107 | It is also the same thing you get if you `require('babel-core')`. 108 | 109 | #### references 110 | 111 | This is an object that contains arrays of all the references to 112 | things imported from macro keyed based on the name of the import. The items 113 | in each array are the paths to the references. 114 | 115 |
116 | 117 | Some examples: 118 | 119 | ```javascript 120 | import MyMacro from './my.macro' 121 | 122 | MyMacro( 123 | {someOption: true}, 124 | ` 125 | some stuff 126 | `, 127 | ) 128 | 129 | // references: { default: [BabelPath] } 130 | ``` 131 | 132 | ```javascript 133 | import {foo as FooMacro} from './my.macro' 134 | 135 | FooMacro( 136 | {someOption: true}, 137 | ` 138 | some stuff 139 | `, 140 | ) 141 | 142 | // references: { foo: [BabelPath] } 143 | ``` 144 | 145 | ```javascript 146 | import {foo as FooMacro} from './my.macro' 147 | 148 | // no usage... 149 | 150 | // references: {} 151 | ``` 152 | 153 |
154 | 155 | From here, it's just a matter of doing doing stuff with the `BabelPath`s that 156 | you're given. For that check out [the babel handbook][babel-handbook]. 157 | 158 | > One other thing to note is that after your macro has run, babel-plugin-macros will 159 | > remove the import/require statement for you. 160 | 161 | #### config (EXPERIMENTAL!) 162 | 163 | There is an experimental feature that allows users to configure your macro. We 164 | use [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which 165 | can be located in any of the following files up the directories from the 166 | importing file: 167 | 168 | * `.babel-plugin-macrosrc` 169 | * `.babel-plugin-macrosrc.json` 170 | * `.babel-plugin-macrosrc.yaml` 171 | * `.babel-plugin-macrosrc.yml` 172 | * `.babel-plugin-macrosrc.js` 173 | * `babel-plugin-macros.config.js` 174 | * `babelMacros` in `package.json` 175 | 176 | To specify that your plugin is configurable, you pass a `configName` to 177 | `createMacro`: 178 | 179 | ```javascript 180 | const {createMacro} = require('babel-plugin-macros') 181 | const configName = 'taggedTranslations' 182 | module.exports = createMacro(taggedTranslationsMacro, {configName}) 183 | function taggedTranslationsMacro({references, state, babel, config}) { 184 | // config would be taggedTranslations portion of the config as loaded from `cosmiconfig` 185 | } 186 | ``` 187 | 188 | Then to configure this, users would do something like this: 189 | 190 | ```javascript 191 | // babel-plugin-macros.config.js 192 | module.exports = { 193 | taggedTranslations: { 194 | someConfig: {}, 195 | }, 196 | } 197 | ``` 198 | 199 | And the `config` object you would receive would be: `{someConfig: {}}`. 200 | 201 | ## Throwing Helpful Errors 202 | 203 | Debugging stuff that transpiles your code is the worst, especially for 204 | beginners. That's why it's important that you make assertions, and catch errors 205 | to throw more meaningful errors with helpful information for the developer to 206 | know what to do to resolve the issue. 207 | 208 | In an effort to make this easier for you, `babel-plugin-macros` will wrap the 209 | invocation of your plugin in a `try/catch` and throw as helpful an error message 210 | as possible for you. 211 | 212 | To make it even better, you can throw your own with more context. For example: 213 | 214 | ```javascript 215 | const {createMacro, MacroError} = require('babel-plugin-macros') 216 | 217 | module.exports = createMacro(myMacro) 218 | 219 | function myMacro({references, state, babel}) { 220 | // something unexpected happens: 221 | throw new MacroError( 222 | 'Some helpful and contextual message. Learn more: ' + 223 | 'https://github.com/your-org/your-repo/blob/master/docs/errors.md#learn-more-about-eror-title', 224 | ) 225 | } 226 | ``` 227 | 228 | ## Testing your macro 229 | 230 | The best way to test your macro is using [`babel-plugin-tester`][tester]: 231 | 232 | ```javascript 233 | import pluginTester from 'babel-plugin-tester' 234 | import plugin from 'babel-plugin-macros' 235 | 236 | pluginTester({ 237 | plugin, 238 | snapshot: true, 239 | babelOptions: {filename: __filename}, 240 | tests: [ 241 | ` 242 | import MyMacro from '../my.macro' 243 | 244 | MyMacro({someOption: true}, \` 245 | some stuff 246 | \`) 247 | `, 248 | ], 249 | }) 250 | ``` 251 | 252 | There is currently no way to get code coverage for your macro this way however. 253 | If you want code coverage, you'll have to call your macro yourself. 254 | Contributions to improve this experience are definitely welcome! 255 | 256 | [preval]: https://github.com/kentcdodds/babel-plugin-preval 257 | [babel-handbook]: https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md 258 | [tester]: https://github.com/babel-utils/babel-plugin-tester 259 | [keyword]: https://docs.npmjs.com/files/package.json#keywords 260 | [npm-babel-plugin-macros]: https://www.npmjs.com/browse/keyword/babel-plugin-macros 261 | [cosmiconfig]: https://www.npmjs.com/package/cosmiconfig 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

babel-plugin-macros 🎣

3 | 4 | Enables zero-config, importable babel plugins 5 | 6 |
7 | 8 |
9 | 10 | [![Build Status][build-badge]][build] 11 | [![Code Coverage][coverage-badge]][coverage] 12 | [![version][version-badge]][package] 13 | [![downloads][downloads-badge]][npmchart] 14 | [![MIT License][license-badge]][license] 15 | 16 | [![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) 17 | [![PRs Welcome][prs-badge]][prs] 18 | [![Donate][donate-badge]][donate] 19 | [![Code of Conduct][coc-badge]][coc] 20 | 21 | [![Watch on GitHub][github-watch-badge]][github-watch] 22 | [![Star on GitHub][github-star-badge]][github-star] 23 | [![Tweet][twitter-badge]][twitter] 24 | 25 | ## The problem 26 | 27 | Check out this guest post on the Babel.js blog for a complete write up on the problem, motivation, and solution. 28 | 29 | Currently, each babel plugin in the babel ecosystem requires that you configure 30 | it individually. This is fine for things like language features, but can be 31 | frustrating overhead for libraries that allow for compile-time code 32 | transformation as an optimization. 33 | 34 | ## This solution 35 | 36 | babel-plugin-macros defines a standard interface for libraries that want to use 37 | compile-time code transformation without requiring the user to add a babel 38 | plugin to their build system (other than `babel-plugin-macros`, which is ideally 39 | already in place). 40 | 41 |
42 | 43 | Expand for more details on the motivation 44 | 45 | For instance, many css-in-js libraries have a css tagged template string 46 | function: 47 | 48 | ```js 49 | const styles = css` 50 | .red { 51 | color: red; 52 | } 53 | ` 54 | ``` 55 | 56 | The function compiles your css into (for example) an object with generated class 57 | names for each of the classes you defined in your css: 58 | 59 | ```js 60 | console.log(styles) // { red: "1f-d34j8rn43y587t" } 61 | ``` 62 | 63 | This class name can be generated at runtime (in the browser), but this has some 64 | disadvantages: 65 | 66 | * There is cpu usage/time overhead; the client needs to run the code to generate 67 | these classes every time the page loads 68 | * There is code bundle size overhead; the client needs to receive a CSS parser 69 | in order to generate these class names, and shipping this makes the amount of 70 | js the client needs to parse larger. 71 | 72 | To help solve those issues, many css-in-js libraries write their own babel 73 | plugin that generates the class names at compile-time instead of runtime: 74 | 75 | ```js 76 | // Before running through babel: 77 | const styles = css` 78 | .red { 79 | color: red; 80 | } 81 | ` 82 | // After running through babel, with the library-specific plugin: 83 | const styles = {red: '1f-d34j8rn43y587t'} 84 | ``` 85 | 86 | If the css-in-js library supported babel-plugin-macros instead, then they 87 | wouldn't need their own babel plugin to compile these out; they could instead 88 | rely on babel-plugin-macros to do it for them. So if a user already had 89 | `babel-plugin-macros` installed and configured with babel, then they wouldn't 90 | need to change their babel configuration to get the compile-time benefits of the 91 | library. This would be most useful if the boilerplate they were using came with 92 | `babel-plugin-macros` out of the box, which is true for 93 | [`create-react-app`][cra]. 94 | 95 | Although css-in-js is the most common example, there are lots of other things 96 | you could use `babel-plugin-macros` for, like: 97 | 98 | * Compiling GraphQL fragments into objects so that the client doesn't need a 99 | GraphQL parser 100 | * Eval-ing out code at compile time that will be baked into the runtime code, 101 | for instance to get a list of directories in the filesystem (see 102 | [preval][preval]) 103 | 104 |
105 | 106 | ## Table of Contents 107 | 108 | 109 | 110 | 111 | 112 | * [Installation](#installation) 113 | * [Usage](#usage) 114 | * [User docs](#user-docs) 115 | * [Author docs](#author-docs) 116 | * [Caveats](#caveats) 117 | * [FAQ](#faq) 118 | * [How do I find available macros?](#how-do-i-find-available-macros) 119 | * [What's the difference between babel plugins and macros?](#whats-the-difference-between-babel-plugins-and-macros) 120 | * [In what order are macros executed?](#in-what-order-are-macros-executed) 121 | * [Does it work with function calls only?](#does-it-work-with-function-calls-only) 122 | * [How about implicit optimizations at compile time?](#how-about-implicit-optimizations-at-compile-time) 123 | * [Should macros be dependencies or devDependencies?](#should-macros-be-dependencies-or-devdependencies) 124 | * [Inspiration](#inspiration) 125 | * [Other Solutions](#other-solutions) 126 | * [Contributors](#contributors) 127 | * [LICENSE](#license) 128 | 129 | 130 | 131 | ## Installation 132 | 133 | This module is distributed via [npm][npm] which is bundled with [node][node] and 134 | should be installed as one of your project's `devDependencies`: 135 | 136 | ``` 137 | npm install --save-dev babel-plugin-macros 138 | ``` 139 | 140 | ## Usage 141 | 142 | ### User docs 143 | 144 | Are you trying to use `babel-plugin-macros`? Go to 145 | [`other/docs/user.md`](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md). 146 | 147 | ### Author docs 148 | 149 | Are you trying to make your own macros that works with `babel-plugin-macros`? Go to 150 | [`other/docs/author.md`](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md). 151 | (you should probably read the user docs too). 152 | 153 | ### Caveats 154 | 155 | #### Babel cache problem 156 | 157 | Most of the time you'll probably be using this with the babel cache enabled in webpack to rebuild faster. If your macro function is **not pure** which gets different output with same code (e.g., IO side effects) it will cause recompile mechanism fail. Unfortunately you'll also experience this problem while developing your macro as well. If there's not a change to the source code that's being transpiled, then babel will use the cache rather than running your macro again. 158 | 159 | For now, to force recompile the code you can simply add a cache busting comment in the file: 160 | 161 | ```diff 162 | import macro from 'non-pure.macro'; 163 | 164 | -// Do some changes of your code or 165 | +// add a cache busting comment to force recompile. 166 | macro('parameters'); 167 | ``` 168 | 169 | This problem is still being worked on and is not unique to `babel-plugin-macros`. For more details and workarounds, please check related issues below: 170 | 171 | * babel-plugin-preval: [How to force recompile? #19](https://github.com/kentcdodds/babel-plugin-preval/issues/19) 172 | * graphql.macro: [Recompile problem (babel cache) #6](https://github.com/evenchange4/graphql.macro/issues/6) 173 | 174 | ## FAQ 175 | 176 | ### How do I find available macros? 177 | 178 | You can write your own without publishing them to `npm`, but if you'd like to 179 | see existing macros you can add to your project, then take a look at 180 | [`other/docs/macros.md`](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/macros.md) 181 | 182 | Please add any you don't see listed! 183 | 184 | ### What's the difference between babel plugins and macros? 185 | 186 | Let's use 187 | [`babel-plugin-console`](https://www.npmjs.com/package/babel-plugin-console) as 188 | an example. 189 | 190 | If we used `babel-plugin-console`, it would look like this: 191 | 192 | 1. Add `babel-plugin-console` to `.babelrc` 193 | 2. Use it in a code: 194 | 195 | ```js 196 | function add100(a) { 197 | const oneHundred = 100 198 | console.scope('Add 100 to another number') 199 | return add(a, oneHundred) 200 | } 201 | 202 | function add(a, b) { 203 | return a + b 204 | } 205 | ``` 206 | 207 | When that code is run, the `scope` function does some pretty nifty things: 208 | 209 | **Browser:** 210 | 211 | ![Browser console scoping add100](https://github.com/mattphillips/babel-plugin-console/raw/53536cba919d5be49d4f66d957769c07ca7a4207/assets/add100-chrome.gif) 212 | 213 | **Node:** 214 | 215 | Node console scoping add100 216 | 217 | Instead, let's use the macro it's shipped with like this: 218 | 219 | 1. Add `babel-plugin-macros` to `.babelrc` (only once for all macros) 220 | 2. Use it in a code: 221 | 222 | ```js 223 | import scope from 'babel-plugin-console/scope.macro' 224 | function add100(a) { 225 | const oneHundred = 100 226 | scope('Add 100 to another number') 227 | return add(a, oneHundred) 228 | } 229 | 230 | function add(a, b) { 231 | return a + b 232 | } 233 | ``` 234 | 235 | The result is exactly the same, but this approach has a few advantages: 236 | 237 | **Advantages:** 238 | 239 | * requires only one entry in `.babelrc` for all macros used in project. Add that 240 | once and you can use all the macros you want 241 | * toolkits (like [create-react-app][cra]) may already support 242 | `babel-plugin-macros`, so no configuration is needed at all 243 | * it's explicit. With `console.scope` people may be fooled that it's just a 244 | normal `console` API when there's really a babel transpilation going on. When 245 | you import `scope`, it's obvious that it's macro and does something with the 246 | code at compile time. Some ESLint rules may also have issues with plugins that 247 | look for "global" variables 248 | * macros are safer and easier to write, because they receive exactly the AST 249 | node to process 250 | * If you misconfigure `babel-plugin-console` you wont find out until you run the 251 | code. If you misconfigure `babel-plugin-macros` you'll get a compile-time 252 | error. 253 | 254 | **Drawbacks:** 255 | 256 | * Cannot (should not) be used for implicit transpilations (like syntax plugins) 257 | * Explicitness is more verbose. Which some people might consider a drawback... 258 | 259 | ### In what order are macros executed? 260 | 261 | This is another advantage of `babel-plugin-macros` over regular plugins. The 262 | user of the macro is in control of the ordering! The order of execution is the 263 | same order as imported. The order of execution is clear, explicit and in full 264 | control of the user: 265 | 266 | ```js 267 | import preval from 'preval.macro' 268 | import idx from 'idx.macro' 269 | 270 | // preval macro is evaluated first, then idx 271 | ``` 272 | 273 | This differs from the current situation with babel plugins where it's 274 | prohibitively difficult to control the order plugins run in a particular file. 275 | 276 | ### Does it work with function calls only? 277 | 278 | No! Any AST node type is supported. 279 | 280 | It can be tagged template literal: 281 | 282 | ```js 283 | import eval from 'eval.macro' 284 | const val = eval`7 * 6` 285 | ``` 286 | 287 | A function: 288 | 289 | ```js 290 | import eval from 'eval.macro' 291 | const val = eval('7 * 6') 292 | ``` 293 | 294 | JSX Element: 295 | 296 | ```js 297 | import Eval from 'eval.macro' 298 | const val = 7 * 6 299 | ``` 300 | 301 | Really, anything... 302 | 303 | See the [testing snapshot](https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/__snapshots__/index.js.snap) for more examples. 304 | 305 | ### How about implicit optimizations at compile time? 306 | 307 | All examples above were _explicit_ - a macro was imported and then evaluated 308 | with a specific AST node. 309 | 310 | Completely different story are _implicit_ babel plugins, like 311 | [transform-react-constant-elements](https://babeljs.io/docs/plugins/transform-react-constant-elements/), 312 | which process whole AST tree. 313 | 314 | Explicit is often a better pattern than implicit because it requires others to 315 | understand how things are globally configured. This is in this spirit are 316 | `babel-plugin-macros` designed. However, some things _do_ need to be implicit, 317 | and those kinds of babel plugins can't be turned into macros. 318 | 319 | ### Should macros be dependencies or devDependencies? 320 | 321 | Macros are processed at build-time and not required at runtime. They should be devDependencies. 322 | 323 | ## Inspiration 324 | 325 | * [threepointone/babel-plugin-macros](https://github.com/threepointone/babel-plugin-macros) 326 | * [facebookincubator/create-react-app#2730][cra-issue] 327 | 328 | Thank you to [@phpnode](https://github.com/phpnode) for donating the npm package 329 | `babel-plugin-macros`. 330 | 331 | ## Other Solutions 332 | 333 | * [sweetjs](http://sweetjs.org/) 334 | 335 | ## Contributors 336 | 337 | Thanks goes to these people ([emoji key][emojis]): 338 | 339 | 340 | 341 | 342 | | [
Kent C. Dodds](https://kentcdodds.com)
[💻](/kentcdodds/babel-plugin-macros/commits?author=kentcdodds "Code") [📖](/kentcdodds/babel-plugin-macros/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](/kentcdodds/babel-plugin-macros/commits?author=kentcdodds "Tests") | [
Sunil Pai](https://github.com/threepointone)
[🤔](#ideas-threepointone "Ideas, Planning, & Feedback") | [
Stephen Scott](http://suchipi.com/)
[💬](#question-suchipi "Answering Questions") [📖](/kentcdodds/babel-plugin-macros/commits?author=suchipi "Documentation") | [
Michiel Dral](http://twitter.com/dralletje)
[🤔](#ideas-dralletje "Ideas, Planning, & Feedback") | [
Kye Hohenberger](https://github.com/tkh44)
[🤔](#ideas-tkh44 "Ideas, Planning, & Feedback") | [
Mitchell Hamilton](https://hamil.town)
[💻](/kentcdodds/babel-plugin-macros/commits?author=mitchellhamilton "Code") [⚠️](/kentcdodds/babel-plugin-macros/commits?author=mitchellhamilton "Tests") | [
Justin Hall](https://github.com/wKovacs64)
[📖](/kentcdodds/babel-plugin-macros/commits?author=wKovacs64 "Documentation") | 343 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 344 | | [
Brian Pedersen](https://github.com/PiereDome)
[💻](/kentcdodds/babel-plugin-macros/commits?author=PiereDome "Code") [📖](/kentcdodds/babel-plugin-macros/commits?author=PiereDome "Documentation") | [
Andrew Palm](https://github.com/apalm)
[💻](/kentcdodds/babel-plugin-macros/commits?author=apalm "Code") | [
Michael Hsu](https://michaelhsu.tw/)
[📖](/kentcdodds/babel-plugin-macros/commits?author=evenchange4 "Documentation") [🔌](#plugin-evenchange4 "Plugin/utility libraries") | [
Bo Lingen](https://github.com/citycide)
[💻](/kentcdodds/babel-plugin-macros/commits?author=citycide "Code") | [
Tyler Haas](https://github.com/tylerthehaas)
[📖](/kentcdodds/babel-plugin-macros/commits?author=tylerthehaas "Documentation") | [
FWeinb](https://github.com/FWeinb)
[💻](/kentcdodds/babel-plugin-macros/commits?author=FWeinb "Code") | 345 | 346 | 347 | 348 | This project follows the [all-contributors][all-contributors] specification. 349 | Contributions of any kind welcome! 350 | 351 | ## LICENSE 352 | 353 | MIT 354 | 355 | [npm]: https://www.npmjs.com/ 356 | [node]: https://nodejs.org 357 | [build-badge]: https://img.shields.io/travis/kentcdodds/babel-plugin-macros.svg?style=flat-square 358 | [build]: https://travis-ci.org/kentcdodds/babel-plugin-macros 359 | [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/babel-plugin-macros.svg?style=flat-square 360 | [coverage]: https://codecov.io/github/kentcdodds/babel-plugin-macros 361 | [version-badge]: https://img.shields.io/npm/v/babel-plugin-macros.svg?style=flat-square 362 | [package]: https://www.npmjs.com/package/babel-plugin-macros 363 | [downloads-badge]: https://img.shields.io/npm/dm/babel-plugin-macros.svg?style=flat-square 364 | [npmchart]: http://npmcharts.com/compare/babel-plugin-macros 365 | [license-badge]: https://img.shields.io/npm/l/babel-plugin-macros.svg?style=flat-square 366 | [license]: https://github.com/kentcdodds/babel-plugin-macros/blob/master/LICENSE 367 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 368 | [prs]: http://makeapullrequest.com 369 | [donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square 370 | [donate]: http://kcd.im/donate 371 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 372 | [coc]: https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/CODE_OF_CONDUCT.md 373 | [github-watch-badge]: https://img.shields.io/github/watchers/kentcdodds/babel-plugin-macros.svg?style=social 374 | [github-watch]: https://github.com/kentcdodds/babel-plugin-macros/watchers 375 | [github-star-badge]: https://img.shields.io/github/stars/kentcdodds/babel-plugin-macros.svg?style=social 376 | [github-star]: https://github.com/kentcdodds/babel-plugin-macros/stargazers 377 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20babel-plugin-macros!%20https://github.com/kentcdodds/babel-plugin-macros%20%F0%9F%91%8D 378 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/babel-plugin-macros.svg?style=social 379 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key 380 | [all-contributors]: https://github.com/kentcdodds/all-contributors 381 | [preval]: https://github.com/kentcdodds/babel-plugin-preval 382 | [cra]: https://github.com/facebookincubator/create-react-app 383 | [cra-issue]: https://github.com/facebookincubator/create-react-app/issues/2730 384 | --------------------------------------------------------------------------------