├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── .gitignore ├── .huskyrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── other ├── MAINTAINING.md ├── USERS.md ├── docs │ ├── author.md │ └── user.md ├── manual-releases.md └── mock-modules │ ├── @scope │ └── package │ │ └── macro.js │ ├── babel-plugin-macros-test-error-thrower.macro │ └── index.js │ ├── babel-plugin-macros-test-error-thrower │ └── macro.js │ ├── babel-plugin-macros-test-fake │ └── macro.js │ └── babel-plugin-path-replace │ └── index.js ├── package.json └── src ├── __tests__ ├── __snapshots__ │ ├── create-macros.js.snap │ └── index.js.snap ├── create-macros.js ├── fixtures │ ├── config │ │ ├── babel-plugin-macros.config.js │ │ ├── cjs-code.js │ │ ├── code.js │ │ └── configurable.macro.js │ ├── emotion-esm.macro.js │ ├── emotion.macro.js │ ├── error-thrower.macro.js │ ├── eval-macro.js │ ├── eval.macro.js │ ├── jsx-id-prefix.macro.js │ ├── jsx-id-prefix.plugin.js │ ├── keep-imports.macro.js │ ├── macro-error-thrower.macro.js │ ├── non-wrapped.macro.js │ ├── path-replace-issue │ │ ├── .babelrc │ │ └── variable-assignment.js │ └── primitive-config │ │ ├── babel-plugin-macros.config.js │ │ ├── code.js │ │ └── configurable.macro.js └── index.js └── index.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "babel-plugin-macros", 3 | "projectOwner": "kentcdodds", 4 | "imageSize": 100, 5 | "commit": false, 6 | "contributorsPerLine": 7, 7 | "repoHost": "https://github.com", 8 | "repoType": "github", 9 | "skipCi": false, 10 | "files": [ 11 | "README.md" 12 | ], 13 | "contributors": [ 14 | { 15 | "login": "kentcdodds", 16 | "name": "Kent C. Dodds", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 18 | "profile": "https://kentcdodds.com", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "infra", 23 | "test" 24 | ] 25 | }, 26 | { 27 | "login": "threepointone", 28 | "name": "Sunil Pai", 29 | "avatar_url": "https://avatars1.githubusercontent.com/u/18808?v=3", 30 | "profile": "https://github.com/threepointone", 31 | "contributions": [ 32 | "ideas" 33 | ] 34 | }, 35 | { 36 | "login": "suchipi", 37 | "name": "Lily Scott", 38 | "avatar_url": "https://avatars0.githubusercontent.com/u/1341513?v=4", 39 | "profile": "http://suchipi.com", 40 | "contributions": [ 41 | "question", 42 | "doc" 43 | ] 44 | }, 45 | { 46 | "login": "dralletje", 47 | "name": "Michiel Dral", 48 | "avatar_url": "https://avatars1.githubusercontent.com/u/767261?v=4", 49 | "profile": "http://twitter.com/dralletje", 50 | "contributions": [ 51 | "ideas" 52 | ] 53 | }, 54 | { 55 | "login": "tkh44", 56 | "name": "Kye Hohenberger", 57 | "avatar_url": "https://avatars2.githubusercontent.com/u/662750?v=4", 58 | "profile": "https://github.com/tkh44", 59 | "contributions": [ 60 | "ideas" 61 | ] 62 | }, 63 | { 64 | "login": "mitchellhamilton", 65 | "name": "Mitchell Hamilton", 66 | "avatar_url": "https://avatars1.githubusercontent.com/u/11481355?v=4", 67 | "profile": "https://hamil.town", 68 | "contributions": [ 69 | "code", 70 | "test" 71 | ] 72 | }, 73 | { 74 | "login": "wKovacs64", 75 | "name": "Justin Hall", 76 | "avatar_url": "https://avatars1.githubusercontent.com/u/1288694?v=4", 77 | "profile": "https://github.com/wKovacs64", 78 | "contributions": [ 79 | "doc" 80 | ] 81 | }, 82 | { 83 | "login": "PiereDome", 84 | "name": "Brian Pedersen", 85 | "avatar_url": "https://avatars3.githubusercontent.com/u/1903016?v=4", 86 | "profile": "https://github.com/PiereDome", 87 | "contributions": [ 88 | "code", 89 | "doc" 90 | ] 91 | }, 92 | { 93 | "login": "apalm", 94 | "name": "Andrew Palm", 95 | "avatar_url": "https://avatars3.githubusercontent.com/u/4495237?v=4", 96 | "profile": "https://github.com/apalm", 97 | "contributions": [ 98 | "code" 99 | ] 100 | }, 101 | { 102 | "login": "evenchange4", 103 | "name": "Michael Hsu", 104 | "avatar_url": "https://avatars1.githubusercontent.com/u/1527371?v=4", 105 | "profile": "https://michaelhsu.tw/", 106 | "contributions": [ 107 | "doc", 108 | "plugin" 109 | ] 110 | }, 111 | { 112 | "login": "citycide", 113 | "name": "Bo Lingen", 114 | "avatar_url": "https://avatars2.githubusercontent.com/u/16605186?v=4", 115 | "profile": "https://github.com/citycide", 116 | "contributions": [ 117 | "code" 118 | ] 119 | }, 120 | { 121 | "login": "tylerthehaas", 122 | "name": "Tyler Haas", 123 | "avatar_url": "https://avatars1.githubusercontent.com/u/11150235?v=4", 124 | "profile": "https://github.com/tylerthehaas", 125 | "contributions": [ 126 | "doc" 127 | ] 128 | }, 129 | { 130 | "login": "FWeinb", 131 | "name": "FWeinb", 132 | "avatar_url": "https://avatars0.githubusercontent.com/u/1250430?v=4", 133 | "profile": "https://github.com/FWeinb", 134 | "contributions": [ 135 | "code" 136 | ] 137 | }, 138 | { 139 | "login": "tricoder42", 140 | "name": "Tomáš Ehrlich", 141 | "avatar_url": "https://avatars2.githubusercontent.com/u/827862?v=4", 142 | "profile": "http://www.tomasehrlich.cz", 143 | "contributions": [ 144 | "bug", 145 | "code" 146 | ] 147 | }, 148 | { 149 | "login": "jgierer12", 150 | "name": "Jonas Gierer", 151 | "avatar_url": "https://avatars0.githubusercontent.com/u/4331946?v=4", 152 | "profile": "https://github.com/jgierer12", 153 | "contributions": [ 154 | "doc" 155 | ] 156 | }, 157 | { 158 | "login": "lPadier", 159 | "name": "Loïc Padier", 160 | "avatar_url": "https://avatars2.githubusercontent.com/u/4009640?v=4", 161 | "profile": "http://loicpadier.com", 162 | "contributions": [ 163 | "code" 164 | ] 165 | }, 166 | { 167 | "login": "pshrmn", 168 | "name": "Paul Sherman", 169 | "avatar_url": "https://avatars0.githubusercontent.com/u/1127037?v=4", 170 | "profile": "https://www.pshrmn.com", 171 | "contributions": [ 172 | "code" 173 | ] 174 | }, 175 | { 176 | "login": "conartist6", 177 | "name": "Conrad Buck", 178 | "avatar_url": "https://avatars1.githubusercontent.com/u/540777?v=4", 179 | "profile": "http://burningpotato.com", 180 | "contributions": [ 181 | "code", 182 | "test", 183 | "doc" 184 | ] 185 | }, 186 | { 187 | "login": "InvictusMB", 188 | "name": "InvictusMB", 189 | "avatar_url": "https://avatars3.githubusercontent.com/u/3091209?v=4", 190 | "profile": "https://github.com/InvictusMB", 191 | "contributions": [ 192 | "test" 193 | ] 194 | }, 195 | { 196 | "login": "coderberry", 197 | "name": "Eric Berry", 198 | "avatar_url": "https://avatars2.githubusercontent.com/u/12481?v=4", 199 | "profile": "https://codefund.io", 200 | "contributions": [ 201 | "fundingFinding" 202 | ] 203 | }, 204 | { 205 | "login": "futagoza", 206 | "name": "Futago-za Ryuu", 207 | "avatar_url": "https://avatars1.githubusercontent.com/u/1943570?v=4", 208 | "profile": "http://futagoza.github.io/", 209 | "contributions": [ 210 | "code", 211 | "test" 212 | ] 213 | }, 214 | { 215 | "login": "lucleray", 216 | "name": "Luc", 217 | "avatar_url": "https://avatars3.githubusercontent.com/u/6616955?v=4", 218 | "profile": "https://luc.im", 219 | "contributions": [ 220 | "code" 221 | ] 222 | }, 223 | { 224 | "login": "wintercounter", 225 | "name": "Victor Vincent", 226 | "avatar_url": "https://avatars2.githubusercontent.com/u/963776?v=4", 227 | "profile": "http://wintercounter.me", 228 | "contributions": [ 229 | "code" 230 | ] 231 | }, 232 | { 233 | "login": "mvasilkov", 234 | "name": "я котик пур-пур", 235 | "avatar_url": "https://avatars3.githubusercontent.com/u/140257?v=4", 236 | "profile": "http://mvasilkov.ovh", 237 | "contributions": [ 238 | "doc" 239 | ] 240 | }, 241 | { 242 | "login": "soska", 243 | "name": "Armando Sosa", 244 | "avatar_url": "https://avatars0.githubusercontent.com/u/139577?v=4", 245 | "profile": "http://armandososa.com", 246 | "contributions": [ 247 | "doc" 248 | ] 249 | }, 250 | { 251 | "login": "matvp91", 252 | "name": "Matthias", 253 | "avatar_url": "https://avatars3.githubusercontent.com/u/12699796?v=4", 254 | "profile": "https://github.com/matvp91", 255 | "contributions": [ 256 | "code" 257 | ] 258 | }, 259 | { 260 | "login": "JoviDeCroock", 261 | "name": "Jovi De Croock", 262 | "avatar_url": "https://avatars3.githubusercontent.com/u/17125876?v=4", 263 | "profile": "https://www.jovidecroock.com/", 264 | "contributions": [ 265 | "code", 266 | "test" 267 | ] 268 | }, 269 | { 270 | "login": "VictorArowo", 271 | "name": "Victor Arowo", 272 | "avatar_url": "https://avatars0.githubusercontent.com/u/25545108?v=4", 273 | "profile": "http://victorarowo.com", 274 | "contributions": [ 275 | "doc" 276 | ] 277 | }, 278 | { 279 | "login": "alexanderchan", 280 | "name": "Alex Chan", 281 | "avatar_url": "https://avatars.githubusercontent.com/u/1864372?v=4", 282 | "profile": "https://twitter.com/alexandermchan", 283 | "contributions": [ 284 | "doc" 285 | ] 286 | }, 287 | { 288 | "login": "probablyup", 289 | "name": "Evan Jacobs", 290 | "avatar_url": "https://avatars.githubusercontent.com/u/570070?v=4", 291 | "profile": "https://probablyup.com", 292 | "contributions": [ 293 | "code" 294 | ] 295 | } 296 | ] 297 | } 298 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `babel-plugin-macros` version: 15 | - `node` version: 16 | - `npm` version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | 22 | ``` 23 | 24 | What you did: 25 | 26 | What happened: 27 | 28 | 29 | 30 | Reproduction repository: 31 | 32 | 36 | 37 | Problem description: 38 | 39 | Suggested solution: 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | 31 | **Checklist**: 32 | 33 | 34 | 35 | 36 | - [ ] Documentation 37 | - [ ] Tests 38 | - [ ] Ready to be merged 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - '+([0-9])?(.{+([0-9]),x}).x' 6 | - 'main' 7 | - 'next' 8 | - 'next-major' 9 | - 'beta' 10 | - 'alpha' 11 | - '!all-contributors/**' 12 | pull_request: {} 13 | jobs: 14 | main: 15 | # ignore all-contributors PRs 16 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 17 | strategy: 18 | matrix: 19 | node: [10.13, 12, 14, 15] 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: ⬇️ Checkout repo 23 | uses: actions/checkout@v2 24 | 25 | - name: ⎔ Setup node 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node }} 29 | 30 | - name: 📥 Download deps 31 | uses: bahmutov/npm-install@v1 32 | with: 33 | useLockFile: false 34 | 35 | - name: ▶️ Run validate script 36 | run: npm run validate 37 | 38 | - name: ⬆️ Upload coverage report 39 | uses: codecov/codecov-action@v1 40 | 41 | release: 42 | needs: main 43 | runs-on: ubuntu-latest 44 | if: 45 | ${{ github.repository == 'kentcdodds/babel-plugin-macros' && 46 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 47 | github.ref) && github.event_name == 'push' }} 48 | steps: 49 | - name: ⬇️ Checkout repo 50 | uses: actions/checkout@v2 51 | 52 | - name: ⎔ Setup node 53 | uses: actions/setup-node@v1 54 | with: 55 | node-version: 14 56 | 57 | - name: 📥 Download deps 58 | uses: bahmutov/npm-install@v1 59 | with: 60 | useLockFile: false 61 | 62 | - name: 🚀 Release 63 | uses: cycjimmy/semantic-release-action@v2 64 | with: 65 | semantic_version: 17 66 | branches: | 67 | [ 68 | '+([0-9])?(.{+([0-9]),x}).x', 69 | 'main', 70 | 'next', 71 | 'next-major', 72 | {name: 'beta', prerelease: true}, 73 | {name: 'alpha', prerelease: true} 74 | ] 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 77 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | 6 | # these cause more harm than good 7 | # when working with contributors 8 | package-lock.json 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/husky') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /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_ 6 | series [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 `main` branch pointing at the original repository and make pull 15 | > requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/kentcdodds/babel-plugin-macros 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` branch 25 | > to use the upstream main branch whenever you run `git pull`. Then you can make 26 | > all of your pull request branches based on this `main` branch. Whenever you 27 | > want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Committing and Pushing changes 30 | 31 | Please make sure to run the tests before you commit your changes. You can run 32 | `npm run test:update` which will update any snapshots that need updating. Make 33 | sure to include those changes (if they exist) in your commit. 34 | 35 | ## Help needed 36 | 37 | Please checkout the [the open issues][issues] 38 | 39 | Also, please watch the repo and respond to questions/bug reports/feature 40 | requests! Thanks! 41 | 42 | 43 | [egghead]: https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github 44 | [all-contributors]: https://github.com/all-contributors/all-contributors 45 | [issues]: https://github.com/kentcdodds/babel-plugin-macros/issues 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

