├── .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 | 
231 |
232 | **Node:**
233 |
234 |
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 |
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 | [](https://github.com/kentcdodds/babel-plugin-macros)
102 |
103 | ```
104 | [](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 | ,
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 |
--------------------------------------------------------------------------------