babel-plugin-macros 🎣

3 | 4 |

Allows you to build simple compile-time libraries

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]][npmtrends] 14 | [![MIT License][license-badge]][license] 15 | [![All Contributors][all-contributors-badge]](#contributors-) 16 | [![PRs Welcome][prs-badge]][prs] 17 | [![Code of Conduct][coc-badge]][coc] 18 | 19 | 20 | ## The problem 21 | 22 | Check out 23 | [this guest post](https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros) 24 | on the Babel.js blog for a complete write up on the problem, motivation, and 25 | solution. 26 | 27 | Currently, each babel plugin in the babel ecosystem requires that you configure 28 | it individually. This is fine for things like language features, but can be 29 | frustrating overhead for libraries that allow for compile-time code 30 | transformation as an optimization. 31 | 32 | ## This solution 33 | 34 | babel-plugin-macros defines a standard interface for libraries that want to use 35 | compile-time code transformation without requiring the user to add a babel 36 | plugin to their build system (other than `babel-plugin-macros`, which is ideally 37 | already in place). 38 | 39 |
40 | 41 | Expand for more details on the motivation 42 | 43 | For instance, many css-in-js libraries have a css tagged template string 44 | function: 45 | 46 | ```js 47 | const styles = css` 48 | .red { 49 | color: red; 50 | } 51 | ` 52 | ``` 53 | 54 | The function compiles your css into (for example) an object with generated class 55 | names for each of the classes you defined in your css: 56 | 57 | ```js 58 | console.log(styles) // { red: "1f-d34j8rn43y587t" } 59 | ``` 60 | 61 | This class name can be generated at runtime (in the browser), but this has some 62 | disadvantages: 63 | 64 | - There is cpu usage/time overhead; the client needs to run the code to generate 65 | these classes every time the page loads 66 | - There is code bundle size overhead; the client needs to receive a CSS parser 67 | in order to generate these class names, and shipping this makes the amount of 68 | js the client needs to parse larger. 69 | 70 | To help solve those issues, many css-in-js libraries write their own babel 71 | plugin that generates the class names at compile-time instead of runtime: 72 | 73 | ```js 74 | // Before running through babel: 75 | const styles = css` 76 | .red { 77 | color: red; 78 | } 79 | ` 80 | // After running through babel, with the library-specific plugin: 81 | const styles = {red: '1f-d34j8rn43y587t'} 82 | ``` 83 | 84 | If the css-in-js library supported babel-plugin-macros instead, then they 85 | wouldn't need their own babel plugin to compile these out; they could instead 86 | rely on babel-plugin-macros to do it for them. So if a user already had 87 | `babel-plugin-macros` installed and configured with babel, then they wouldn't 88 | need to change their babel configuration to get the compile-time benefits of the 89 | library. This would be most useful if the boilerplate they were using came with 90 | `babel-plugin-macros` out of the box, which is true for 91 | [`create-react-app`][cra]. 92 | 93 | Although css-in-js is the most common example, there are lots of other things 94 | you could use `babel-plugin-macros` for, like: 95 | 96 | - Compiling GraphQL fragments into objects so that the client doesn't need a 97 | GraphQL parser 98 | - Eval-ing out code at compile time that will be baked into the runtime code, 99 | for instance to get a list of directories in the filesystem (see 100 | [preval][preval]) 101 | 102 |
103 | 104 | ## Table of Contents 105 | 106 | 107 | 108 | 109 | - [Installation](#installation) 110 | - [Usage](#usage) 111 | - [User docs](#user-docs) 112 | - [Author docs](#author-docs) 113 | - [Caveats](#caveats) 114 | - [FAQ](#faq) 115 | - [How do I find available macros?](#how-do-i-find-available-macros) 116 | - [What's the difference between babel plugins and macros?](#whats-the-difference-between-babel-plugins-and-macros) 117 | - [In what order are macros executed?](#in-what-order-are-macros-executed) 118 | - [Does it work with function calls only?](#does-it-work-with-function-calls-only) 119 | - [How about implicit optimizations at compile time?](#how-about-implicit-optimizations-at-compile-time) 120 | - [Inspiration](#inspiration) 121 | - [Other Solutions](#other-solutions) 122 | - [Issues](#issues) 123 | - [🐛 Bugs](#-bugs) 124 | - [💡 Feature Requests](#-feature-requests) 125 | - [Contributors ✨](#contributors-) 126 | - [LICENSE](#license) 127 | 128 | 129 | 130 | ## Installation 131 | 132 | This module is distributed via [npm][npm] which is bundled with [node][node] and 133 | should be installed as one of your project's `devDependencies`: 134 | 135 | ``` 136 | npm install --save-dev babel-plugin-macros 137 | ``` 138 | 139 | ## Usage 140 | 141 | > You may like to watch 142 | > [this YouTube video](https://www.youtube.com/watch?v=1queadQ0048&list=PLV5CVI1eNcJgCrPH_e6d57KRUTiDZgs0u) 143 | > to get an idea of what macros is and how it can be used. 144 | 145 | ### User docs 146 | 147 | Are you trying to use `babel-plugin-macros`? Go to 148 | [`other/docs/user.md`](other/docs/user.md). 149 | 150 | ### Author docs 151 | 152 | Are you trying to make your own macros that works with `babel-plugin-macros`? Go 153 | to [`other/docs/author.md`](other/docs/author.md). (you should probably read the 154 | user docs too). 155 | 156 | ### Caveats 157 | 158 | #### Babel cache problem 159 | 160 | > **Note:** This issue is not present when used in Create React App. 161 | 162 | Most of the time you'll probably be using this with the babel cache enabled in 163 | webpack to rebuild faster. If your macro function is **not pure** which gets 164 | different output with same code (e.g., IO side effects) it will cause recompile 165 | mechanism fail. Unfortunately you'll also experience this problem while 166 | developing your macro as well. If there's not a change to the source code that's 167 | being transpiled, then babel will use the cache rather than running your macro 168 | again. 169 | 170 | For now, to force recompile the code you can simply add a cache busting comment 171 | in the file: 172 | 173 | ```diff 174 | import macro from 'non-pure.macro'; 175 | 176 | -// Do some changes of your code or 177 | +// add a cache busting comment to force recompile. 178 | macro('parameters'); 179 | ``` 180 | 181 | This problem is still being worked on and is not unique to 182 | `babel-plugin-macros`. For more details and workarounds, please check related 183 | issues below: 184 | 185 | - babel-plugin-preval: 186 | [How to force recompile? #19](https://github.com/kentcdodds/babel-plugin-preval/issues/19) 187 | - graphql.macro: 188 | [Recompile problem (babel cache) #6](https://github.com/evenchange4/graphql.macro/issues/6) 189 | - twin.macro: 190 | [Can't change taliwind config #37](https://github.com/ben-rogerson/twin.macro/discussions/37) 191 | 192 | ## FAQ 193 | 194 | ### How do I find available macros? 195 | 196 | You can write your own without publishing them to `npm`, but if you'd like to 197 | see existing macros you can add to your project, then take a look at the 198 | [Awesome babel macros](https://github.com/jgierer12/awesome-babel-macros) 199 | repository. 200 | 201 | Please add any you don't see listed! 202 | 203 | ### What's the difference between babel plugins and macros? 204 | 205 | Let's use 206 | [`babel-plugin-console`](https://www.npmjs.com/package/babel-plugin-console) as 207 | an example. 208 | 209 | If we used `babel-plugin-console`, it would look like this: 210 | 211 | 1. Add `babel-plugin-console` to `.babelrc` 212 | 2. Use it in a code: 213 | 214 | ```js 215 | function add100(a) { 216 | const oneHundred = 100 217 | console.scope('Add 100 to another number') 218 | return add(a, oneHundred) 219 | } 220 | 221 | function add(a, b) { 222 | return a + b 223 | } 224 | ``` 225 | 226 | When that code is run, the `scope` function does some pretty nifty things: 227 | 228 | **Browser:** 229 | 230 | ![Browser console scoping add100](https://github.com/mattphillips/babel-plugin-console/raw/53536cba919d5be49d4f66d957769c07ca7a4207/assets/add100-chrome.gif) 231 | 232 | **Node:** 233 | 234 | Node console scoping add100 235 | 236 | Instead, let's use the macro it's shipped with like this: 237 | 238 | 1. Add `babel-plugin-macros` to `.babelrc` (only once for all macros) 239 | 2. Use it in a code: 240 | 241 | ```js 242 | import scope from 'babel-plugin-console/scope.macro' 243 | function add100(a) { 244 | const oneHundred = 100 245 | scope('Add 100 to another number') 246 | return add(a, oneHundred) 247 | } 248 | 249 | function add(a, b) { 250 | return a + b 251 | } 252 | ``` 253 | 254 | The result is exactly the same, but this approach has a few advantages: 255 | 256 | **Advantages:** 257 | 258 | - requires only one entry in `.babelrc` for all macros used in project. Add that 259 | once and you can use all the macros you want 260 | - toolkits (like [create-react-app][cra]) may already support 261 | `babel-plugin-macros`, so no configuration is needed at all 262 | - it's explicit. With `console.scope` people may be fooled that it's just a 263 | normal `console` API when there's really a babel transpilation going on. When 264 | you import `scope`, it's obvious that it's macro and does something with the 265 | code at compile time. Some ESLint rules may also have issues with plugins that 266 | look for "global" variables 267 | - macros are safer and easier to write, because they receive exactly the AST 268 | node to process 269 | - If you misconfigure `babel-plugin-console` you wont find out until you run the 270 | code. If you misconfigure `babel-plugin-macros` you'll get a compile-time 271 | error. 272 | 273 | **Drawbacks:** 274 | 275 | - Cannot (should not) be used for implicit transpilations (like syntax plugins) 276 | - Explicitness is more verbose. Which some people might consider a drawback... 277 | 278 | ### In what order are macros executed? 279 | 280 | This is another advantage of `babel-plugin-macros` over regular plugins. The 281 | user of the macro is in control of the ordering! The order of execution is the 282 | same order as imported. The order of execution is clear, explicit and in full 283 | control of the user: 284 | 285 | ```js 286 | import preval from 'preval.macro' 287 | import idx from 'idx.macro' 288 | 289 | // preval macro is evaluated first, then idx 290 | ``` 291 | 292 | This differs from the current situation with babel plugins where it's 293 | prohibitively difficult to control the order plugins run in a particular file. 294 | 295 | ### Does it work with function calls only? 296 | 297 | No! Any AST node type is supported. 298 | 299 | It can be tagged template literal: 300 | 301 | ```js 302 | import eval from 'eval.macro' 303 | const val = eval`7 * 6` 304 | ``` 305 | 306 | A function: 307 | 308 | ```js 309 | import eval from 'eval.macro' 310 | const val = eval('7 * 6') 311 | ``` 312 | 313 | JSX Element: 314 | 315 | ```js 316 | import Eval from 'eval.macro' 317 | const val = 7 * 6 318 | ``` 319 | 320 | Really, anything... 321 | 322 | See the [testing snapshot](src/__tests__/__snapshots__/index.js.snap) for more 323 | examples. 324 | 325 | ### How about implicit optimizations at compile time? 326 | 327 | All examples above were _explicit_ - a macro was imported and then evaluated 328 | with a specific AST node. 329 | 330 | Completely different story are _implicit_ babel plugins, like 331 | [transform-react-constant-elements](https://babeljs.io/docs/plugins/transform-react-constant-elements/), 332 | which process whole AST tree. 333 | 334 | Explicit is often a better pattern than implicit because it requires others to 335 | understand how things are globally configured. This is in this spirit are 336 | `babel-plugin-macros` designed. However, some things _do_ need to be implicit, 337 | and those kinds of babel plugins can't be turned into macros. 338 | 339 | ## Inspiration 340 | 341 | - [threepointone/babel-plugin-macros](https://github.com/threepointone/babel-plugin-macros) 342 | - [facebookincubator/create-react-app#2730][cra-issue] 343 | 344 | Thank you to [@phpnode](https://github.com/phpnode) for donating the npm package 345 | `babel-plugin-macros`. 346 | 347 | ## Other Solutions 348 | 349 | - [sweetjs](http://sweetjs.org/) 350 | 351 | ## Issues 352 | 353 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 354 | label._ 355 | 356 | ### 🐛 Bugs 357 | 358 | Please file an issue for bugs, missing documentation, or unexpected behavior. 359 | 360 | [**See Bugs**][bugs] 361 | 362 | ### 💡 Feature Requests 363 | 364 | Please file an issue to suggest new features. Vote on feature requests by adding 365 | a 👍. This helps maintainers prioritize what to work on. 366 | 367 | [**See Feature Requests**][requests] 368 | 369 | ## Contributors ✨ 370 | 371 | Thanks goes to these people ([emoji key][emojis]): 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 |

Kent C. Dodds

💻 📖 🚇 ⚠️

Sunil Pai

🤔

Lily Scott

💬 📖

Michiel Dral

🤔

Kye Hohenberger

🤔

Mitchell Hamilton

💻 ⚠️

Justin Hall

📖

Brian Pedersen

💻 📖

Andrew Palm

💻

Michael Hsu

📖 🔌

Bo Lingen

💻

Tyler Haas

📖

FWeinb

💻

Tomáš Ehrlich

🐛 💻

Jonas Gierer

📖

Loïc Padier

💻

Paul Sherman

💻

Conrad Buck

💻 ⚠️ 📖

InvictusMB

⚠️

Eric Berry

🔍

Futago-za Ryuu

💻 ⚠️

Luc

💻

Victor Vincent

💻

я котик пур-пур

📖

Armando Sosa

📖

Matthias

💻

Jovi De Croock

💻 ⚠️

Victor Arowo

📖

Alex Chan

📖

Evan Jacobs

💻
418 | 419 | 420 | 421 | 422 | 423 | 424 | This project follows the [all-contributors][all-contributors] specification. 425 | Contributions of any kind welcome! 426 | 427 | ## LICENSE 428 | 429 | MIT 430 | 431 | 432 | [npm]: https://www.npmjs.com 433 | [node]: https://nodejs.org 434 | [build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/babel-plugin-macros/validate?logo=github&style=flat-square 435 | [build]: https://github.com/kentcdodds/babel-plugin-macros/actions?query=workflow%3Avalidate 436 | [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/babel-plugin-macros.svg?style=flat-square 437 | [coverage]: https://codecov.io/github/kentcdodds/babel-plugin-macros 438 | [version-badge]: https://img.shields.io/npm/v/babel-plugin-macros.svg?style=flat-square 439 | [package]: https://www.npmjs.com/package/babel-plugin-macros 440 | [downloads-badge]: https://img.shields.io/npm/dm/babel-plugin-macros.svg?style=flat-square 441 | [npmtrends]: http://www.npmtrends.com/babel-plugin-macros 442 | [license-badge]: https://img.shields.io/npm/l/babel-plugin-macros.svg?style=flat-square 443 | [license]: https://github.com/kentcdodds/babel-plugin-macros/blob/main/LICENSE 444 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 445 | [prs]: http://makeapullrequest.com 446 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 447 | [coc]: https://github.com/kentcdodds/babel-plugin-macros/blob/main/CODE_OF_CONDUCT.md 448 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key 449 | [all-contributors]: https://github.com/all-contributors/all-contributors 450 | [all-contributors-badge]: https://img.shields.io/github/all-contributors/kentcdodds/babel-plugin-macros?color=orange&style=flat-square 451 | [bugs]: https://github.com/kentcdodds/babel-plugin-macros/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug 452 | [requests]: https://github.com/kentcdodds/babel-plugin-macros/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement 453 | [good-first-issue]: https://github.com/kentcdodds/babel-plugin-macros/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 454 | [preval]: https://github.com/kentcdodds/babel-plugin-preval 455 | [cra]: https://github.com/facebook/create-react-app 456 | [cra-issue]: https://github.com/facebook/create-react-app/issues/2730 457 | 458 | -------------------------------------------------------------------------------- /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 8 | conduct are 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 13 | learn to solve their own problems. We have an issue template and hopefully most 14 | folks follow it. If it's not clear what the issue is, invite them to create a 15 | minimal reproduction of what they're trying to accomplish or the bug they think 16 | they've found. 17 | 18 | Once it's determined that a code change is necessary, point people to 19 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a 20 | pull request. If they're the one who needs the feature, they're the one who can 21 | build it. If they need some hand holding and you have time to lend a hand, 22 | please do so. It's an investment into another human being, and an investment 23 | into a potential maintainer. 24 | 25 | Remember that this is open source, so the code is not yours, it's ours. If 26 | someone needs a change in the codebase, you don't have to make it happen 27 | yourself. Commit as much time to the project as you want/need to. Nobody can ask 28 | any more of you than that. 29 | 30 | ## Pull Requests 31 | 32 | As a maintainer, you're fine to make your branches on the main repo or on your 33 | own fork. Either way is fine. 34 | 35 | When we receive a pull request, a github action is kicked off automatically (see 36 | the `.github/workflows/validate.yml` for what runs in the action). We avoid 37 | merging anything that breaks the validate action. 38 | 39 | Please review PRs and focus on the code rather than the individual. You never 40 | know when this is someone's first ever PR and we want their experience to be as 41 | positive as possible, so be uplifting and constructive. 42 | 43 | When you merge the pull request, 99% of the time you should use the 44 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) 45 | feature. This keeps our git history clean, but more importantly, this allows us 46 | to make any necessary changes to the commit message so we release what we want 47 | to release. See the next section on Releases for more about that. 48 | 49 | ## Release 50 | 51 | Our releases are automatic. They happen whenever code lands into `main`. A 52 | github action gets kicked off and if it's successful, a tool called 53 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is 54 | used to automatically publish a new release to npm as well as a changelog to 55 | GitHub. It is only able to determine the version and whether a release is 56 | necessary by the git commit messages. With this in mind, **please brush up on 57 | [the commit message convention][commit] which drives our releases.** 58 | 59 | > One important note about this: Please make sure that commit messages do NOT 60 | > contain the words "BREAKING CHANGE" in them unless we want to push a major 61 | > version. I've been burned by this more than once where someone will include 62 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not 63 | > a huge deal honestly, but kind of annoying... 64 | 65 | ## Thanks! 66 | 67 | Thank you so much for helping to maintain this project! 68 | 69 | [commit]: 70 | https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 71 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | If you or your company uses this project, add your name to this list! Eventually 4 | we may have a website to showcase these (wanna build it!?) 5 | 6 | > No users have been added yet! 7 | 8 | 13 | -------------------------------------------------------------------------------- /other/docs/author.md: -------------------------------------------------------------------------------- 1 | # `babel-plugin-macros` Usage for macros authors 2 | 3 | > See also: 4 | > [the `user` docs](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md). 5 | 6 | Is this your first time working with ASTs? Here are some resources: 7 | 8 | - [Writing custom Babel and ESLint plugins with ASTs](https://youtu.be/VBscbcm2Mok?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf): 9 | A 53 minute talk by [@kentcdodds](https://twitter.com/kentcdodds) 10 | - [babel-handbook](https://github.com/thejameskyle/babel-handbook): A guided 11 | handbook on how to use Babel and how to create plugins for Babel by 12 | [@thejameskyle](https://twitter.com/thejameskyle) 13 | - [Code Transformation and Linting](https://kentcdodds.com/workshops/#code-transformation-and-linting): 14 | A workshop (recording available on Frontend Masters) with exercises of making 15 | custom Babel and ESLint plugins 16 | 17 | ## Writing a macro 18 | 19 | > You might appreciate 20 | > [this example repo](https://github.com/kentcdodds/cra-macro-example) which 21 | > shows how to write and use macros in a create-react-app application. 22 | 23 | A macro is a JavaScript module that exports a function. Here's a simple example: 24 | 25 | ```javascript 26 | const {createMacro} = require('babel-plugin-macros') 27 | 28 | // `createMacro` is simply a function that ensures your macro is only 29 | // called in the context of a babel transpilation and will throw an 30 | // error with a helpful message if someone does not have babel-plugin-macros 31 | // configured correctly 32 | module.exports = createMacro(myMacro) 33 | 34 | function myMacro({references, state, babel}) { 35 | // state is the second argument you're passed to a visitor in a 36 | // normal babel plugin. `babel` is the `babel-plugin-macros` module. 37 | // do whatever you like to the AST paths you find in `references` 38 | // read more below... 39 | } 40 | ``` 41 | 42 | It can be published to the npm registry (for generic macros, like a css-in-js 43 | library) or used locally (for domain-specific macros, like handling some special 44 | case for your company's localization efforts). 45 | 46 | > Before you write a custom macro, you might consider whether 47 | > [`babel-plugin-preval`][preval] help you do what you want as it's pretty 48 | > powerful. 49 | 50 | There are two parts to the `babel-plugin-macros` API: 51 | 52 | 1. The filename convention 53 | 2. The function you export 54 | 55 | ### Filename 56 | 57 | The way that `babel-plugin-macros` determines whether to run a macro is based on 58 | the source string of the `import` or `require` statement. It must match this 59 | regex: `/[./]macro(\.c?js)?$/` for example: 60 | 61 | _matches_: 62 | 63 | ``` 64 | 'my.macro' 65 | 'my.macro.js' 66 | 'my.macro.cjs' 67 | 'my/macro' 68 | 'my/macro.js' 69 | 'my/macro.cjs' 70 | ``` 71 | 72 | _does not match_: 73 | 74 | ``` 75 | 'my-macro' 76 | 'my.macro.is-sweet' 77 | 'my/macro/rocks' 78 | ``` 79 | 80 | > So long as your file can be required at a matching path, you're good. So you 81 | > could put it in: `my/macro/index.js` and people would: `require('my/macro')` 82 | > which would work fine. 83 | 84 | **If you're going to publish this to npm,** the most ergonomic thing would be to 85 | name it something that ends in `.macro`. If it's part of a larger package, then 86 | calling the file `macro.js` or placing it in `macro/index.js` is a great way to 87 | go as well. Then people could do: 88 | 89 | ```js 90 | import Nice from 'nice.macro' 91 | // or 92 | import Sweet from 'sweet/macro' 93 | ``` 94 | 95 | In addition, please publish your macro with the [`keyword`][keyword] of 96 | `babel-plugin-macros` (note the "s"). That way folks can easily find macros by 97 | searching for the [`babel-plugin-macros` keyword on 98 | npm][npm-babel-plugin-macros]. In addition, and you can add this badge to the 99 | top of your README: 100 | 101 | [![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) 102 | 103 | ``` 104 | [![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) 105 | ``` 106 | 107 | ### Function API 108 | 109 | The macro you create should export a function. That function accepts a single 110 | parameter which is an object with the following properties: 111 | 112 | #### state 113 | 114 | The state of the file being traversed. It's the second argument you receive in a 115 | visitor function in a normal babel plugin. 116 | 117 | #### babel 118 | 119 | This is the same thing you get as an argument to normal babel plugins. It is 120 | also the same thing you get if you `require('babel-core')`. 121 | 122 | #### references 123 | 124 | This is an object that contains arrays of all the references to things imported 125 | from macro keyed based on the name of the import. The items in each array are 126 | the paths to the references. 127 | 128 |
129 | 130 | Some examples: 131 | 132 | ```javascript 133 | import MyMacro from './my.macro' 134 | 135 | MyMacro( 136 | {someOption: true}, 137 | ` 138 | some stuff 139 | `, 140 | ) 141 | 142 | // references: { default: [BabelPath] } 143 | ``` 144 | 145 | ```javascript 146 | import {foo as FooMacro} from './my.macro' 147 | 148 | FooMacro( 149 | {someOption: true}, 150 | ` 151 | some stuff 152 | `, 153 | ) 154 | 155 | // references: { foo: [BabelPath] } 156 | ``` 157 | 158 | ```javascript 159 | import {foo as FooMacro} from './my.macro' 160 | 161 | // no usage... 162 | 163 | // references: {} 164 | ``` 165 | 166 |
167 | 168 | From here, it's just a matter of doing stuff with the `BabelPath`s that 169 | you're given. For that check out [the babel handbook][babel-handbook]. 170 | 171 | > One other thing to note is that after your macro has run, babel-plugin-macros 172 | > will remove the import/require statement for you. 173 | 174 | #### source 175 | 176 | This is a string used as import declaration's source - i.e. `'./my.macro'`. 177 | 178 | #### config 179 | 180 | There is a feature that allows users to configure your macro. 181 | 182 | To specify that your plugin is configurable, you pass a `configName` to 183 | `createMacro`. 184 | 185 | A configuration is created from data combined from two sources: We use 186 | [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which 187 | can be located in any of the following files up the directories from the 188 | importing file: 189 | 190 | - `.babel-plugin-macrosrc` 191 | - `.babel-plugin-macrosrc.json` 192 | - `.babel-plugin-macrosrc.yaml` 193 | - `.babel-plugin-macrosrc.yml` 194 | - `.babel-plugin-macrosrc.js` 195 | - `babel-plugin-macros.config.js` 196 | - `babelMacros` in `package.json` 197 | 198 | The content of the config will be merged with the content of the babel macros 199 | plugin options. Config options take priority. 200 | 201 | All together specifying and using the config might look like this: 202 | 203 | ```javascript 204 | // .babel-plugin-macros.config.js 205 | module.exports = { 206 | taggedTranslations: {locale: 'en_US'}, 207 | } 208 | 209 | // .babel.config.js 210 | module.exports = { 211 | plugins: [ 212 | [ 213 | "macros", 214 | { 215 | taggedTranslations: { locale: "en_GB" }, 216 | }, 217 | ], 218 | ], 219 | } 220 | 221 | 222 | // taggedTranslations.macro.js 223 | const {createMacro} = require('babel-plugin-macros') 224 | module.exports = createMacro(taggedTranslationsMacro, { 225 | configName: 'taggedTranslations', 226 | }) 227 | function taggedTranslationsMacro({references, state, babel, config}) { 228 | const {locale = 'en'} = config 229 | } 230 | ``` 231 | 232 | Note that in the above example if both files were specified, the final locale 233 | value would be `en_US`, since that is the value in the plugin config file. 234 | 235 | ### Keeping imports 236 | 237 | As said before, `babel-plugin-macros` automatically removes an import statement 238 | of macro. If you want to keep it because you have other plugins processing 239 | macros, return `{ keepImports: true }` from your macro: 240 | 241 | ```javascript 242 | const {createMacro} = require('babel-plugin-macros') 243 | 244 | module.exports = createMacro(taggedTranslationsMacro) 245 | 246 | function taggedTranslationsMacro({references, state, babel}) { 247 | // process node from references 248 | 249 | return { 250 | keepImports: true, 251 | } 252 | } 253 | ``` 254 | 255 | ## Throwing Helpful Errors 256 | 257 | Debugging stuff that transpiles your code is the worst, especially for 258 | beginners. That's why it's important that you make assertions, and catch errors 259 | to throw more meaningful errors with helpful information for the developer to 260 | know what to do to resolve the issue. 261 | 262 | In an effort to make this easier for you, `babel-plugin-macros` will wrap the 263 | invocation of your plugin in a `try/catch` and throw as helpful an error message 264 | as possible for you. 265 | 266 | To make it even better, you can throw your own with more context. For example: 267 | 268 | ```javascript 269 | const {createMacro, MacroError} = require('babel-plugin-macros') 270 | 271 | module.exports = createMacro(myMacro) 272 | 273 | function myMacro({references, state, babel}) { 274 | // something unexpected happens: 275 | throw new MacroError( 276 | 'Some helpful and contextual message. Learn more: ' + 277 | 'https://github.com/your-org/your-repo/blob/master/docs/errors.md#learn-more-about-eror-title', 278 | ) 279 | } 280 | ``` 281 | 282 | ## Testing your macro 283 | 284 | The best way to test your macro is using [`babel-plugin-tester`][tester]: 285 | 286 | ```javascript 287 | import pluginTester from 'babel-plugin-tester' 288 | import plugin from 'babel-plugin-macros' 289 | 290 | pluginTester({ 291 | plugin, 292 | snapshot: true, 293 | babelOptions: {filename: __filename}, 294 | tests: [ 295 | ` 296 | import MyMacro from '../my.macro' 297 | 298 | MyMacro({someOption: true}, \` 299 | some stuff 300 | \`) 301 | `, 302 | ], 303 | }) 304 | ``` 305 | 306 | There is currently no way to get code coverage for your macro this way however. 307 | If you want code coverage, you'll have to call your macro yourself. 308 | Contributions to improve this experience are definitely welcome! 309 | 310 | ## Async logic 311 | 312 | Unfortunately, babel plugins are synchronous so you can't do anything 313 | asynchronous with `babel-plugin-macros`. However, you can cheat a bit by running 314 | `child_process`'s `spawnSync` to synchronously execute a file. It's definitely a 315 | hack and is not great for performance, but in most cases it's fast enough™️. 316 | 317 | Luckily, [@Zemnmez](https://github.com/Zemnmez) created 318 | [`do-sync`](https://github.com/Zemnmez/do-sync) which makes doing this much more 319 | straightforward: 320 | 321 | ```javascript 322 | const {doSync} = require('do-sync') 323 | const {createMacro, MacroError} = require('babel-plugin-macros') 324 | 325 | module.exports = createMacro(myMacro) 326 | 327 | const getTheFlowers = doSync(async (arg1, arg2) => { 328 | const dep = require('some-dependency') 329 | const flowers = await dep(arg1, arg2.stuff) 330 | return flowers 331 | }) 332 | 333 | function myMacro({references, state, babel}) { 334 | const flowers = getTheFlowers('...', {stuff: '...'}) 335 | // ... more sync stuff 336 | } 337 | ``` 338 | 339 | [preval]: https://github.com/kentcdodds/babel-plugin-preval 340 | [babel-handbook]: 341 | https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md 342 | [tester]: https://github.com/babel-utils/babel-plugin-tester 343 | [keyword]: https://docs.npmjs.com/files/package.json#keywords 344 | [npm-babel-plugin-macros]: 345 | https://www.npmjs.com/browse/keyword/babel-plugin-macros 346 | [cosmiconfig]: https://www.npmjs.com/package/cosmiconfig 347 | -------------------------------------------------------------------------------- /other/docs/user.md: -------------------------------------------------------------------------------- 1 | # `babel-plugin-macros` Usage for users 2 | 3 | > See also: 4 | > [the `author` docs](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md). 5 | 6 | ## Adding the plugin to your config 7 | 8 | ### Via `.babelrc` (Recommended) 9 | 10 | **.babelrc** 11 | 12 | ```json 13 | { 14 | "plugins": ["macros"] 15 | } 16 | ``` 17 | 18 | ### Via [`babel.config.js`](https://babeljs.io/docs/en/configuration#babelconfigjs) 19 | 20 | **babel.config.js** 21 | 22 | ```javascript 23 | module.exports = function (api) { 24 | return { 25 | plugins: ['macros'], 26 | } 27 | } 28 | ``` 29 | 30 | ### Via CLI 31 | 32 | ```shell 33 | babel --plugins babel-plugin-macros script.js 34 | ``` 35 | 36 | ### Via Node API 37 | 38 | ```js 39 | require('babel-core').transform('code', { 40 | plugins: ['macros'], 41 | }) 42 | ``` 43 | 44 | ## Using a macro 45 | 46 | With the `babel-plugin-macros` plugin added to your config, we can now use a 47 | macro that works with the `babel-plugin-macros` API. Let's assume we have such a 48 | module in our project called `eval.macro.js`. To use it, we `import` or 49 | `require` the macro module in our code like so: 50 | 51 | ```javascript 52 | import MyEval from './eval.macro' 53 | // or 54 | const MyEval = require('./eval.macro') 55 | ``` 56 | 57 | Then we use that variable however the documentation for the macro says. 58 | Incidentally, `eval.macro.js` actually exists in the tests for 59 | `babel-plugin-macros` [here][eval-macro] and you can see how it transforms our 60 | code in [the `babel-plugin-macros` snapshots][eval-snapshots]. 61 | 62 | > Note here that the real benefit is that we don't need to configure anything 63 | > for every macro you add. We simply configure `babel-plugin-macros`, then we 64 | > can use any macro available. This is part of the benefit of using 65 | > `babel-plugin-macros`. 66 | 67 | [eval-macro]: 68 | https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/fixtures/eval.macro.js 69 | [eval-snapshots]: 70 | https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/__snapshots__/index.js.snap 71 | 72 | ### Using with create-react-app 73 | 74 | > [Checkout the CRA Macro Example repo](https://github.com/kentcdodds/cra-macro-example) 75 | 76 | `babel-plugin-macros` ships with `react-scripts` 2.0! This is awesome because it 77 | allows for babel to be configured in a nice way without having to eject from 78 | `create-react-app`! 79 | 80 | Before deciding to use this however you should be aware of a few things: 81 | 82 | 1. Features may be broken or not work as expected 83 | 2. Documentation for new features is still sparse, so look through the pull 84 | requests for how they're expected to work 85 | 86 | With that being said you can use all the awesomeness of `babel-plugin-macros` 87 | inside `create-react-app` by running one of the following commands based on your 88 | situation. 89 | 90 | ``` 91 | $ # Create a new application 92 | $ npx create-react-app my-app 93 | $ # Upgrade an existing application 94 | $ yarn upgrade react-scripts 95 | ``` 96 | 97 | ### config 98 | 99 | There is a feature that allows you to configure your macro. We use 100 | [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which 101 | can be located in any of the following files up the directories from the 102 | importing file: 103 | 104 | - `.babel-plugin-macrosrc` 105 | - `.babel-plugin-macrosrc.json` 106 | - `.babel-plugin-macrosrc.yaml` 107 | - `.babel-plugin-macrosrc.yml` 108 | - `.babel-plugin-macrosrc.js` 109 | - `babel-plugin-macros.config.js` 110 | - `babelMacros` in `package.json` 111 | 112 | You need to specify your `configName`. EG: For configuring [styled-components 113 | macro][styled-components], the `configName` is `"styledComponents"`: 114 | 115 | ```js 116 | // babel-plugin-macros.config.js 117 | module.exports = { 118 | // ... 119 | // Other macros config 120 | styledComponents: { 121 | pure: true, 122 | }, 123 | } 124 | ``` 125 | 126 | [cosmiconfig]: https://www.npmjs.com/package/cosmiconfig 127 | [styled-components]: https://www.styled-components.com/docs/tooling#babel-macro 128 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when 4 | there are useful changes in the code that justify a release. But sometimes 5 | things get messed up one way or another and we need to trigger the release 6 | ourselves. When this happens, simply bump the number below and commit that with 7 | the following commit message based on your needs: 8 | 9 | **Major** 10 | 11 | ``` 12 | fix(release): manually release a major version 13 | 14 | There was an issue with a major release, so this manual-releases.md 15 | change is to release a new major version. 16 | 17 | Reference: # 18 | 19 | BREAKING CHANGE: 20 | ``` 21 | 22 | **Minor** 23 | 24 | ``` 25 | feat(release): manually release a minor version 26 | 27 | There was an issue with a minor release, so this manual-releases.md 28 | change is to release a new minor version. 29 | 30 | Reference: # 31 | ``` 32 | 33 | **Patch** 34 | 35 | ``` 36 | fix(release): manually release a patch version 37 | 38 | There was an issue with a patch release, so this manual-releases.md 39 | change is to release a new patch version. 40 | 41 | Reference: # 42 | ``` 43 | 44 | The number of times we've had to do a manual release is: 0 45 | -------------------------------------------------------------------------------- /other/mock-modules/@scope/package/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 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /other/mock-modules/babel-plugin-path-replace/index.js: -------------------------------------------------------------------------------- 1 | const types = require('@babel/types') 2 | 3 | const problematicVisitor = { 4 | VariableDeclarator: { 5 | enter(path) { 6 | const initPath = path.get('init') 7 | 8 | initPath.replaceWith( 9 | types.sequenceExpression([ 10 | types.stringLiteral('foobar'), 11 | initPath.node, 12 | ]), 13 | ) 14 | }, 15 | }, 16 | } 17 | 18 | module.exports = () => ({ 19 | visitor: { 20 | Program: { 21 | enter(path) { 22 | path.traverse(problematicVisitor) 23 | }, 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-macros", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Allows you to build compile-time libraries", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "kcd-scripts lint", 8 | "setup": "npm install && npm run validate -s", 9 | "test": "kcd-scripts test", 10 | "test:update": "npm test -- --updateSnapshot --coverage", 11 | "validate": "kcd-scripts validate" 12 | }, 13 | "files": [ 14 | "src/index.js" 15 | ], 16 | "keywords": [ 17 | "babel-plugin", 18 | "macros", 19 | "macro", 20 | "babel-macro", 21 | "babel-plugin-macro", 22 | "babel-macros", 23 | "babel-plugin-macros" 24 | ], 25 | "author": "Kent C. Dodds (https://kentcdodds.com)", 26 | "license": "MIT", 27 | "dependencies": { 28 | "cosmiconfig": "^7.0.0", 29 | "resolve": "^1.19.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.12.9", 33 | "@babel/parser": "^7.12.7", 34 | "@babel/plugin-transform-modules-commonjs": "^7.16.7", 35 | "@babel/types": "^7.12.7", 36 | "ast-pretty-print": "^2.0.1", 37 | "babel-plugin-tester": "^10.0.0", 38 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 39 | "cpy": "^8.1.1", 40 | "kcd-scripts": "^7.1.0" 41 | }, 42 | "eslintConfig": { 43 | "extends": "./node_modules/kcd-scripts/eslint.js" 44 | }, 45 | "eslintIgnore": [ 46 | "node_modules", 47 | "coverage", 48 | "dist" 49 | ], 50 | "babel": { 51 | "plugins": [ 52 | "@babel/transform-modules-commonjs" 53 | ] 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/kentcdodds/babel-plugin-macros" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/kentcdodds/babel-plugin-macros/issues" 61 | }, 62 | "homepage": "https://github.com/kentcdodds/babel-plugin-macros#readme", 63 | "engines": { 64 | "node": ">=10", 65 | "npm": ">=6" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`unknown plugin Macros are applied in the order respecting plugins order: Macros are applied in the order respecting plugins order 1`] = ` 4 | 5 | import Wrap from "./fixtures/jsx-id-prefix.macro"; 6 | 7 | const bar = Wrap(

); 8 | 9 | ↓ ↓ ↓ ↓ ↓ ↓ 10 | 11 | const bar = Wrap( 12 |
13 |

14 |
, 15 | ) 16 | 17 | 18 | `; 19 | 20 | exports[`unknown plugin Supports named imports: Supports named imports 1`] = ` 21 | 22 | import {css as CSS, styled as STYLED} from './fixtures/emotion.macro' 23 | const red = CSS\` 24 | background-color: red; 25 | \` 26 | 27 | const Div = STYLED.div\` 28 | composes: \${red} 29 | color: blue; 30 | \` 31 | 32 | ↓ ↓ ↓ ↓ ↓ ↓ 33 | 34 | const red = 'background-color: red;' 35 | const Div = STYLED.div\`composes: background-color: red; 36 | color: blue;\` 37 | 38 | 39 | `; 40 | 41 | exports[`unknown plugin Works as a JSXElement: Works as a JSXElement 1`] = ` 42 | 43 | import MyEval from './fixtures/eval.macro' 44 | const x = 34 + 45 45 | 46 | ↓ ↓ ↓ ↓ ↓ ↓ 47 | 48 | const x = 79 49 | 50 | 51 | `; 52 | 53 | exports[`unknown plugin 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`] = ` 54 | 55 | import errorThrower from 'babel-plugin-macros-test-error-thrower/macro' 56 | errorThrower('hi') 57 | 58 | ↓ ↓ ↓ ↓ ↓ ↓ 59 | 60 | Error: babel-plugin-macros-test-error-thrower/macro: not helpful Learn more: https://www.npmjs.com/package/babel-plugin-macros-test-error-thrower 61 | 62 | `; 63 | 64 | exports[`unknown plugin appends the npm URL for errors thrown by node modules: appends the npm URL for errors thrown by node modules 1`] = ` 65 | 66 | import errorThrower from 'babel-plugin-macros-test-error-thrower.macro' 67 | errorThrower('hi') 68 | 69 | ↓ ↓ ↓ ↓ ↓ ↓ 70 | 71 | Error: babel-plugin-macros-test-error-thrower.macro: not helpful Learn more: https://www.npmjs.com/package/babel-plugin-macros-test-error-thrower.macro 72 | 73 | `; 74 | 75 | exports[`unknown plugin does nothing but remove macros if it is unused: does nothing but remove macros if it is unused 1`] = ` 76 | 77 | import foo from "./fixtures/eval.macro"; 78 | 79 | const bar = 42; 80 | 81 | ↓ ↓ ↓ ↓ ↓ ↓ 82 | 83 | const bar = 42 84 | 85 | 86 | `; 87 | 88 | exports[`unknown plugin forwards MacroErrors thrown by the macro: forwards MacroErrors thrown by the macro 1`] = ` 89 | 90 | import errorThrower from './fixtures/macro-error-thrower.macro' 91 | errorThrower('hey') 92 | 93 | ↓ ↓ ↓ ↓ ↓ ↓ 94 | 95 | MacroError: very helpful 96 | 97 | `; 98 | 99 | exports[`unknown plugin macros can set their configName and get their config: macros can set their configName and get their config 1`] = ` 100 | 101 | import configured from './configurable.macro' 102 | 103 | // eslint-disable-next-line babel/no-unused-expressions 104 | configured\`stuff\` 105 | 106 | ↓ ↓ ↓ ↓ ↓ ↓ 107 | 108 | // eslint-disable-next-line babel/no-unused-expressions 109 | configured\`stuff\` 110 | 111 | 112 | `; 113 | 114 | exports[`unknown plugin optionally keep imports (import declaration): optionally keep imports (import declaration) 1`] = ` 115 | 116 | import macro from './fixtures/keep-imports.macro' 117 | const red = macro('noop'); 118 | 119 | ↓ ↓ ↓ ↓ ↓ ↓ 120 | 121 | import macro from './fixtures/keep-imports.macro' 122 | const red = macro('noop') 123 | 124 | 125 | `; 126 | 127 | exports[`unknown plugin optionally keep imports (variable assignment): optionally keep imports (variable assignment) 1`] = ` 128 | 129 | const macro = require('./fixtures/keep-imports.macro') 130 | const red = macro('noop'); 131 | 132 | ↓ ↓ ↓ ↓ ↓ ↓ 133 | 134 | const macro = require('./fixtures/keep-imports.macro') 135 | 136 | const red = macro('noop') 137 | 138 | 139 | `; 140 | 141 | exports[`unknown plugin optionally keep imports in combination with babel-preset-env (#80): optionally keep imports in combination with babel-preset-env (#80) 1`] = ` 142 | 143 | import macro from './fixtures/keep-imports.macro' 144 | const red = macro('noop') 145 | 146 | ↓ ↓ ↓ ↓ ↓ ↓ 147 | 148 | 'use strict' 149 | 150 | var _keepImports = require('./fixtures/keep-imports.macro') 151 | 152 | var _keepImports2 = _interopRequireDefault(_keepImports) 153 | 154 | function _interopRequireDefault(obj) { 155 | return obj && obj.__esModule ? obj : {default: obj} 156 | } 157 | 158 | const red = (0, _keepImports2.default)('noop') 159 | 160 | 161 | `; 162 | 163 | exports[`unknown plugin prepends the relative path for errors thrown by the macro: prepends the relative path for errors thrown by the macro 1`] = ` 164 | 165 | import errorThrower from './fixtures/error-thrower.macro' 166 | errorThrower('hey') 167 | 168 | ↓ ↓ ↓ ↓ ↓ ↓ 169 | 170 | Error: ./fixtures/error-thrower.macro: very unhelpful 171 | 172 | `; 173 | 174 | exports[`unknown plugin raises an error if macro does not exist: raises an error if macro does not exist 1`] = ` 175 | 176 | import foo from './some-macros-that-doesnt-even-need-to-exist.macro' 177 | export default 'something else' 178 | 179 | ↓ ↓ ↓ ↓ ↓ ↓ 180 | 181 | Error: Cannot find module './some-macros-that-doesnt-even-need-to-exist.macro' from '/src/__tests__' 182 | 183 | `; 184 | 185 | exports[`unknown plugin supports compiled macros (\`__esModule\` + \`export default\`): supports compiled macros (\`__esModule\` + \`export default\`) 1`] = ` 186 | 187 | import {css, styled} from './fixtures/emotion-esm.macro' 188 | const red = css\` 189 | background-color: red; 190 | \` 191 | 192 | const Div = styled.div\` 193 | composes: \${red} 194 | color: blue; 195 | \` 196 | 197 | ↓ ↓ ↓ ↓ ↓ ↓ 198 | 199 | const red = css\` 200 | background-color: red; 201 | \` 202 | const Div = styled.div\` 203 | composes: \${red} 204 | color: blue; 205 | \` 206 | 207 | 208 | `; 209 | 210 | exports[`unknown plugin supports macros from node_modules with scope: supports macros from node_modules with scope 1`] = ` 211 | 212 | import fakeMacro from '@scope/package/macro' 213 | fakeMacro('hi') 214 | 215 | ↓ ↓ ↓ ↓ ↓ ↓ 216 | 217 | fakeMacro('hi') 218 | 219 | 220 | `; 221 | 222 | exports[`unknown plugin supports macros from node_modules: supports macros from node_modules 1`] = ` 223 | 224 | import fakeMacro from 'babel-plugin-macros-test-fake/macro' 225 | fakeMacro('hi') 226 | 227 | ↓ ↓ ↓ ↓ ↓ ↓ 228 | 229 | fakeMacro('hi') 230 | 231 | 232 | `; 233 | 234 | exports[`unknown plugin throws an error if the macro is not properly wrapped: throws an error if the macro is not properly wrapped 1`] = ` 235 | 236 | import unwrapped from './fixtures/non-wrapped.macro' 237 | unwrapped('hey') 238 | 239 | ↓ ↓ ↓ ↓ ↓ ↓ 240 | 241 | 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 242 | 243 | `; 244 | 245 | exports[`unknown plugin when a custom isMacrosName option is used on a import: when a custom isMacrosName option is used on a import 1`] = ` 246 | 247 | import myEval from './fixtures/eval-macro.js' 248 | const x = myEval\`34 + 45\` 249 | 250 | ↓ ↓ ↓ ↓ ↓ ↓ 251 | 252 | const x = 79 253 | 254 | 255 | `; 256 | 257 | exports[`unknown plugin when a custom isMacrosName option is used on a require: when a custom isMacrosName option is used on a require 1`] = ` 258 | 259 | const evaler = require('./fixtures/eval-macro.js') 260 | const x = evaler\`34 + 45\` 261 | 262 | ↓ ↓ ↓ ↓ ↓ ↓ 263 | 264 | const x = 79 265 | 266 | 267 | `; 268 | 269 | exports[`unknown plugin when a plugin that replaces paths is used, macros still work properly: when a plugin that replaces paths is used, macros still work properly 1`] = ` 270 | 271 | import myEval from '../eval.macro' 272 | 273 | const result = myEval\`+('4' + '2')\` 274 | 275 | global.result = result 276 | 277 | ↓ ↓ ↓ ↓ ↓ ↓ 278 | 279 | const result = ('foobar', 42) 280 | global.result = result 281 | 282 | 283 | `; 284 | 285 | exports[`unknown plugin when configuration is specified in plugin options: when configuration is specified in plugin options 1`] = ` 286 | 287 | import configured from './configurable.macro' 288 | 289 | // eslint-disable-next-line babel/no-unused-expressions 290 | configured\`stuff\` 291 | 292 | ↓ ↓ ↓ ↓ ↓ ↓ 293 | 294 | // eslint-disable-next-line babel/no-unused-expressions 295 | configured\`stuff\` 296 | 297 | 298 | `; 299 | 300 | exports[`unknown plugin when configuration is specified in plugin options: when configuration is specified in plugin options 2`] = ` 301 | 302 | const configured = require('./configurable.macro') 303 | 304 | // eslint-disable-next-line babel/no-unused-expressions 305 | configured\`stuff\` 306 | 307 | ↓ ↓ ↓ ↓ ↓ ↓ 308 | 309 | // eslint-disable-next-line babel/no-unused-expressions 310 | configured\`stuff\` 311 | 312 | 313 | `; 314 | 315 | exports[`unknown plugin when configuration is specified incorrectly in plugin options: when configuration is specified incorrectly in plugin options 1`] = ` 316 | 317 | import configured from './configurable.macro' 318 | 319 | // eslint-disable-next-line babel/no-unused-expressions 320 | configured\`stuff\` 321 | 322 | ↓ ↓ ↓ ↓ ↓ ↓ 323 | 324 | // eslint-disable-next-line babel/no-unused-expressions 325 | configured\`stuff\` 326 | 327 | 328 | `; 329 | 330 | exports[`unknown plugin when plugin options configuration cannot be merged with file configuration: when plugin options configuration cannot be merged with file configuration 1`] = ` 331 | 332 | import configured from './configurable.macro' 333 | 334 | // eslint-disable-next-line babel/no-unused-expressions 335 | configured\`stuff\` 336 | 337 | ↓ ↓ ↓ ↓ ↓ ↓ 338 | 339 | Error: /src/__tests__/fixtures/primitive-config/babel-plugin-macros.config.js specified a configurableMacro config of type object, but the the macros plugin's options.configurableMacro did contain an object. Both configs must contain objects for their options to be mergeable. 340 | 341 | `; 342 | 343 | exports[`unknown plugin when there is an error reading the config, a helpful message is logged 1`] = ` 344 | Array [ 345 | 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., 346 | ] 347 | `; 348 | 349 | exports[`unknown plugin 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`] = ` 350 | 351 | import configured from './configurable.macro' 352 | 353 | // eslint-disable-next-line babel/no-unused-expressions 354 | configured\`stuff\` 355 | 356 | ↓ ↓ ↓ ↓ ↓ ↓ 357 | 358 | Error: this is a cosmiconfig error 359 | 360 | `; 361 | 362 | exports[`unknown plugin 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`] = ` 363 | 364 | import configured from './configurable.macro' 365 | 366 | // eslint-disable-next-line babel/no-unused-expressions 367 | configured\`stuff\` 368 | 369 | ↓ ↓ ↓ ↓ ↓ ↓ 370 | 371 | // eslint-disable-next-line babel/no-unused-expressions 372 | configured\`stuff\` 373 | 374 | 375 | `; 376 | 377 | exports[`unknown plugin works with function calls: works with function calls 1`] = ` 378 | 379 | import myEval from './fixtures/eval.macro' 380 | const x = myEval('34 + 45') 381 | 382 | ↓ ↓ ↓ ↓ ↓ ↓ 383 | 384 | const x = 79 385 | 386 | 387 | `; 388 | 389 | exports[`unknown plugin works with import: works with import 1`] = ` 390 | 391 | import myEval from './fixtures/eval.macro' 392 | const x = myEval\`34 + 45\` 393 | 394 | ↓ ↓ ↓ ↓ ↓ ↓ 395 | 396 | const x = 79 397 | 398 | 399 | `; 400 | 401 | exports[`unknown plugin works with require destructuring and aliasing: works with require destructuring and aliasing 1`] = ` 402 | 403 | const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro') 404 | const red = CSS\` 405 | background-color: red; 406 | \` 407 | 408 | const Div = STYLED.div\` 409 | composes: \${red} 410 | color: blue; 411 | \` 412 | 413 | ↓ ↓ ↓ ↓ ↓ ↓ 414 | 415 | const red = 'background-color: red;' 416 | const Div = STYLED.div\`composes: background-color: red; 417 | color: blue;\` 418 | 419 | 420 | `; 421 | 422 | exports[`unknown plugin works with require destructuring: works with require destructuring 1`] = ` 423 | 424 | const {css, styled} = require('./fixtures/emotion.macro') 425 | const red = css\` 426 | background-color: red; 427 | \` 428 | 429 | const Div = styled.div\` 430 | composes: \${red} 431 | color: blue; 432 | \` 433 | 434 | ↓ ↓ ↓ ↓ ↓ ↓ 435 | 436 | const red = 'background-color: red;' 437 | const Div = styled.div\`composes: background-color: red; 438 | color: blue;\` 439 | 440 | 441 | `; 442 | 443 | exports[`unknown plugin works with require: works with require 1`] = ` 444 | 445 | const evaler = require('./fixtures/eval.macro') 446 | const x = evaler\`34 + 45\` 447 | 448 | ↓ ↓ ↓ ↓ ↓ ↓ 449 | 450 | const x = 79 451 | 452 | 453 | `; 454 | -------------------------------------------------------------------------------- /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__/fixtures/config/babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configurableMacro: { 3 | fileConfig: true, 4 | someConfig: true, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/config/cjs-code.js: -------------------------------------------------------------------------------- 1 | const configured = require('./configurable.macro') 2 | 3 | // eslint-disable-next-line babel/no-unused-expressions 4 | configured`stuff` 5 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/config/code.js: -------------------------------------------------------------------------------- 1 | import configured from './configurable.macro' 2 | 3 | // eslint-disable-next-line babel/no-unused-expressions 4 | configured`stuff` 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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(cssRef.parentPath.get('quasi').evaluate().value.trim()), 13 | ) 14 | } 15 | }) 16 | references.styled.forEach(styledRef => { 17 | if (styledRef.parentPath.parentPath.type === 'TaggedTemplateExpression') { 18 | const quasi = styledRef.parentPath.parentPath.get('quasi') 19 | const val = quasi.evaluate().value.trim() 20 | const replacement = t.templateLiteral( 21 | [t.templateElement({raw: val, cooked: val})], 22 | [], 23 | ) 24 | quasi.replaceWith(replacement) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/eval-macro.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./eval.macro') 2 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/eval.macro.js: -------------------------------------------------------------------------------- 1 | const {parse} = require('@babel/parser') 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 = parse(`var x = ${JSON.stringify(object)}`) 53 | return fileNode.program.body[0].declarations[0].init 54 | } 55 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/jsx-id-prefix.macro.js: -------------------------------------------------------------------------------- 1 | // adds "prefix-" to each `id` attribute 2 | const {createMacro} = require('../../') 3 | 4 | module.exports = createMacro(wrapWidget) 5 | 6 | function wrapWidget({references, babel}) { 7 | const {types: t} = babel 8 | references.default.forEach(wrap => { 9 | wrap.parentPath.traverse({ 10 | JSXAttribute(path) { 11 | const name = path.get('name') 12 | if (t.isJSXIdentifier(name) && name.node.name === 'id') { 13 | const value = path.get('value') 14 | if (t.isStringLiteral(value)) 15 | value.replaceWith(t.stringLiteral(`macro-${value.node.value}`)) 16 | } 17 | }, 18 | }) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/jsx-id-prefix.plugin.js: -------------------------------------------------------------------------------- 1 | // babel-plugin adding `plugin-` prefix to each "id" JSX attribute 2 | module.exports = main 3 | 4 | function main({types: t}) { 5 | return { 6 | visitor: { 7 | // intentionally traversing from Program, 8 | // if it matches JSXAttribute here the issue won't be reproduced 9 | Program(progPath) { 10 | progPath.traverse({ 11 | JSXAttribute(path) { 12 | const name = path.get('name') 13 | if (t.isJSXIdentifier(name) && name.node.name === 'id') { 14 | const value = path.get('value') 15 | if (t.isStringLiteral(value)) 16 | value.replaceWith(t.stringLiteral(`plugin-${value.node.value}`)) 17 | } 18 | }, 19 | }) 20 | }, 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/keep-imports.macro.js: -------------------------------------------------------------------------------- 1 | const {createMacro} = require('../../') 2 | 3 | module.exports = createMacro(keepImportMacro) 4 | 5 | function keepImportMacro() { 6 | return {keepImports: true} 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/non-wrapped.macro.js: -------------------------------------------------------------------------------- 1 | module.exports = () => {} 2 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/path-replace-issue/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["path-replace"] 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/path-replace-issue/variable-assignment.js: -------------------------------------------------------------------------------- 1 | import myEval from '../eval.macro' 2 | 3 | const result = myEval`+('4' + '2')` 4 | 5 | global.result = result 6 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/primitive-config/babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configurableMacro: 4, 3 | } 4 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/primitive-config/code.js: -------------------------------------------------------------------------------- 1 | import configured from './configurable.macro' 2 | 3 | // eslint-disable-next-line babel/no-unused-expressions 4 | configured`stuff` 5 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/primitive-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 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {cosmiconfigSync as cosmiconfigSyncMock} from 'cosmiconfig' 3 | import cpy from 'cpy' 4 | import babel from '@babel/core' 5 | import pluginTester from 'babel-plugin-tester' 6 | import plugin from '../' 7 | 8 | const projectRoot = path.join(__dirname, '../../') 9 | 10 | jest.mock('cosmiconfig', () => { 11 | const cosmiconfigExports = jest.requireActual('cosmiconfig') 12 | const actualCosmiconfigSync = cosmiconfigExports.cosmiconfigSync 13 | function fakeCosmiconfigSync(...args) { 14 | fakeCosmiconfigSync.explorer = actualCosmiconfigSync(...args) 15 | return fakeCosmiconfigSync.explorer 16 | } 17 | return {...cosmiconfigExports, cosmiconfigSync: fakeCosmiconfigSync} 18 | }) 19 | 20 | beforeAll(() => { 21 | // copy our mock modules to the node_modules directory 22 | // so we can test how things work when importing a macro 23 | // from the node_modules directory. 24 | return cpy(['**/*.js'], path.join('..', '..', 'node_modules'), { 25 | parents: true, 26 | cwd: path.join(projectRoot, 'other', 'mock-modules'), 27 | }) 28 | }) 29 | 30 | beforeEach(() => { 31 | jest.spyOn(console, 'error').mockImplementation(() => {}) 32 | }) 33 | 34 | afterEach(() => { 35 | console.error.mockRestore() 36 | jest.clearAllMocks() 37 | }) 38 | 39 | expect.addSnapshotSerializer({ 40 | print(val) { 41 | return ( 42 | val 43 | .split(projectRoot) 44 | .join('/') 45 | .replace(/\\/g, '/') 46 | // Remove the path of file which thrown an error 47 | .replace(/Error:[^:]*:/, 'Error:') 48 | ) 49 | }, 50 | test(val) { 51 | return typeof val === 'string' 52 | }, 53 | }) 54 | 55 | pluginTester({ 56 | plugin, 57 | snapshot: true, 58 | babelOptions: { 59 | filename: __filename, 60 | parserOpts: { 61 | plugins: ['jsx'], 62 | }, 63 | generatorOpts: {quotes: 'double'}, 64 | }, 65 | tests: [ 66 | { 67 | title: 'does nothing to code that does not import macro', 68 | snapshot: false, 69 | code: ` 70 | import foo from './some-file-without-macro' 71 | 72 | const bar = require('./some-other-file-without-macro') 73 | `, 74 | }, 75 | { 76 | title: 'does nothing but remove macros if it is unused', 77 | snapshot: true, 78 | code: ` 79 | import foo from "./fixtures/eval.macro"; 80 | 81 | const bar = 42; 82 | `, 83 | }, 84 | { 85 | title: 'raises an error if macro does not exist', 86 | error: true, 87 | code: ` 88 | import foo from './some-macros-that-doesnt-even-need-to-exist.macro' 89 | export default 'something else' 90 | `, 91 | }, 92 | { 93 | title: 'works with import', 94 | code: ` 95 | import myEval from './fixtures/eval.macro' 96 | const x = myEval\`34 + 45\` 97 | `, 98 | }, 99 | { 100 | title: 'works with require', 101 | code: ` 102 | const evaler = require('./fixtures/eval.macro') 103 | const x = evaler\`34 + 45\` 104 | `, 105 | }, 106 | { 107 | title: 'works with require destructuring', 108 | code: ` 109 | const {css, styled} = require('./fixtures/emotion.macro') 110 | const red = css\` 111 | background-color: red; 112 | \` 113 | 114 | const Div = styled.div\` 115 | composes: \${red} 116 | color: blue; 117 | \` 118 | `, 119 | }, 120 | { 121 | title: 'works with require destructuring and aliasing', 122 | code: ` 123 | const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro') 124 | const red = CSS\` 125 | background-color: red; 126 | \` 127 | 128 | const Div = STYLED.div\` 129 | composes: \${red} 130 | color: blue; 131 | \` 132 | `, 133 | }, 134 | { 135 | title: 'works with function calls', 136 | code: ` 137 | import myEval from './fixtures/eval.macro' 138 | const x = myEval('34 + 45') 139 | `, 140 | }, 141 | { 142 | title: 'Works as a JSXElement', 143 | code: ` 144 | import MyEval from './fixtures/eval.macro' 145 | const x = 34 + 45 146 | `, 147 | }, 148 | { 149 | title: 'Supports named imports', 150 | code: ` 151 | import {css as CSS, styled as STYLED} from './fixtures/emotion.macro' 152 | const red = CSS\` 153 | background-color: red; 154 | \` 155 | 156 | const Div = STYLED.div\` 157 | composes: \${red} 158 | color: blue; 159 | \` 160 | `, 161 | }, 162 | { 163 | title: 'supports compiled macros (`__esModule` + `export default`)', 164 | code: ` 165 | import {css, styled} from './fixtures/emotion-esm.macro' 166 | const red = css\` 167 | background-color: red; 168 | \` 169 | 170 | const Div = styled.div\` 171 | composes: \${red} 172 | color: blue; 173 | \` 174 | `, 175 | }, 176 | { 177 | title: 'supports macros from node_modules', 178 | code: ` 179 | import fakeMacro from 'babel-plugin-macros-test-fake/macro' 180 | fakeMacro('hi') 181 | `, 182 | teardown() { 183 | try { 184 | // kinda abusing the babel-plugin-tester API here 185 | // to make an extra assertion 186 | // eslint-disable-next-line 187 | const fakeMacro = require('babel-plugin-macros-test-fake/macro') 188 | expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1) 189 | expect(fakeMacro.innerFn).toHaveBeenCalledWith({ 190 | references: expect.any(Object), 191 | source: expect.stringContaining( 192 | 'babel-plugin-macros-test-fake/macro', 193 | ), 194 | state: expect.any(Object), 195 | babel: expect.any(Object), 196 | isBabelMacrosCall: true, 197 | }) 198 | expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel) 199 | } catch (e) { 200 | console.error(e) 201 | throw e 202 | } 203 | }, 204 | }, 205 | { 206 | title: 'supports macros from node_modules with scope', 207 | code: ` 208 | import fakeMacro from '@scope/package/macro' 209 | fakeMacro('hi') 210 | `, 211 | teardown() { 212 | try { 213 | // kinda abusing the babel-plugin-tester API here 214 | // to make an extra assertion 215 | // eslint-disable-next-line 216 | const fakeMacro = require('@scope/package/macro') 217 | expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1) 218 | expect(fakeMacro.innerFn).toHaveBeenCalledWith({ 219 | references: expect.any(Object), 220 | source: expect.stringContaining('@scope/package/macro'), 221 | state: expect.any(Object), 222 | babel: expect.any(Object), 223 | isBabelMacrosCall: true, 224 | }) 225 | expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel) 226 | } catch (e) { 227 | console.error(e) 228 | throw e 229 | } 230 | }, 231 | }, 232 | { 233 | title: 'optionally keep imports (variable assignment)', 234 | code: ` 235 | const macro = require('./fixtures/keep-imports.macro') 236 | const red = macro('noop'); 237 | `, 238 | }, 239 | { 240 | title: 'optionally keep imports (import declaration)', 241 | code: ` 242 | import macro from './fixtures/keep-imports.macro' 243 | const red = macro('noop'); 244 | `, 245 | }, 246 | { 247 | title: 248 | 'optionally keep imports in combination with babel-preset-env (#80)', 249 | code: ` 250 | import macro from './fixtures/keep-imports.macro' 251 | const red = macro('noop') 252 | `, 253 | babelOptions: { 254 | plugins: [ 255 | require.resolve('babel-plugin-transform-es2015-modules-commonjs'), 256 | ], 257 | }, 258 | }, 259 | { 260 | title: 'throws an error if the macro is not properly wrapped', 261 | error: true, 262 | code: ` 263 | import unwrapped from './fixtures/non-wrapped.macro' 264 | unwrapped('hey') 265 | `, 266 | }, 267 | { 268 | title: 'forwards MacroErrors thrown by the macro', 269 | error: true, 270 | code: ` 271 | import errorThrower from './fixtures/macro-error-thrower.macro' 272 | errorThrower('hey') 273 | `, 274 | }, 275 | { 276 | title: 'prepends the relative path for errors thrown by the macro', 277 | error: true, 278 | code: ` 279 | import errorThrower from './fixtures/error-thrower.macro' 280 | errorThrower('hey') 281 | `, 282 | }, 283 | { 284 | title: 'appends the npm URL for errors thrown by node modules', 285 | error: true, 286 | code: ` 287 | import errorThrower from 'babel-plugin-macros-test-error-thrower.macro' 288 | errorThrower('hi') 289 | `, 290 | }, 291 | { 292 | title: 293 | 'appends the npm URL for errors thrown by node modules with a slash', 294 | error: true, 295 | code: ` 296 | import errorThrower from 'babel-plugin-macros-test-error-thrower/macro' 297 | errorThrower('hi') 298 | `, 299 | }, 300 | { 301 | title: 'macros can set their configName and get their config', 302 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 303 | teardown() { 304 | try { 305 | const babelMacrosConfig = require('./fixtures/config/babel-plugin-macros.config') 306 | const configurableMacro = require('./fixtures/config/configurable.macro') 307 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 308 | expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual( 309 | babelMacrosConfig[configurableMacro.configName], 310 | ) 311 | 312 | configurableMacro.realMacro.mockClear() 313 | } catch (e) { 314 | console.error(e) 315 | throw e 316 | } 317 | }, 318 | }, 319 | { 320 | title: 321 | 'when there is an error reading the config, a helpful message is logged', 322 | error: true, 323 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 324 | setup() { 325 | jest 326 | .spyOn(cosmiconfigSyncMock.explorer, 'search') 327 | .mockImplementationOnce(() => { 328 | throw new Error('this is a cosmiconfig error') 329 | }) 330 | jest.spyOn(console, 'error').mockImplementationOnce(() => {}) 331 | return function teardown() { 332 | try { 333 | expect(console.error).toHaveBeenCalledTimes(1) 334 | expect(console.error.mock.calls[0]).toMatchSnapshot() 335 | console.error.mockClear() 336 | } catch (e) { 337 | console.error(e) 338 | console.error.mockClear() 339 | throw e 340 | } 341 | } 342 | }, 343 | }, 344 | { 345 | title: 'when there is no config to load, then no config is passed', 346 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 347 | setup() { 348 | jest 349 | .spyOn(cosmiconfigSyncMock.explorer, 'search') 350 | .mockImplementationOnce(() => { 351 | return null 352 | }) 353 | return function teardown() { 354 | try { 355 | const configurableMacro = require('./fixtures/config/configurable.macro') 356 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 357 | expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual( 358 | {}, 359 | ) 360 | configurableMacro.realMacro.mockClear() 361 | } catch (e) { 362 | console.error(e) 363 | throw e 364 | } 365 | } 366 | }, 367 | }, 368 | { 369 | title: 'when configuration is specified in plugin options', 370 | pluginOptions: { 371 | configurableMacro: { 372 | someConfig: false, 373 | somePluginConfig: true, 374 | }, 375 | }, 376 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 377 | teardown() { 378 | try { 379 | const configurableMacro = require('./fixtures/config/configurable.macro') 380 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 381 | expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({ 382 | fileConfig: true, 383 | someConfig: true, 384 | somePluginConfig: true, 385 | }) 386 | configurableMacro.realMacro.mockClear() 387 | } catch (e) { 388 | console.error(e) 389 | throw e 390 | } 391 | }, 392 | }, 393 | { 394 | title: 'when configuration is specified in plugin options', 395 | pluginOptions: { 396 | configurableMacro: { 397 | someConfig: false, 398 | somePluginConfig: true, 399 | }, 400 | }, 401 | fixture: path.join(__dirname, 'fixtures/config/cjs-code.js'), 402 | teardown() { 403 | try { 404 | const configurableMacro = require('./fixtures/config/configurable.macro') 405 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 406 | expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({ 407 | fileConfig: true, 408 | someConfig: true, 409 | somePluginConfig: true, 410 | }) 411 | configurableMacro.realMacro.mockClear() 412 | } catch (e) { 413 | console.error(e) 414 | throw e 415 | } 416 | }, 417 | }, 418 | { 419 | title: 'when configuration is specified incorrectly in plugin options', 420 | fixture: path.join(__dirname, 'fixtures/config/code.js'), 421 | pluginOptions: { 422 | configurableMacro: 2, 423 | }, 424 | teardown() { 425 | try { 426 | const configurableMacro = require('./fixtures/config/configurable.macro') 427 | expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) 428 | expect(configurableMacro.realMacro).not.toHaveBeenCalledWith( 429 | expect.objectContaining({ 430 | config: expect.any, 431 | }), 432 | ) 433 | configurableMacro.realMacro.mockClear() 434 | } catch (e) { 435 | console.error(e) 436 | throw e 437 | } 438 | }, 439 | }, 440 | { 441 | title: 'when a custom isMacrosName option is used on a import', 442 | pluginOptions: { 443 | isMacrosName(v) { 444 | return v.endsWith('-macro.js') 445 | }, 446 | }, 447 | code: ` 448 | import myEval from './fixtures/eval-macro.js' 449 | const x = myEval\`34 + 45\` 450 | `, 451 | }, 452 | { 453 | title: 'when a custom isMacrosName option is used on a require', 454 | pluginOptions: { 455 | isMacrosName(v) { 456 | return v.endsWith('-macro.js') 457 | }, 458 | }, 459 | code: ` 460 | const evaler = require('./fixtures/eval-macro.js') 461 | const x = evaler\`34 + 45\` 462 | `, 463 | }, 464 | { 465 | title: 466 | 'when plugin options configuration cannot be merged with file configuration', 467 | error: true, 468 | fixture: path.join(__dirname, 'fixtures/primitive-config/code.js'), 469 | pluginOptions: { 470 | configurableMacro: {}, 471 | }, 472 | }, 473 | { 474 | title: 475 | 'when a plugin that replaces paths is used, macros still work properly', 476 | fixture: path.join( 477 | __dirname, 478 | 'fixtures/path-replace-issue/variable-assignment.js', 479 | ), 480 | babelOptions: { 481 | babelrc: true, 482 | }, 483 | }, 484 | { 485 | title: 'Macros are applied in the order respecting plugins order', 486 | code: ` 487 | import Wrap from "./fixtures/jsx-id-prefix.macro"; 488 | 489 | const bar = Wrap(

); 490 | `, 491 | babelOptions: { 492 | presets: [{plugins: [require('./fixtures/jsx-id-prefix.plugin')]}], 493 | }, 494 | }, 495 | ], 496 | }) 497 | 498 | /* eslint no-console:0 */ 499 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const p = require('path') 2 | const resolve = require('resolve') 3 | // const printAST = require('ast-pretty-print') 4 | 5 | const macrosRegex = /[./]macro(\.c?js)?$/ 6 | const testMacrosRegex = v => macrosRegex.test(v) 7 | 8 | // https://stackoverflow.com/a/32749533/971592 9 | class MacroError extends Error { 10 | constructor(message) { 11 | super(message) 12 | this.name = 'MacroError' 13 | /* istanbul ignore else */ 14 | if (typeof Error.captureStackTrace === 'function') { 15 | Error.captureStackTrace(this, this.constructor) 16 | } else if (!this.stack) { 17 | this.stack = new Error(message).stack 18 | } 19 | } 20 | } 21 | 22 | let _configExplorer = null 23 | function getConfigExplorer() { 24 | return (_configExplorer = 25 | _configExplorer || 26 | // Lazy load cosmiconfig since it is a relatively large bundle 27 | require('cosmiconfig').cosmiconfigSync('babel-plugin-macros', { 28 | searchPlaces: [ 29 | 'package.json', 30 | '.babel-plugin-macrosrc', 31 | '.babel-plugin-macrosrc.json', 32 | '.babel-plugin-macrosrc.yaml', 33 | '.babel-plugin-macrosrc.yml', 34 | '.babel-plugin-macrosrc.js', 35 | 'babel-plugin-macros.config.js', 36 | ], 37 | packageProp: 'babelMacros', 38 | })) 39 | } 40 | 41 | function createMacro(macro, options = {}) { 42 | if (options.configName === 'options') { 43 | throw new Error( 44 | `You cannot use the configName "options". It is reserved for babel-plugin-macros.`, 45 | ) 46 | } 47 | macroWrapper.isBabelMacro = true 48 | macroWrapper.options = options 49 | return macroWrapper 50 | 51 | function macroWrapper(args) { 52 | const {source, isBabelMacrosCall} = args 53 | if (!isBabelMacrosCall) { 54 | throw new MacroError( 55 | `The macro you imported from "${source}" is being executed outside the context of compilation with babel-plugin-macros. ` + 56 | `This indicates that you don't have the babel plugin "babel-plugin-macros" configured correctly. ` + 57 | `Please see the documentation for how to configure babel-plugin-macros properly: ` + 58 | 'https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md', 59 | ) 60 | } 61 | return macro(args) 62 | } 63 | } 64 | 65 | function nodeResolvePath(source, basedir) { 66 | return resolve.sync(source, { 67 | basedir, 68 | extensions: ['.js', '.ts', '.tsx', '.mjs', '.cjs', '.jsx'], 69 | // This is here to support the package being globally installed 70 | // read more: https://github.com/kentcdodds/babel-plugin-macros/pull/138 71 | paths: [p.resolve(__dirname, '../../')], 72 | }) 73 | } 74 | 75 | function macrosPlugin( 76 | babel, 77 | // istanbul doesn't like the default of an object for the plugin options 78 | // but I think older versions of babel didn't always pass options 79 | // istanbul ignore next 80 | { 81 | require: _require = require, 82 | resolvePath = nodeResolvePath, 83 | isMacrosName = testMacrosRegex, 84 | ...options 85 | } = {}, 86 | ) { 87 | function interopRequire(path) { 88 | // eslint-disable-next-line import/no-dynamic-require 89 | const o = _require(path) 90 | return o && o.__esModule && o.default ? o.default : o 91 | } 92 | 93 | return { 94 | name: 'macros', 95 | visitor: { 96 | Program(progPath, state) { 97 | progPath.traverse({ 98 | ImportDeclaration(path) { 99 | const isMacros = looksLike(path, { 100 | node: { 101 | source: { 102 | value: v => isMacrosName(v), 103 | }, 104 | }, 105 | }) 106 | if (!isMacros) { 107 | return 108 | } 109 | const imports = path.node.specifiers.map(s => ({ 110 | localName: s.local.name, 111 | importedName: 112 | s.type === 'ImportDefaultSpecifier' 113 | ? 'default' 114 | : s.imported.name, 115 | })) 116 | const source = path.node.source.value 117 | const result = applyMacros({ 118 | path, 119 | imports, 120 | source, 121 | state, 122 | babel, 123 | interopRequire, 124 | resolvePath, 125 | options, 126 | }) 127 | 128 | if (!result || !result.keepImports) { 129 | path.remove() 130 | } 131 | }, 132 | VariableDeclaration(path) { 133 | const isMacros = child => 134 | looksLike(child, { 135 | node: { 136 | init: { 137 | callee: { 138 | type: 'Identifier', 139 | name: 'require', 140 | }, 141 | arguments: args => 142 | args.length === 1 && isMacrosName(args[0].value), 143 | }, 144 | }, 145 | }) 146 | 147 | path 148 | .get('declarations') 149 | .filter(isMacros) 150 | .forEach(child => { 151 | const imports = child.node.id.name 152 | ? [{localName: child.node.id.name, importedName: 'default'}] 153 | : child.node.id.properties.map(property => ({ 154 | localName: property.value.name, 155 | importedName: property.key.name, 156 | })) 157 | 158 | const call = child.get('init') 159 | const source = call.node.arguments[0].value 160 | const result = applyMacros({ 161 | path: call, 162 | imports, 163 | source, 164 | state, 165 | babel, 166 | interopRequire, 167 | resolvePath, 168 | options, 169 | }) 170 | 171 | if (!result || !result.keepImports) { 172 | child.remove() 173 | } 174 | }) 175 | }, 176 | }) 177 | }, 178 | }, 179 | } 180 | } 181 | 182 | // eslint-disable-next-line complexity 183 | function applyMacros({ 184 | path, 185 | imports, 186 | source, 187 | state, 188 | babel, 189 | interopRequire, 190 | resolvePath, 191 | options, 192 | }) { 193 | /* istanbul ignore next (pretty much only useful for astexplorer I think) */ 194 | const { 195 | file: { 196 | opts: {filename = ''}, 197 | }, 198 | } = state 199 | let hasReferences = false 200 | const referencePathsByImportName = imports.reduce( 201 | (byName, {importedName, localName}) => { 202 | const binding = path.scope.getBinding(localName) 203 | 204 | byName[importedName] = binding.referencePaths 205 | hasReferences = hasReferences || Boolean(byName[importedName].length) 206 | 207 | return byName 208 | }, 209 | {}, 210 | ) 211 | 212 | const isRelative = source.indexOf('.') === 0 213 | const requirePath = resolvePath(source, p.dirname(getFullFilename(filename))) 214 | 215 | const macro = interopRequire(requirePath) 216 | if (!macro.isBabelMacro) { 217 | throw new Error( 218 | `The macro imported from "${source}" must be wrapped in "createMacro" ` + 219 | `which you can get from "babel-plugin-macros". ` + 220 | `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`, 221 | ) 222 | } 223 | const config = getConfig(macro, filename, source, options) 224 | 225 | let result 226 | try { 227 | /** 228 | * Other plugins that run before babel-plugin-macros might use path.replace, where a path is 229 | * put into its own replacement. Apparently babel does not update the scope after such 230 | * an operation. As a remedy, the whole scope is traversed again with an empty "Identifier" 231 | * visitor - this makes the problem go away. 232 | * 233 | * See: https://github.com/kentcdodds/import-all.macro/issues/7 234 | */ 235 | state.file.scope.path.traverse({ 236 | Identifier() {}, 237 | }) 238 | 239 | result = macro({ 240 | references: referencePathsByImportName, 241 | source, 242 | state, 243 | babel, 244 | config, 245 | isBabelMacrosCall: true, 246 | }) 247 | } catch (error) { 248 | if (error.name === 'MacroError') { 249 | throw error 250 | } 251 | error.message = `${source}: ${error.message}` 252 | if (!isRelative) { 253 | error.message = `${ 254 | error.message 255 | } Learn more: https://www.npmjs.com/package/${source.replace( 256 | // remove everything after package name 257 | // @org/package/macro -> @org/package 258 | // package/macro -> package 259 | /^((?:@[^/]+\/)?[^/]+).*/, 260 | '$1', 261 | )}` 262 | } 263 | throw error 264 | } 265 | return result 266 | } 267 | 268 | function getConfigFromFile(configName, filename) { 269 | try { 270 | const loaded = getConfigExplorer().search(filename) 271 | 272 | if (loaded) { 273 | return { 274 | options: loaded.config[configName], 275 | path: loaded.filepath, 276 | } 277 | } 278 | } catch (e) { 279 | return {error: e} 280 | } 281 | return {} 282 | } 283 | 284 | function getConfigFromOptions(configName, options) { 285 | if (options.hasOwnProperty(configName)) { 286 | if (options[configName] && typeof options[configName] !== 'object') { 287 | // eslint-disable-next-line no-console 288 | console.error( 289 | `The macro plugin options' ${configName} property was not an object or null.`, 290 | ) 291 | } else { 292 | return {options: options[configName]} 293 | } 294 | } 295 | return {} 296 | } 297 | 298 | function getConfig(macro, filename, source, options) { 299 | const {configName} = macro.options 300 | if (configName) { 301 | const fileConfig = getConfigFromFile(configName, filename) 302 | const optionsConfig = getConfigFromOptions(configName, options) 303 | 304 | if ( 305 | optionsConfig.options === undefined && 306 | fileConfig.options === undefined && 307 | fileConfig.error !== undefined 308 | ) { 309 | // eslint-disable-next-line no-console 310 | console.error( 311 | `There was an error trying to load the config "${configName}" ` + 312 | `for the macro imported from "${source}. ` + 313 | `Please see the error thrown for more information.`, 314 | ) 315 | throw fileConfig.error 316 | } 317 | 318 | if ( 319 | fileConfig.options !== undefined && 320 | optionsConfig.options !== undefined && 321 | typeof fileConfig.options !== 'object' 322 | ) { 323 | throw new Error( 324 | `${fileConfig.path} specified a ${configName} config of type ` + 325 | `${typeof optionsConfig.options}, but the the macros plugin's ` + 326 | `options.${configName} did contain an object. Both configs must ` + 327 | `contain objects for their options to be mergeable.`, 328 | ) 329 | } 330 | 331 | return { 332 | ...optionsConfig.options, 333 | ...fileConfig.options, 334 | } 335 | } 336 | return undefined 337 | } 338 | 339 | /* 340 | istanbul ignore next 341 | because this is hard to test 342 | and not worth it... 343 | */ 344 | function getFullFilename(filename) { 345 | if (p.isAbsolute(filename)) { 346 | return filename 347 | } 348 | return p.join(process.cwd(), filename) 349 | } 350 | 351 | function looksLike(a, b) { 352 | return ( 353 | a && 354 | b && 355 | Object.keys(b).every(bKey => { 356 | const bVal = b[bKey] 357 | const aVal = a[bKey] 358 | if (typeof bVal === 'function') { 359 | return bVal(aVal) 360 | } 361 | return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal) 362 | }) 363 | ) 364 | } 365 | 366 | function isPrimitive(val) { 367 | // eslint-disable-next-line 368 | return val == null || /^[sbn]/.test(typeof val) 369 | } 370 | 371 | module.exports = macrosPlugin 372 | Object.assign(module.exports, { 373 | createMacro, 374 | MacroError, 375 | }) 376 | --------------------------------------------------------------------------------