Repository: kentcdodds/babel-macros Branch: main Commit: fc04c3ed721c Files: 50 Total size: 106.9 KB Directory structure: gitextract_7a72cab4/ ├── .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.js │ ├── babel-plugin-macros-test-error-thrower.macro/ │ │ └── index.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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .all-contributorsrc ================================================ { "projectName": "babel-plugin-macros", "projectOwner": "kentcdodds", "imageSize": 100, "commit": false, "contributorsPerLine": 7, "repoHost": "https://github.com", "repoType": "github", "skipCi": false, "files": [ "README.md" ], "contributors": [ { "login": "kentcdodds", "name": "Kent C. Dodds", "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", "profile": "https://kentcdodds.com", "contributions": [ "code", "doc", "infra", "test" ] }, { "login": "threepointone", "name": "Sunil Pai", "avatar_url": "https://avatars1.githubusercontent.com/u/18808?v=3", "profile": "https://github.com/threepointone", "contributions": [ "ideas" ] }, { "login": "suchipi", "name": "Lily Scott", "avatar_url": "https://avatars0.githubusercontent.com/u/1341513?v=4", "profile": "http://suchipi.com", "contributions": [ "question", "doc" ] }, { "login": "dralletje", "name": "Michiel Dral", "avatar_url": "https://avatars1.githubusercontent.com/u/767261?v=4", "profile": "http://twitter.com/dralletje", "contributions": [ "ideas" ] }, { "login": "tkh44", "name": "Kye Hohenberger", "avatar_url": "https://avatars2.githubusercontent.com/u/662750?v=4", "profile": "https://github.com/tkh44", "contributions": [ "ideas" ] }, { "login": "mitchellhamilton", "name": "Mitchell Hamilton", "avatar_url": "https://avatars1.githubusercontent.com/u/11481355?v=4", "profile": "https://hamil.town", "contributions": [ "code", "test" ] }, { "login": "wKovacs64", "name": "Justin Hall", "avatar_url": "https://avatars1.githubusercontent.com/u/1288694?v=4", "profile": "https://github.com/wKovacs64", "contributions": [ "doc" ] }, { "login": "PiereDome", "name": "Brian Pedersen", "avatar_url": "https://avatars3.githubusercontent.com/u/1903016?v=4", "profile": "https://github.com/PiereDome", "contributions": [ "code", "doc" ] }, { "login": "apalm", "name": "Andrew Palm", "avatar_url": "https://avatars3.githubusercontent.com/u/4495237?v=4", "profile": "https://github.com/apalm", "contributions": [ "code" ] }, { "login": "evenchange4", "name": "Michael Hsu", "avatar_url": "https://avatars1.githubusercontent.com/u/1527371?v=4", "profile": "https://michaelhsu.tw/", "contributions": [ "doc", "plugin" ] }, { "login": "citycide", "name": "Bo Lingen", "avatar_url": "https://avatars2.githubusercontent.com/u/16605186?v=4", "profile": "https://github.com/citycide", "contributions": [ "code" ] }, { "login": "tylerthehaas", "name": "Tyler Haas", "avatar_url": "https://avatars1.githubusercontent.com/u/11150235?v=4", "profile": "https://github.com/tylerthehaas", "contributions": [ "doc" ] }, { "login": "FWeinb", "name": "FWeinb", "avatar_url": "https://avatars0.githubusercontent.com/u/1250430?v=4", "profile": "https://github.com/FWeinb", "contributions": [ "code" ] }, { "login": "tricoder42", "name": "Tomáš Ehrlich", "avatar_url": "https://avatars2.githubusercontent.com/u/827862?v=4", "profile": "http://www.tomasehrlich.cz", "contributions": [ "bug", "code" ] }, { "login": "jgierer12", "name": "Jonas Gierer", "avatar_url": "https://avatars0.githubusercontent.com/u/4331946?v=4", "profile": "https://github.com/jgierer12", "contributions": [ "doc" ] }, { "login": "lPadier", "name": "Loïc Padier", "avatar_url": "https://avatars2.githubusercontent.com/u/4009640?v=4", "profile": "http://loicpadier.com", "contributions": [ "code" ] }, { "login": "pshrmn", "name": "Paul Sherman", "avatar_url": "https://avatars0.githubusercontent.com/u/1127037?v=4", "profile": "https://www.pshrmn.com", "contributions": [ "code" ] }, { "login": "conartist6", "name": "Conrad Buck", "avatar_url": "https://avatars1.githubusercontent.com/u/540777?v=4", "profile": "http://burningpotato.com", "contributions": [ "code", "test", "doc" ] }, { "login": "InvictusMB", "name": "InvictusMB", "avatar_url": "https://avatars3.githubusercontent.com/u/3091209?v=4", "profile": "https://github.com/InvictusMB", "contributions": [ "test" ] }, { "login": "coderberry", "name": "Eric Berry", "avatar_url": "https://avatars2.githubusercontent.com/u/12481?v=4", "profile": "https://codefund.io", "contributions": [ "fundingFinding" ] }, { "login": "futagoza", "name": "Futago-za Ryuu", "avatar_url": "https://avatars1.githubusercontent.com/u/1943570?v=4", "profile": "http://futagoza.github.io/", "contributions": [ "code", "test" ] }, { "login": "lucleray", "name": "Luc", "avatar_url": "https://avatars3.githubusercontent.com/u/6616955?v=4", "profile": "https://luc.im", "contributions": [ "code" ] }, { "login": "wintercounter", "name": "Victor Vincent", "avatar_url": "https://avatars2.githubusercontent.com/u/963776?v=4", "profile": "http://wintercounter.me", "contributions": [ "code" ] }, { "login": "mvasilkov", "name": "я котик пур-пур", "avatar_url": "https://avatars3.githubusercontent.com/u/140257?v=4", "profile": "http://mvasilkov.ovh", "contributions": [ "doc" ] }, { "login": "soska", "name": "Armando Sosa", "avatar_url": "https://avatars0.githubusercontent.com/u/139577?v=4", "profile": "http://armandososa.com", "contributions": [ "doc" ] }, { "login": "matvp91", "name": "Matthias", "avatar_url": "https://avatars3.githubusercontent.com/u/12699796?v=4", "profile": "https://github.com/matvp91", "contributions": [ "code" ] }, { "login": "JoviDeCroock", "name": "Jovi De Croock", "avatar_url": "https://avatars3.githubusercontent.com/u/17125876?v=4", "profile": "https://www.jovidecroock.com/", "contributions": [ "code", "test" ] }, { "login": "VictorArowo", "name": "Victor Arowo", "avatar_url": "https://avatars0.githubusercontent.com/u/25545108?v=4", "profile": "http://victorarowo.com", "contributions": [ "doc" ] }, { "login": "alexanderchan", "name": "Alex Chan", "avatar_url": "https://avatars.githubusercontent.com/u/1864372?v=4", "profile": "https://twitter.com/alexandermchan", "contributions": [ "doc" ] }, { "login": "probablyup", "name": "Evan Jacobs", "avatar_url": "https://avatars.githubusercontent.com/u/570070?v=4", "profile": "https://probablyup.com", "contributions": [ "code" ] } ] } ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ - `babel-plugin-macros` version: - `node` version: - `npm` version: Relevant code or config ```javascript ``` What you did: What happened: Reproduction repository: Problem description: Suggested solution: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ **What**: **Why**: **How**: **Checklist**: - [ ] Documentation - [ ] Tests - [ ] Ready to be merged ================================================ FILE: .github/workflows/validate.yml ================================================ name: validate on: push: branches: - '+([0-9])?(.{+([0-9]),x}).x' - 'main' - 'next' - 'next-major' - 'beta' - 'alpha' - '!all-contributors/**' pull_request: {} jobs: main: # ignore all-contributors PRs if: ${{ !contains(github.head_ref, 'all-contributors') }} strategy: matrix: node: [10.13, 12, 14, 15] runs-on: ubuntu-latest steps: - name: ⬇️ Checkout repo uses: actions/checkout@v2 - name: ⎔ Setup node uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} - name: 📥 Download deps uses: bahmutov/npm-install@v1 with: useLockFile: false - name: ▶️ Run validate script run: npm run validate - name: ⬆️ Upload coverage report uses: codecov/codecov-action@v1 release: needs: main runs-on: ubuntu-latest if: ${{ github.repository == 'kentcdodds/babel-plugin-macros' && contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', github.ref) && github.event_name == 'push' }} steps: - name: ⬇️ Checkout repo uses: actions/checkout@v2 - name: ⎔ Setup node uses: actions/setup-node@v1 with: node-version: 14 - name: 📥 Download deps uses: bahmutov/npm-install@v1 with: useLockFile: false - name: 🚀 Release uses: cycjimmy/semantic-release-action@v2 with: semantic_version: 17 branches: | [ '+([0-9])?(.{+([0-9]),x}).x', 'main', 'next', 'next-major', {name: 'beta', prerelease: true}, {name: 'alpha', prerelease: true} ] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .gitignore ================================================ node_modules coverage dist .DS_Store # these cause more harm than good # when working with contributors package-lock.json yarn.lock ================================================ FILE: .huskyrc.js ================================================ module.exports = require('kcd-scripts/husky') ================================================ FILE: .npmrc ================================================ registry=https://registry.npmjs.org/ ================================================ FILE: .prettierignore ================================================ node_modules coverage dist ================================================ FILE: .prettierrc.js ================================================ module.exports = require('kcd-scripts/prettier') ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release). You can see it on the [releases page](../../releases). ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thanks for being willing to contribute! **Working on your first Pull Request?** You can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub][egghead] ## Project setup 1. Fork and clone the repo 2. Run `npm run setup -s` to install dependencies and run validation 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` > Tip: Keep your `main` branch pointing at the original repository and make pull > requests from branches on your fork. To do this, run: > > ``` > git remote add upstream https://github.com/kentcdodds/babel-plugin-macros > git fetch upstream > git branch --set-upstream-to=upstream/main main > ``` > > This will add the original repository as a "remote" called "upstream," Then > fetch the git information from that remote, then set your local `main` branch > to use the upstream main branch whenever you run `git pull`. Then you can make > all of your pull request branches based on this `main` branch. Whenever you > want to update your version of `main`, do a regular `git pull`. ## Committing and Pushing changes Please make sure to run the tests before you commit your changes. You can run `npm run test:update` which will update any snapshots that need updating. Make sure to include those changes (if they exist) in your commit. ## Help needed Please checkout the [the open issues][issues] Also, please watch the repo and respond to questions/bug reports/feature requests! Thanks! [egghead]: https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github [all-contributors]: https://github.com/all-contributors/all-contributors [issues]: https://github.com/kentcdodds/babel-plugin-macros/issues ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 Kent C. Dodds Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

babel-plugin-macros 🎣

Allows you to build simple compile-time libraries

--- [![Build Status][build-badge]][build] [![Code Coverage][coverage-badge]][coverage] [![version][version-badge]][package] [![downloads][downloads-badge]][npmtrends] [![MIT License][license-badge]][license] [![All Contributors][all-contributors-badge]](#contributors-) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] ## The problem Check out [this guest post](https://babeljs.io/blog/2017/09/11/zero-config-with-babel-macros) on the Babel.js blog for a complete write up on the problem, motivation, and solution. Currently, each babel plugin in the babel ecosystem requires that you configure it individually. This is fine for things like language features, but can be frustrating overhead for libraries that allow for compile-time code transformation as an optimization. ## This solution babel-plugin-macros defines a standard interface for libraries that want to use compile-time code transformation without requiring the user to add a babel plugin to their build system (other than `babel-plugin-macros`, which is ideally already in place).
Expand for more details on the motivation For instance, many css-in-js libraries have a css tagged template string function: ```js const styles = css` .red { color: red; } ` ``` The function compiles your css into (for example) an object with generated class names for each of the classes you defined in your css: ```js console.log(styles) // { red: "1f-d34j8rn43y587t" } ``` This class name can be generated at runtime (in the browser), but this has some disadvantages: - There is cpu usage/time overhead; the client needs to run the code to generate these classes every time the page loads - There is code bundle size overhead; the client needs to receive a CSS parser in order to generate these class names, and shipping this makes the amount of js the client needs to parse larger. To help solve those issues, many css-in-js libraries write their own babel plugin that generates the class names at compile-time instead of runtime: ```js // Before running through babel: const styles = css` .red { color: red; } ` // After running through babel, with the library-specific plugin: const styles = {red: '1f-d34j8rn43y587t'} ``` If the css-in-js library supported babel-plugin-macros instead, then they wouldn't need their own babel plugin to compile these out; they could instead rely on babel-plugin-macros to do it for them. So if a user already had `babel-plugin-macros` installed and configured with babel, then they wouldn't need to change their babel configuration to get the compile-time benefits of the library. This would be most useful if the boilerplate they were using came with `babel-plugin-macros` out of the box, which is true for [`create-react-app`][cra]. Although css-in-js is the most common example, there are lots of other things you could use `babel-plugin-macros` for, like: - Compiling GraphQL fragments into objects so that the client doesn't need a GraphQL parser - Eval-ing out code at compile time that will be baked into the runtime code, for instance to get a list of directories in the filesystem (see [preval][preval])
## Table of Contents - [Installation](#installation) - [Usage](#usage) - [User docs](#user-docs) - [Author docs](#author-docs) - [Caveats](#caveats) - [FAQ](#faq) - [How do I find available macros?](#how-do-i-find-available-macros) - [What's the difference between babel plugins and macros?](#whats-the-difference-between-babel-plugins-and-macros) - [In what order are macros executed?](#in-what-order-are-macros-executed) - [Does it work with function calls only?](#does-it-work-with-function-calls-only) - [How about implicit optimizations at compile time?](#how-about-implicit-optimizations-at-compile-time) - [Inspiration](#inspiration) - [Other Solutions](#other-solutions) - [Issues](#issues) - [🐛 Bugs](#-bugs) - [💡 Feature Requests](#-feature-requests) - [Contributors ✨](#contributors-) - [LICENSE](#license) ## Installation This module is distributed via [npm][npm] which is bundled with [node][node] and should be installed as one of your project's `devDependencies`: ``` npm install --save-dev babel-plugin-macros ``` ## Usage > You may like to watch > [this YouTube video](https://www.youtube.com/watch?v=1queadQ0048&list=PLV5CVI1eNcJgCrPH_e6d57KRUTiDZgs0u) > to get an idea of what macros is and how it can be used. ### User docs Are you trying to use `babel-plugin-macros`? Go to [`other/docs/user.md`](other/docs/user.md). ### Author docs Are you trying to make your own macros that works with `babel-plugin-macros`? Go to [`other/docs/author.md`](other/docs/author.md). (you should probably read the user docs too). ### Caveats #### Babel cache problem > **Note:** This issue is not present when used in Create React App. Most of the time you'll probably be using this with the babel cache enabled in webpack to rebuild faster. If your macro function is **not pure** which gets different output with same code (e.g., IO side effects) it will cause recompile mechanism fail. Unfortunately you'll also experience this problem while developing your macro as well. If there's not a change to the source code that's being transpiled, then babel will use the cache rather than running your macro again. For now, to force recompile the code you can simply add a cache busting comment in the file: ```diff import macro from 'non-pure.macro'; -// Do some changes of your code or +// add a cache busting comment to force recompile. macro('parameters'); ``` This problem is still being worked on and is not unique to `babel-plugin-macros`. For more details and workarounds, please check related issues below: - babel-plugin-preval: [How to force recompile? #19](https://github.com/kentcdodds/babel-plugin-preval/issues/19) - graphql.macro: [Recompile problem (babel cache) #6](https://github.com/evenchange4/graphql.macro/issues/6) - twin.macro: [Can't change taliwind config #37](https://github.com/ben-rogerson/twin.macro/discussions/37) ## FAQ ### How do I find available macros? You can write your own without publishing them to `npm`, but if you'd like to see existing macros you can add to your project, then take a look at the [Awesome babel macros](https://github.com/jgierer12/awesome-babel-macros) repository. Please add any you don't see listed! ### What's the difference between babel plugins and macros? Let's use [`babel-plugin-console`](https://www.npmjs.com/package/babel-plugin-console) as an example. If we used `babel-plugin-console`, it would look like this: 1. Add `babel-plugin-console` to `.babelrc` 2. Use it in a code: ```js function add100(a) { const oneHundred = 100 console.scope('Add 100 to another number') return add(a, oneHundred) } function add(a, b) { return a + b } ``` When that code is run, the `scope` function does some pretty nifty things: **Browser:** ![Browser console scoping add100](https://github.com/mattphillips/babel-plugin-console/raw/53536cba919d5be49d4f66d957769c07ca7a4207/assets/add100-chrome.gif) **Node:** Node console scoping add100 Instead, let's use the macro it's shipped with like this: 1. Add `babel-plugin-macros` to `.babelrc` (only once for all macros) 2. Use it in a code: ```js import scope from 'babel-plugin-console/scope.macro' function add100(a) { const oneHundred = 100 scope('Add 100 to another number') return add(a, oneHundred) } function add(a, b) { return a + b } ``` The result is exactly the same, but this approach has a few advantages: **Advantages:** - requires only one entry in `.babelrc` for all macros used in project. Add that once and you can use all the macros you want - toolkits (like [create-react-app][cra]) may already support `babel-plugin-macros`, so no configuration is needed at all - it's explicit. With `console.scope` people may be fooled that it's just a normal `console` API when there's really a babel transpilation going on. When you import `scope`, it's obvious that it's macro and does something with the code at compile time. Some ESLint rules may also have issues with plugins that look for "global" variables - macros are safer and easier to write, because they receive exactly the AST node to process - If you misconfigure `babel-plugin-console` you wont find out until you run the code. If you misconfigure `babel-plugin-macros` you'll get a compile-time error. **Drawbacks:** - Cannot (should not) be used for implicit transpilations (like syntax plugins) - Explicitness is more verbose. Which some people might consider a drawback... ### In what order are macros executed? This is another advantage of `babel-plugin-macros` over regular plugins. The user of the macro is in control of the ordering! The order of execution is the same order as imported. The order of execution is clear, explicit and in full control of the user: ```js import preval from 'preval.macro' import idx from 'idx.macro' // preval macro is evaluated first, then idx ``` This differs from the current situation with babel plugins where it's prohibitively difficult to control the order plugins run in a particular file. ### Does it work with function calls only? No! Any AST node type is supported. It can be tagged template literal: ```js import eval from 'eval.macro' const val = eval`7 * 6` ``` A function: ```js import eval from 'eval.macro' const val = eval('7 * 6') ``` JSX Element: ```js import Eval from 'eval.macro' const val = 7 * 6 ``` Really, anything... See the [testing snapshot](src/__tests__/__snapshots__/index.js.snap) for more examples. ### How about implicit optimizations at compile time? All examples above were _explicit_ - a macro was imported and then evaluated with a specific AST node. Completely different story are _implicit_ babel plugins, like [transform-react-constant-elements](https://babeljs.io/docs/plugins/transform-react-constant-elements/), which process whole AST tree. Explicit is often a better pattern than implicit because it requires others to understand how things are globally configured. This is in this spirit are `babel-plugin-macros` designed. However, some things _do_ need to be implicit, and those kinds of babel plugins can't be turned into macros. ## Inspiration - [threepointone/babel-plugin-macros](https://github.com/threepointone/babel-plugin-macros) - [facebookincubator/create-react-app#2730][cra-issue] Thank you to [@phpnode](https://github.com/phpnode) for donating the npm package `babel-plugin-macros`. ## Other Solutions - [sweetjs](http://sweetjs.org/) ## Issues _Looking to contribute? Look for the [Good First Issue][good-first-issue] label._ ### 🐛 Bugs Please file an issue for bugs, missing documentation, or unexpected behavior. [**See Bugs**][bugs] ### 💡 Feature Requests Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on. [**See Feature Requests**][requests] ## Contributors ✨ Thanks goes to these people ([emoji key][emojis]):

Kent C. Dodds

💻 📖 🚇 ⚠️

Sunil Pai

🤔

Lily Scott

💬 📖

Michiel Dral

🤔

Kye Hohenberger

🤔

Mitchell Hamilton

💻 ⚠️

Justin Hall

📖

Brian Pedersen

💻 📖

Andrew Palm

💻

Michael Hsu

📖 🔌

Bo Lingen

💻

Tyler Haas

📖

FWeinb

💻

Tomáš Ehrlich

🐛 💻

Jonas Gierer

📖

Loïc Padier

💻

Paul Sherman

💻

Conrad Buck

💻 ⚠️ 📖

InvictusMB

⚠️

Eric Berry

🔍

Futago-za Ryuu

💻 ⚠️

Luc

💻

Victor Vincent

💻

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

📖

Armando Sosa

📖

Matthias

💻

Jovi De Croock

💻 ⚠️

Victor Arowo

📖

Alex Chan

📖

Evan Jacobs

💻
This project follows the [all-contributors][all-contributors] specification. Contributions of any kind welcome! ## LICENSE MIT [npm]: https://www.npmjs.com [node]: https://nodejs.org [build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/babel-plugin-macros/validate?logo=github&style=flat-square [build]: https://github.com/kentcdodds/babel-plugin-macros/actions?query=workflow%3Avalidate [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/babel-plugin-macros.svg?style=flat-square [coverage]: https://codecov.io/github/kentcdodds/babel-plugin-macros [version-badge]: https://img.shields.io/npm/v/babel-plugin-macros.svg?style=flat-square [package]: https://www.npmjs.com/package/babel-plugin-macros [downloads-badge]: https://img.shields.io/npm/dm/babel-plugin-macros.svg?style=flat-square [npmtrends]: http://www.npmtrends.com/babel-plugin-macros [license-badge]: https://img.shields.io/npm/l/babel-plugin-macros.svg?style=flat-square [license]: https://github.com/kentcdodds/babel-plugin-macros/blob/main/LICENSE [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square [prs]: http://makeapullrequest.com [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square [coc]: https://github.com/kentcdodds/babel-plugin-macros/blob/main/CODE_OF_CONDUCT.md [emojis]: https://github.com/all-contributors/all-contributors#emoji-key [all-contributors]: https://github.com/all-contributors/all-contributors [all-contributors-badge]: https://img.shields.io/github/all-contributors/kentcdodds/babel-plugin-macros?color=orange&style=flat-square [bugs]: https://github.com/kentcdodds/babel-plugin-macros/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug [requests]: https://github.com/kentcdodds/babel-plugin-macros/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement [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 [preval]: https://github.com/kentcdodds/babel-plugin-preval [cra]: https://github.com/facebook/create-react-app [cra-issue]: https://github.com/facebook/create-react-app/issues/2730 ================================================ FILE: other/MAINTAINING.md ================================================ # Maintaining This is documentation for maintainers of this project. ## Code of Conduct Please review, understand, and be an example of it. Violations of the code of conduct are taken seriously, even (especially) for maintainers. ## Issues We want to support and build the community. We do that best by helping people learn to solve their own problems. We have an issue template and hopefully most folks follow it. If it's not clear what the issue is, invite them to create a minimal reproduction of what they're trying to accomplish or the bug they think they've found. Once it's determined that a code change is necessary, point people to [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a pull request. If they're the one who needs the feature, they're the one who can build it. If they need some hand holding and you have time to lend a hand, please do so. It's an investment into another human being, and an investment into a potential maintainer. Remember that this is open source, so the code is not yours, it's ours. If someone needs a change in the codebase, you don't have to make it happen yourself. Commit as much time to the project as you want/need to. Nobody can ask any more of you than that. ## Pull Requests As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either way is fine. When we receive a pull request, a github action is kicked off automatically (see the `.github/workflows/validate.yml` for what runs in the action). We avoid merging anything that breaks the validate action. Please review PRs and focus on the code rather than the individual. You never know when this is someone's first ever PR and we want their experience to be as positive as possible, so be uplifting and constructive. When you merge the pull request, 99% of the time you should use the [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) feature. This keeps our git history clean, but more importantly, this allows us to make any necessary changes to the commit message so we release what we want to release. See the next section on Releases for more about that. ## Release Our releases are automatic. They happen whenever code lands into `main`. A github action gets kicked off and if it's successful, a tool called [`semantic-release`](https://github.com/semantic-release/semantic-release) is used to automatically publish a new release to npm as well as a changelog to GitHub. It is only able to determine the version and whether a release is necessary by the git commit messages. With this in mind, **please brush up on [the commit message convention][commit] which drives our releases.** > One important note about this: Please make sure that commit messages do NOT > contain the words "BREAKING CHANGE" in them unless we want to push a major > version. I've been burned by this more than once where someone will include > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not > a huge deal honestly, but kind of annoying... ## Thanks! Thank you so much for helping to maintain this project! [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md ================================================ FILE: other/USERS.md ================================================ # Users If you or your company uses this project, add your name to this list! Eventually we may have a website to showcase these (wanna build it!?) > No users have been added yet! ================================================ FILE: other/docs/author.md ================================================ # `babel-plugin-macros` Usage for macros authors > See also: > [the `user` docs](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md). Is this your first time working with ASTs? Here are some resources: - [Writing custom Babel and ESLint plugins with ASTs](https://youtu.be/VBscbcm2Mok?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf): A 53 minute talk by [@kentcdodds](https://twitter.com/kentcdodds) - [babel-handbook](https://github.com/thejameskyle/babel-handbook): A guided handbook on how to use Babel and how to create plugins for Babel by [@thejameskyle](https://twitter.com/thejameskyle) - [Code Transformation and Linting](https://kentcdodds.com/workshops/#code-transformation-and-linting): A workshop (recording available on Frontend Masters) with exercises of making custom Babel and ESLint plugins ## Writing a macro > You might appreciate > [this example repo](https://github.com/kentcdodds/cra-macro-example) which > shows how to write and use macros in a create-react-app application. A macro is a JavaScript module that exports a function. Here's a simple example: ```javascript const {createMacro} = require('babel-plugin-macros') // `createMacro` is simply a function that ensures your macro is only // called in the context of a babel transpilation and will throw an // error with a helpful message if someone does not have babel-plugin-macros // configured correctly module.exports = createMacro(myMacro) function myMacro({references, state, babel}) { // state is the second argument you're passed to a visitor in a // normal babel plugin. `babel` is the `babel-plugin-macros` module. // do whatever you like to the AST paths you find in `references` // read more below... } ``` It can be published to the npm registry (for generic macros, like a css-in-js library) or used locally (for domain-specific macros, like handling some special case for your company's localization efforts). > Before you write a custom macro, you might consider whether > [`babel-plugin-preval`][preval] help you do what you want as it's pretty > powerful. There are two parts to the `babel-plugin-macros` API: 1. The filename convention 2. The function you export ### Filename The way that `babel-plugin-macros` determines whether to run a macro is based on the source string of the `import` or `require` statement. It must match this regex: `/[./]macro(\.c?js)?$/` for example: _matches_: ``` 'my.macro' 'my.macro.js' 'my.macro.cjs' 'my/macro' 'my/macro.js' 'my/macro.cjs' ``` _does not match_: ``` 'my-macro' 'my.macro.is-sweet' 'my/macro/rocks' ``` > So long as your file can be required at a matching path, you're good. So you > could put it in: `my/macro/index.js` and people would: `require('my/macro')` > which would work fine. **If you're going to publish this to npm,** the most ergonomic thing would be to name it something that ends in `.macro`. If it's part of a larger package, then calling the file `macro.js` or placing it in `macro/index.js` is a great way to go as well. Then people could do: ```js import Nice from 'nice.macro' // or import Sweet from 'sweet/macro' ``` In addition, please publish your macro with the [`keyword`][keyword] of `babel-plugin-macros` (note the "s"). That way folks can easily find macros by searching for the [`babel-plugin-macros` keyword on npm][npm-babel-plugin-macros]. In addition, and you can add this badge to the top of your README: [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros) ``` [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros) ``` ### Function API The macro you create should export a function. That function accepts a single parameter which is an object with the following properties: #### state The state of the file being traversed. It's the second argument you receive in a visitor function in a normal babel plugin. #### babel This is the same thing you get as an argument to normal babel plugins. It is also the same thing you get if you `require('babel-core')`. #### references This is an object that contains arrays of all the references to things imported from macro keyed based on the name of the import. The items in each array are the paths to the references.
Some examples: ```javascript import MyMacro from './my.macro' MyMacro( {someOption: true}, ` some stuff `, ) // references: { default: [BabelPath] } ``` ```javascript import {foo as FooMacro} from './my.macro' FooMacro( {someOption: true}, ` some stuff `, ) // references: { foo: [BabelPath] } ``` ```javascript import {foo as FooMacro} from './my.macro' // no usage... // references: {} ```
From here, it's just a matter of doing stuff with the `BabelPath`s that you're given. For that check out [the babel handbook][babel-handbook]. > One other thing to note is that after your macro has run, babel-plugin-macros > will remove the import/require statement for you. #### source This is a string used as import declaration's source - i.e. `'./my.macro'`. #### config There is a feature that allows users to configure your macro. To specify that your plugin is configurable, you pass a `configName` to `createMacro`. A configuration is created from data combined from two sources: We use [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which can be located in any of the following files up the directories from the importing file: - `.babel-plugin-macrosrc` - `.babel-plugin-macrosrc.json` - `.babel-plugin-macrosrc.yaml` - `.babel-plugin-macrosrc.yml` - `.babel-plugin-macrosrc.js` - `babel-plugin-macros.config.js` - `babelMacros` in `package.json` The content of the config will be merged with the content of the babel macros plugin options. Config options take priority. All together specifying and using the config might look like this: ```javascript // .babel-plugin-macros.config.js module.exports = { taggedTranslations: {locale: 'en_US'}, } // .babel.config.js module.exports = { plugins: [ [ "macros", { taggedTranslations: { locale: "en_GB" }, }, ], ], } // taggedTranslations.macro.js const {createMacro} = require('babel-plugin-macros') module.exports = createMacro(taggedTranslationsMacro, { configName: 'taggedTranslations', }) function taggedTranslationsMacro({references, state, babel, config}) { const {locale = 'en'} = config } ``` Note that in the above example if both files were specified, the final locale value would be `en_US`, since that is the value in the plugin config file. ### Keeping imports As said before, `babel-plugin-macros` automatically removes an import statement of macro. If you want to keep it because you have other plugins processing macros, return `{ keepImports: true }` from your macro: ```javascript const {createMacro} = require('babel-plugin-macros') module.exports = createMacro(taggedTranslationsMacro) function taggedTranslationsMacro({references, state, babel}) { // process node from references return { keepImports: true, } } ``` ## Throwing Helpful Errors Debugging stuff that transpiles your code is the worst, especially for beginners. That's why it's important that you make assertions, and catch errors to throw more meaningful errors with helpful information for the developer to know what to do to resolve the issue. In an effort to make this easier for you, `babel-plugin-macros` will wrap the invocation of your plugin in a `try/catch` and throw as helpful an error message as possible for you. To make it even better, you can throw your own with more context. For example: ```javascript const {createMacro, MacroError} = require('babel-plugin-macros') module.exports = createMacro(myMacro) function myMacro({references, state, babel}) { // something unexpected happens: throw new MacroError( 'Some helpful and contextual message. Learn more: ' + 'https://github.com/your-org/your-repo/blob/master/docs/errors.md#learn-more-about-eror-title', ) } ``` ## Testing your macro The best way to test your macro is using [`babel-plugin-tester`][tester]: ```javascript import pluginTester from 'babel-plugin-tester' import plugin from 'babel-plugin-macros' pluginTester({ plugin, snapshot: true, babelOptions: {filename: __filename}, tests: [ ` import MyMacro from '../my.macro' MyMacro({someOption: true}, \` some stuff \`) `, ], }) ``` There is currently no way to get code coverage for your macro this way however. If you want code coverage, you'll have to call your macro yourself. Contributions to improve this experience are definitely welcome! ## Async logic Unfortunately, babel plugins are synchronous so you can't do anything asynchronous with `babel-plugin-macros`. However, you can cheat a bit by running `child_process`'s `spawnSync` to synchronously execute a file. It's definitely a hack and is not great for performance, but in most cases it's fast enough™️. Luckily, [@Zemnmez](https://github.com/Zemnmez) created [`do-sync`](https://github.com/Zemnmez/do-sync) which makes doing this much more straightforward: ```javascript const {doSync} = require('do-sync') const {createMacro, MacroError} = require('babel-plugin-macros') module.exports = createMacro(myMacro) const getTheFlowers = doSync(async (arg1, arg2) => { const dep = require('some-dependency') const flowers = await dep(arg1, arg2.stuff) return flowers }) function myMacro({references, state, babel}) { const flowers = getTheFlowers('...', {stuff: '...'}) // ... more sync stuff } ``` [preval]: https://github.com/kentcdodds/babel-plugin-preval [babel-handbook]: https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md [tester]: https://github.com/babel-utils/babel-plugin-tester [keyword]: https://docs.npmjs.com/files/package.json#keywords [npm-babel-plugin-macros]: https://www.npmjs.com/browse/keyword/babel-plugin-macros [cosmiconfig]: https://www.npmjs.com/package/cosmiconfig ================================================ FILE: other/docs/user.md ================================================ # `babel-plugin-macros` Usage for users > See also: > [the `author` docs](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/author.md). ## Adding the plugin to your config ### Via `.babelrc` (Recommended) **.babelrc** ```json { "plugins": ["macros"] } ``` ### Via [`babel.config.js`](https://babeljs.io/docs/en/configuration#babelconfigjs) **babel.config.js** ```javascript module.exports = function (api) { return { plugins: ['macros'], } } ``` ### Via CLI ```shell babel --plugins babel-plugin-macros script.js ``` ### Via Node API ```js require('babel-core').transform('code', { plugins: ['macros'], }) ``` ## Using a macro With the `babel-plugin-macros` plugin added to your config, we can now use a macro that works with the `babel-plugin-macros` API. Let's assume we have such a module in our project called `eval.macro.js`. To use it, we `import` or `require` the macro module in our code like so: ```javascript import MyEval from './eval.macro' // or const MyEval = require('./eval.macro') ``` Then we use that variable however the documentation for the macro says. Incidentally, `eval.macro.js` actually exists in the tests for `babel-plugin-macros` [here][eval-macro] and you can see how it transforms our code in [the `babel-plugin-macros` snapshots][eval-snapshots]. > Note here that the real benefit is that we don't need to configure anything > for every macro you add. We simply configure `babel-plugin-macros`, then we > can use any macro available. This is part of the benefit of using > `babel-plugin-macros`. [eval-macro]: https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/fixtures/eval.macro.js [eval-snapshots]: https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/__tests__/__snapshots__/index.js.snap ### Using with create-react-app > [Checkout the CRA Macro Example repo](https://github.com/kentcdodds/cra-macro-example) `babel-plugin-macros` ships with `react-scripts` 2.0! This is awesome because it allows for babel to be configured in a nice way without having to eject from `create-react-app`! Before deciding to use this however you should be aware of a few things: 1. Features may be broken or not work as expected 2. Documentation for new features is still sparse, so look through the pull requests for how they're expected to work With that being said you can use all the awesomeness of `babel-plugin-macros` inside `create-react-app` by running one of the following commands based on your situation. ``` $ # Create a new application $ npx create-react-app my-app $ # Upgrade an existing application $ yarn upgrade react-scripts ``` ### config There is a feature that allows you to configure your macro. We use [`cosmiconfig`][cosmiconfig] to read a `babel-plugin-macros` configuration which can be located in any of the following files up the directories from the importing file: - `.babel-plugin-macrosrc` - `.babel-plugin-macrosrc.json` - `.babel-plugin-macrosrc.yaml` - `.babel-plugin-macrosrc.yml` - `.babel-plugin-macrosrc.js` - `babel-plugin-macros.config.js` - `babelMacros` in `package.json` You need to specify your `configName`. EG: For configuring [styled-components macro][styled-components], the `configName` is `"styledComponents"`: ```js // babel-plugin-macros.config.js module.exports = { // ... // Other macros config styledComponents: { pure: true, }, } ``` [cosmiconfig]: https://www.npmjs.com/package/cosmiconfig [styled-components]: https://www.styled-components.com/docs/tooling#babel-macro ================================================ FILE: other/manual-releases.md ================================================ # manual-releases This project has an automated release set up. So things are only released when there are useful changes in the code that justify a release. But sometimes things get messed up one way or another and we need to trigger the release ourselves. When this happens, simply bump the number below and commit that with the following commit message based on your needs: **Major** ``` fix(release): manually release a major version There was an issue with a major release, so this manual-releases.md change is to release a new major version. Reference: # BREAKING CHANGE: ``` **Minor** ``` feat(release): manually release a minor version There was an issue with a minor release, so this manual-releases.md change is to release a new minor version. Reference: # ``` **Patch** ``` fix(release): manually release a patch version There was an issue with a patch release, so this manual-releases.md change is to release a new patch version. Reference: # ``` The number of times we've had to do a manual release is: 0 ================================================ FILE: other/mock-modules/@scope/package/macro.js ================================================ // this is used to make sure that you can require macro from node_modules const {createMacro} = require('../../../src') const innerFn = jest.fn() module.exports = createMacro(innerFn) module.exports.innerFn = innerFn ================================================ FILE: other/mock-modules/babel-plugin-macros-test-error-thrower/macro.js ================================================ // const printAST = require('ast-pretty-print') const {createMacro} = require('../../src') module.exports = createMacro(evalMacro) function evalMacro() { throw new Error('not helpful') } ================================================ FILE: other/mock-modules/babel-plugin-macros-test-error-thrower.macro/index.js ================================================ // const printAST = require('ast-pretty-print') const {createMacro} = require('../../src') module.exports = createMacro(evalMacro) function evalMacro() { throw new Error('not helpful') } ================================================ FILE: other/mock-modules/babel-plugin-macros-test-fake/macro.js ================================================ // this is used to make sure that you can require macro from node_modules const {createMacro} = require('../../src') const innerFn = jest.fn() module.exports = createMacro(innerFn) module.exports.innerFn = innerFn ================================================ FILE: other/mock-modules/babel-plugin-path-replace/index.js ================================================ const types = require('@babel/types') const problematicVisitor = { VariableDeclarator: { enter(path) { const initPath = path.get('init') initPath.replaceWith( types.sequenceExpression([ types.stringLiteral('foobar'), initPath.node, ]), ) }, }, } module.exports = () => ({ visitor: { Program: { enter(path) { path.traverse(problematicVisitor) }, }, }, }) ================================================ FILE: package.json ================================================ { "name": "babel-plugin-macros", "version": "0.0.0-semantically-released", "description": "Allows you to build compile-time libraries", "main": "src/index.js", "scripts": { "lint": "kcd-scripts lint", "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", "test:update": "npm test -- --updateSnapshot --coverage", "validate": "kcd-scripts validate" }, "files": [ "src/index.js" ], "keywords": [ "babel-plugin", "macros", "macro", "babel-macro", "babel-plugin-macro", "babel-macros", "babel-plugin-macros" ], "author": "Kent C. Dodds (https://kentcdodds.com)", "license": "MIT", "dependencies": { "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/parser": "^7.12.7", "@babel/plugin-transform-modules-commonjs": "^7.16.7", "@babel/types": "^7.12.7", "ast-pretty-print": "^2.0.1", "babel-plugin-tester": "^10.0.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", "cpy": "^8.1.1", "kcd-scripts": "^7.1.0" }, "eslintConfig": { "extends": "./node_modules/kcd-scripts/eslint.js" }, "eslintIgnore": [ "node_modules", "coverage", "dist" ], "babel": { "plugins": [ "@babel/transform-modules-commonjs" ] }, "repository": { "type": "git", "url": "https://github.com/kentcdodds/babel-plugin-macros" }, "bugs": { "url": "https://github.com/kentcdodds/babel-plugin-macros/issues" }, "homepage": "https://github.com/kentcdodds/babel-plugin-macros#readme", "engines": { "node": ">=10", "npm": ">=6" } } ================================================ FILE: src/__tests__/__snapshots__/create-macros.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`attempting to create a macros with the configName of options throws an error 1`] = `"You cannot use the configName \\"options\\". It is reserved for babel-plugin-macros."`; exports[`throws error if it is not transpiled 1`] = `"The macro you imported from \\"untranspiled.macro\\" is being executed outside the context of compilation with babel-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"`; ================================================ FILE: src/__tests__/__snapshots__/index.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`unknown plugin Macros are applied in the order respecting plugins order: Macros are applied in the order respecting plugins order 1`] = ` import Wrap from "./fixtures/jsx-id-prefix.macro"; const bar = Wrap(

); ↓ ↓ ↓ ↓ ↓ ↓ const bar = Wrap(

, ) `; exports[`unknown plugin Supports named imports: Supports named imports 1`] = ` import {css as CSS, styled as STYLED} from './fixtures/emotion.macro' const red = CSS\` background-color: red; \` const Div = STYLED.div\` composes: \${red} color: blue; \` ↓ ↓ ↓ ↓ ↓ ↓ const red = 'background-color: red;' const Div = STYLED.div\`composes: background-color: red; color: blue;\` `; exports[`unknown plugin Works as a JSXElement: Works as a JSXElement 1`] = ` import MyEval from './fixtures/eval.macro' const x = 34 + 45 ↓ ↓ ↓ ↓ ↓ ↓ const x = 79 `; 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`] = ` import errorThrower from 'babel-plugin-macros-test-error-thrower/macro' errorThrower('hi') ↓ ↓ ↓ ↓ ↓ ↓ Error: babel-plugin-macros-test-error-thrower/macro: not helpful Learn more: https://www.npmjs.com/package/babel-plugin-macros-test-error-thrower `; exports[`unknown plugin appends the npm URL for errors thrown by node modules: appends the npm URL for errors thrown by node modules 1`] = ` import errorThrower from 'babel-plugin-macros-test-error-thrower.macro' errorThrower('hi') ↓ ↓ ↓ ↓ ↓ ↓ Error: babel-plugin-macros-test-error-thrower.macro: not helpful Learn more: https://www.npmjs.com/package/babel-plugin-macros-test-error-thrower.macro `; exports[`unknown plugin does nothing but remove macros if it is unused: does nothing but remove macros if it is unused 1`] = ` import foo from "./fixtures/eval.macro"; const bar = 42; ↓ ↓ ↓ ↓ ↓ ↓ const bar = 42 `; exports[`unknown plugin forwards MacroErrors thrown by the macro: forwards MacroErrors thrown by the macro 1`] = ` import errorThrower from './fixtures/macro-error-thrower.macro' errorThrower('hey') ↓ ↓ ↓ ↓ ↓ ↓ MacroError: very helpful `; exports[`unknown plugin macros can set their configName and get their config: macros can set their configName and get their config 1`] = ` import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` `; exports[`unknown plugin optionally keep imports (import declaration): optionally keep imports (import declaration) 1`] = ` import macro from './fixtures/keep-imports.macro' const red = macro('noop'); ↓ ↓ ↓ ↓ ↓ ↓ import macro from './fixtures/keep-imports.macro' const red = macro('noop') `; exports[`unknown plugin optionally keep imports (variable assignment): optionally keep imports (variable assignment) 1`] = ` const macro = require('./fixtures/keep-imports.macro') const red = macro('noop'); ↓ ↓ ↓ ↓ ↓ ↓ const macro = require('./fixtures/keep-imports.macro') const red = macro('noop') `; exports[`unknown plugin optionally keep imports in combination with babel-preset-env (#80): optionally keep imports in combination with babel-preset-env (#80) 1`] = ` import macro from './fixtures/keep-imports.macro' const red = macro('noop') ↓ ↓ ↓ ↓ ↓ ↓ 'use strict' var _keepImports = require('./fixtures/keep-imports.macro') var _keepImports2 = _interopRequireDefault(_keepImports) function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {default: obj} } const red = (0, _keepImports2.default)('noop') `; exports[`unknown plugin prepends the relative path for errors thrown by the macro: prepends the relative path for errors thrown by the macro 1`] = ` import errorThrower from './fixtures/error-thrower.macro' errorThrower('hey') ↓ ↓ ↓ ↓ ↓ ↓ Error: ./fixtures/error-thrower.macro: very unhelpful `; exports[`unknown plugin raises an error if macro does not exist: raises an error if macro does not exist 1`] = ` import foo from './some-macros-that-doesnt-even-need-to-exist.macro' export default 'something else' ↓ ↓ ↓ ↓ ↓ ↓ Error: Cannot find module './some-macros-that-doesnt-even-need-to-exist.macro' from '/src/__tests__' `; exports[`unknown plugin supports compiled macros (\`__esModule\` + \`export default\`): supports compiled macros (\`__esModule\` + \`export default\`) 1`] = ` import {css, styled} from './fixtures/emotion-esm.macro' const red = css\` background-color: red; \` const Div = styled.div\` composes: \${red} color: blue; \` ↓ ↓ ↓ ↓ ↓ ↓ const red = css\` background-color: red; \` const Div = styled.div\` composes: \${red} color: blue; \` `; exports[`unknown plugin supports macros from node_modules with scope: supports macros from node_modules with scope 1`] = ` import fakeMacro from '@scope/package/macro' fakeMacro('hi') ↓ ↓ ↓ ↓ ↓ ↓ fakeMacro('hi') `; exports[`unknown plugin supports macros from node_modules: supports macros from node_modules 1`] = ` import fakeMacro from 'babel-plugin-macros-test-fake/macro' fakeMacro('hi') ↓ ↓ ↓ ↓ ↓ ↓ fakeMacro('hi') `; exports[`unknown plugin throws an error if the macro is not properly wrapped: throws an error if the macro is not properly wrapped 1`] = ` import unwrapped from './fixtures/non-wrapped.macro' unwrapped('hey') ↓ ↓ ↓ ↓ ↓ ↓ 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 `; exports[`unknown plugin when a custom isMacrosName option is used on a import: when a custom isMacrosName option is used on a import 1`] = ` import myEval from './fixtures/eval-macro.js' const x = myEval\`34 + 45\` ↓ ↓ ↓ ↓ ↓ ↓ const x = 79 `; exports[`unknown plugin when a custom isMacrosName option is used on a require: when a custom isMacrosName option is used on a require 1`] = ` const evaler = require('./fixtures/eval-macro.js') const x = evaler\`34 + 45\` ↓ ↓ ↓ ↓ ↓ ↓ const x = 79 `; 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`] = ` import myEval from '../eval.macro' const result = myEval\`+('4' + '2')\` global.result = result ↓ ↓ ↓ ↓ ↓ ↓ const result = ('foobar', 42) global.result = result `; exports[`unknown plugin when configuration is specified in plugin options: when configuration is specified in plugin options 1`] = ` import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` `; exports[`unknown plugin when configuration is specified in plugin options: when configuration is specified in plugin options 2`] = ` const configured = require('./configurable.macro') // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` `; exports[`unknown plugin when configuration is specified incorrectly in plugin options: when configuration is specified incorrectly in plugin options 1`] = ` import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` `; exports[`unknown plugin when plugin options configuration cannot be merged with file configuration: when plugin options configuration cannot be merged with file configuration 1`] = ` import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ Error: /src/__tests__/fixtures/primitive-config/babel-plugin-macros.config.js specified a configurableMacro config of type object, but the the macros plugin's options.configurableMacro did contain an object. Both configs must contain objects for their options to be mergeable. `; exports[`unknown plugin when there is an error reading the config, a helpful message is logged 1`] = ` Array [ There was an error trying to load the config "configurableMacro" for the macro imported from "./configurable.macro. Please see the error thrown for more information., ] `; exports[`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`] = ` import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ Error: this is a cosmiconfig error `; 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`] = ` import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` ↓ ↓ ↓ ↓ ↓ ↓ // eslint-disable-next-line babel/no-unused-expressions configured\`stuff\` `; exports[`unknown plugin works with function calls: works with function calls 1`] = ` import myEval from './fixtures/eval.macro' const x = myEval('34 + 45') ↓ ↓ ↓ ↓ ↓ ↓ const x = 79 `; exports[`unknown plugin works with import: works with import 1`] = ` import myEval from './fixtures/eval.macro' const x = myEval\`34 + 45\` ↓ ↓ ↓ ↓ ↓ ↓ const x = 79 `; exports[`unknown plugin works with require destructuring and aliasing: works with require destructuring and aliasing 1`] = ` const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro') const red = CSS\` background-color: red; \` const Div = STYLED.div\` composes: \${red} color: blue; \` ↓ ↓ ↓ ↓ ↓ ↓ const red = 'background-color: red;' const Div = STYLED.div\`composes: background-color: red; color: blue;\` `; exports[`unknown plugin works with require destructuring: works with require destructuring 1`] = ` const {css, styled} = require('./fixtures/emotion.macro') const red = css\` background-color: red; \` const Div = styled.div\` composes: \${red} color: blue; \` ↓ ↓ ↓ ↓ ↓ ↓ const red = 'background-color: red;' const Div = styled.div\`composes: background-color: red; color: blue;\` `; exports[`unknown plugin works with require: works with require 1`] = ` const evaler = require('./fixtures/eval.macro') const x = evaler\`34 + 45\` ↓ ↓ ↓ ↓ ↓ ↓ const x = 79 `; ================================================ FILE: src/__tests__/create-macros.js ================================================ const {createMacro} = require('../') test('throws error if it is not transpiled', () => { const untranspiledMacro = createMacro(() => {}) expect(() => untranspiledMacro({source: 'untranspiled.macro'}), ).toThrowErrorMatchingSnapshot() }) test('attempting to create a macros with the configName of options throws an error', () => { expect(() => createMacro(() => {}, {configName: 'options'}), ).toThrowErrorMatchingSnapshot() }) ================================================ FILE: src/__tests__/fixtures/config/babel-plugin-macros.config.js ================================================ module.exports = { configurableMacro: { fileConfig: true, someConfig: true, }, } ================================================ FILE: src/__tests__/fixtures/config/cjs-code.js ================================================ const configured = require('./configurable.macro') // eslint-disable-next-line babel/no-unused-expressions configured`stuff` ================================================ FILE: src/__tests__/fixtures/config/code.js ================================================ import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured`stuff` ================================================ FILE: src/__tests__/fixtures/config/configurable.macro.js ================================================ const {createMacro} = require('../../..') const configName = 'configurableMacro' const realMacro = jest.fn() module.exports = createMacro(realMacro, {configName}) // for testing purposes only Object.assign(module.exports, { realMacro, configName, }) ================================================ FILE: src/__tests__/fixtures/emotion-esm.macro.js ================================================ const {createMacro} = require('../../') export default createMacro(evalMacro) function evalMacro() { // we're lazy right now // we don't want to eval } ================================================ FILE: src/__tests__/fixtures/emotion.macro.js ================================================ // this is a fake version of emotion // const printAST = require('ast-pretty-print') const {createMacro} = require('../../') module.exports = createMacro(emotionMacro) function emotionMacro({references, babel}) { const {types: t} = babel references.css.forEach(cssRef => { if (cssRef.parentPath.type === 'TaggedTemplateExpression') { cssRef.parentPath.replaceWith( t.stringLiteral(cssRef.parentPath.get('quasi').evaluate().value.trim()), ) } }) references.styled.forEach(styledRef => { if (styledRef.parentPath.parentPath.type === 'TaggedTemplateExpression') { const quasi = styledRef.parentPath.parentPath.get('quasi') const val = quasi.evaluate().value.trim() const replacement = t.templateLiteral( [t.templateElement({raw: val, cooked: val})], [], ) quasi.replaceWith(replacement) } }) } ================================================ FILE: src/__tests__/fixtures/error-thrower.macro.js ================================================ // const printAST = require('ast-pretty-print') const {createMacro} = require('../../') module.exports = createMacro(evalMacro) function evalMacro() { throw new Error('very unhelpful') } ================================================ FILE: src/__tests__/fixtures/eval-macro.js ================================================ module.exports = require('./eval.macro') ================================================ FILE: src/__tests__/fixtures/eval.macro.js ================================================ const {parse} = require('@babel/parser') // const printAST = require('ast-pretty-print') const {createMacro} = require('../../') module.exports = createMacro(evalMacro) function evalMacro({references, state}) { references.default.forEach(referencePath => { if (referencePath.parentPath.type === 'TaggedTemplateExpression') { asTag(referencePath.parentPath.get('quasi'), state) } else if (referencePath.parentPath.type === 'CallExpression') { asFunction(referencePath.parentPath.get('arguments'), state) } else if (referencePath.parentPath.type === 'JSXOpeningElement') { asJSX( { attributes: referencePath.parentPath.get('attributes'), children: referencePath.parentPath.parentPath.get('children'), }, state, ) } else { // TODO: throw a helpful error message } }) } function asTag(quasiPath) { const value = quasiPath.parentPath.get('quasi').evaluate().value quasiPath.parentPath.replaceWith(evalToAST(value)) } function asFunction(argumentsPaths) { const value = argumentsPaths[0].evaluate().value argumentsPaths[0].parentPath.replaceWith(evalToAST(value)) } // eslint-disable-next-line no-unused-vars function asJSX({attributes, children}) { // It's a shame you cannot use evaluate() with JSX const value = children[0].node.value children[0].parentPath.replaceWith(evalToAST(value)) } function evalToAST(value) { let x // eslint-disable-next-line eval(`x = ${value}`) return thingToAST(x) } function thingToAST(object) { const fileNode = parse(`var x = ${JSON.stringify(object)}`) return fileNode.program.body[0].declarations[0].init } ================================================ FILE: src/__tests__/fixtures/jsx-id-prefix.macro.js ================================================ // adds "prefix-" to each `id` attribute const {createMacro} = require('../../') module.exports = createMacro(wrapWidget) function wrapWidget({references, babel}) { const {types: t} = babel references.default.forEach(wrap => { wrap.parentPath.traverse({ JSXAttribute(path) { const name = path.get('name') if (t.isJSXIdentifier(name) && name.node.name === 'id') { const value = path.get('value') if (t.isStringLiteral(value)) value.replaceWith(t.stringLiteral(`macro-${value.node.value}`)) } }, }) }) } ================================================ FILE: src/__tests__/fixtures/jsx-id-prefix.plugin.js ================================================ // babel-plugin adding `plugin-` prefix to each "id" JSX attribute module.exports = main function main({types: t}) { return { visitor: { // intentionally traversing from Program, // if it matches JSXAttribute here the issue won't be reproduced Program(progPath) { progPath.traverse({ JSXAttribute(path) { const name = path.get('name') if (t.isJSXIdentifier(name) && name.node.name === 'id') { const value = path.get('value') if (t.isStringLiteral(value)) value.replaceWith(t.stringLiteral(`plugin-${value.node.value}`)) } }, }) }, }, } } ================================================ FILE: src/__tests__/fixtures/keep-imports.macro.js ================================================ const {createMacro} = require('../../') module.exports = createMacro(keepImportMacro) function keepImportMacro() { return {keepImports: true} } ================================================ FILE: src/__tests__/fixtures/macro-error-thrower.macro.js ================================================ // const printAST = require('ast-pretty-print') const {createMacro, MacroError} = require('../../') module.exports = createMacro(evalMacro) function evalMacro() { throw new MacroError('very helpful') } ================================================ FILE: src/__tests__/fixtures/non-wrapped.macro.js ================================================ module.exports = () => {} ================================================ FILE: src/__tests__/fixtures/path-replace-issue/.babelrc ================================================ { "plugins": ["path-replace"] } ================================================ FILE: src/__tests__/fixtures/path-replace-issue/variable-assignment.js ================================================ import myEval from '../eval.macro' const result = myEval`+('4' + '2')` global.result = result ================================================ FILE: src/__tests__/fixtures/primitive-config/babel-plugin-macros.config.js ================================================ module.exports = { configurableMacro: 4, } ================================================ FILE: src/__tests__/fixtures/primitive-config/code.js ================================================ import configured from './configurable.macro' // eslint-disable-next-line babel/no-unused-expressions configured`stuff` ================================================ FILE: src/__tests__/fixtures/primitive-config/configurable.macro.js ================================================ const {createMacro} = require('../../..') const configName = 'configurableMacro' const realMacro = jest.fn() module.exports = createMacro(realMacro, {configName}) // for testing purposes only Object.assign(module.exports, { realMacro, configName, }) ================================================ FILE: src/__tests__/index.js ================================================ import path from 'path' import {cosmiconfigSync as cosmiconfigSyncMock} from 'cosmiconfig' import cpy from 'cpy' import babel from '@babel/core' import pluginTester from 'babel-plugin-tester' import plugin from '../' const projectRoot = path.join(__dirname, '../../') jest.mock('cosmiconfig', () => { const cosmiconfigExports = jest.requireActual('cosmiconfig') const actualCosmiconfigSync = cosmiconfigExports.cosmiconfigSync function fakeCosmiconfigSync(...args) { fakeCosmiconfigSync.explorer = actualCosmiconfigSync(...args) return fakeCosmiconfigSync.explorer } return {...cosmiconfigExports, cosmiconfigSync: fakeCosmiconfigSync} }) beforeAll(() => { // copy our mock modules to the node_modules directory // so we can test how things work when importing a macro // from the node_modules directory. return cpy(['**/*.js'], path.join('..', '..', 'node_modules'), { parents: true, cwd: path.join(projectRoot, 'other', 'mock-modules'), }) }) beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { console.error.mockRestore() jest.clearAllMocks() }) expect.addSnapshotSerializer({ print(val) { return ( val .split(projectRoot) .join('/') .replace(/\\/g, '/') // Remove the path of file which thrown an error .replace(/Error:[^:]*:/, 'Error:') ) }, test(val) { return typeof val === 'string' }, }) pluginTester({ plugin, snapshot: true, babelOptions: { filename: __filename, parserOpts: { plugins: ['jsx'], }, generatorOpts: {quotes: 'double'}, }, tests: [ { title: 'does nothing to code that does not import macro', snapshot: false, code: ` import foo from './some-file-without-macro' const bar = require('./some-other-file-without-macro') `, }, { title: 'does nothing but remove macros if it is unused', snapshot: true, code: ` import foo from "./fixtures/eval.macro"; const bar = 42; `, }, { title: 'raises an error if macro does not exist', error: true, code: ` import foo from './some-macros-that-doesnt-even-need-to-exist.macro' export default 'something else' `, }, { title: 'works with import', code: ` import myEval from './fixtures/eval.macro' const x = myEval\`34 + 45\` `, }, { title: 'works with require', code: ` const evaler = require('./fixtures/eval.macro') const x = evaler\`34 + 45\` `, }, { title: 'works with require destructuring', code: ` const {css, styled} = require('./fixtures/emotion.macro') const red = css\` background-color: red; \` const Div = styled.div\` composes: \${red} color: blue; \` `, }, { title: 'works with require destructuring and aliasing', code: ` const {css: CSS, styled: STYLED} = require('./fixtures/emotion.macro') const red = CSS\` background-color: red; \` const Div = STYLED.div\` composes: \${red} color: blue; \` `, }, { title: 'works with function calls', code: ` import myEval from './fixtures/eval.macro' const x = myEval('34 + 45') `, }, { title: 'Works as a JSXElement', code: ` import MyEval from './fixtures/eval.macro' const x = 34 + 45 `, }, { title: 'Supports named imports', code: ` import {css as CSS, styled as STYLED} from './fixtures/emotion.macro' const red = CSS\` background-color: red; \` const Div = STYLED.div\` composes: \${red} color: blue; \` `, }, { title: 'supports compiled macros (`__esModule` + `export default`)', code: ` import {css, styled} from './fixtures/emotion-esm.macro' const red = css\` background-color: red; \` const Div = styled.div\` composes: \${red} color: blue; \` `, }, { title: 'supports macros from node_modules', code: ` import fakeMacro from 'babel-plugin-macros-test-fake/macro' fakeMacro('hi') `, teardown() { try { // kinda abusing the babel-plugin-tester API here // to make an extra assertion // eslint-disable-next-line const fakeMacro = require('babel-plugin-macros-test-fake/macro') expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1) expect(fakeMacro.innerFn).toHaveBeenCalledWith({ references: expect.any(Object), source: expect.stringContaining( 'babel-plugin-macros-test-fake/macro', ), state: expect.any(Object), babel: expect.any(Object), isBabelMacrosCall: true, }) expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel) } catch (e) { console.error(e) throw e } }, }, { title: 'supports macros from node_modules with scope', code: ` import fakeMacro from '@scope/package/macro' fakeMacro('hi') `, teardown() { try { // kinda abusing the babel-plugin-tester API here // to make an extra assertion // eslint-disable-next-line const fakeMacro = require('@scope/package/macro') expect(fakeMacro.innerFn).toHaveBeenCalledTimes(1) expect(fakeMacro.innerFn).toHaveBeenCalledWith({ references: expect.any(Object), source: expect.stringContaining('@scope/package/macro'), state: expect.any(Object), babel: expect.any(Object), isBabelMacrosCall: true, }) expect(fakeMacro.innerFn.mock.calls[0].babel).toBe(babel) } catch (e) { console.error(e) throw e } }, }, { title: 'optionally keep imports (variable assignment)', code: ` const macro = require('./fixtures/keep-imports.macro') const red = macro('noop'); `, }, { title: 'optionally keep imports (import declaration)', code: ` import macro from './fixtures/keep-imports.macro' const red = macro('noop'); `, }, { title: 'optionally keep imports in combination with babel-preset-env (#80)', code: ` import macro from './fixtures/keep-imports.macro' const red = macro('noop') `, babelOptions: { plugins: [ require.resolve('babel-plugin-transform-es2015-modules-commonjs'), ], }, }, { title: 'throws an error if the macro is not properly wrapped', error: true, code: ` import unwrapped from './fixtures/non-wrapped.macro' unwrapped('hey') `, }, { title: 'forwards MacroErrors thrown by the macro', error: true, code: ` import errorThrower from './fixtures/macro-error-thrower.macro' errorThrower('hey') `, }, { title: 'prepends the relative path for errors thrown by the macro', error: true, code: ` import errorThrower from './fixtures/error-thrower.macro' errorThrower('hey') `, }, { title: 'appends the npm URL for errors thrown by node modules', error: true, code: ` import errorThrower from 'babel-plugin-macros-test-error-thrower.macro' errorThrower('hi') `, }, { title: 'appends the npm URL for errors thrown by node modules with a slash', error: true, code: ` import errorThrower from 'babel-plugin-macros-test-error-thrower/macro' errorThrower('hi') `, }, { title: 'macros can set their configName and get their config', fixture: path.join(__dirname, 'fixtures/config/code.js'), teardown() { try { const babelMacrosConfig = require('./fixtures/config/babel-plugin-macros.config') const configurableMacro = require('./fixtures/config/configurable.macro') expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual( babelMacrosConfig[configurableMacro.configName], ) configurableMacro.realMacro.mockClear() } catch (e) { console.error(e) throw e } }, }, { title: 'when there is an error reading the config, a helpful message is logged', error: true, fixture: path.join(__dirname, 'fixtures/config/code.js'), setup() { jest .spyOn(cosmiconfigSyncMock.explorer, 'search') .mockImplementationOnce(() => { throw new Error('this is a cosmiconfig error') }) jest.spyOn(console, 'error').mockImplementationOnce(() => {}) return function teardown() { try { expect(console.error).toHaveBeenCalledTimes(1) expect(console.error.mock.calls[0]).toMatchSnapshot() console.error.mockClear() } catch (e) { console.error(e) console.error.mockClear() throw e } } }, }, { title: 'when there is no config to load, then no config is passed', fixture: path.join(__dirname, 'fixtures/config/code.js'), setup() { jest .spyOn(cosmiconfigSyncMock.explorer, 'search') .mockImplementationOnce(() => { return null }) return function teardown() { try { const configurableMacro = require('./fixtures/config/configurable.macro') expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual( {}, ) configurableMacro.realMacro.mockClear() } catch (e) { console.error(e) throw e } } }, }, { title: 'when configuration is specified in plugin options', pluginOptions: { configurableMacro: { someConfig: false, somePluginConfig: true, }, }, fixture: path.join(__dirname, 'fixtures/config/code.js'), teardown() { try { const configurableMacro = require('./fixtures/config/configurable.macro') expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({ fileConfig: true, someConfig: true, somePluginConfig: true, }) configurableMacro.realMacro.mockClear() } catch (e) { console.error(e) throw e } }, }, { title: 'when configuration is specified in plugin options', pluginOptions: { configurableMacro: { someConfig: false, somePluginConfig: true, }, }, fixture: path.join(__dirname, 'fixtures/config/cjs-code.js'), teardown() { try { const configurableMacro = require('./fixtures/config/configurable.macro') expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) expect(configurableMacro.realMacro.mock.calls[0][0].config).toEqual({ fileConfig: true, someConfig: true, somePluginConfig: true, }) configurableMacro.realMacro.mockClear() } catch (e) { console.error(e) throw e } }, }, { title: 'when configuration is specified incorrectly in plugin options', fixture: path.join(__dirname, 'fixtures/config/code.js'), pluginOptions: { configurableMacro: 2, }, teardown() { try { const configurableMacro = require('./fixtures/config/configurable.macro') expect(configurableMacro.realMacro).toHaveBeenCalledTimes(1) expect(configurableMacro.realMacro).not.toHaveBeenCalledWith( expect.objectContaining({ config: expect.any, }), ) configurableMacro.realMacro.mockClear() } catch (e) { console.error(e) throw e } }, }, { title: 'when a custom isMacrosName option is used on a import', pluginOptions: { isMacrosName(v) { return v.endsWith('-macro.js') }, }, code: ` import myEval from './fixtures/eval-macro.js' const x = myEval\`34 + 45\` `, }, { title: 'when a custom isMacrosName option is used on a require', pluginOptions: { isMacrosName(v) { return v.endsWith('-macro.js') }, }, code: ` const evaler = require('./fixtures/eval-macro.js') const x = evaler\`34 + 45\` `, }, { title: 'when plugin options configuration cannot be merged with file configuration', error: true, fixture: path.join(__dirname, 'fixtures/primitive-config/code.js'), pluginOptions: { configurableMacro: {}, }, }, { title: 'when a plugin that replaces paths is used, macros still work properly', fixture: path.join( __dirname, 'fixtures/path-replace-issue/variable-assignment.js', ), babelOptions: { babelrc: true, }, }, { title: 'Macros are applied in the order respecting plugins order', code: ` import Wrap from "./fixtures/jsx-id-prefix.macro"; const bar = Wrap(

); `, babelOptions: { presets: [{plugins: [require('./fixtures/jsx-id-prefix.plugin')]}], }, }, ], }) /* eslint no-console:0 */ ================================================ FILE: src/index.js ================================================ const p = require('path') const resolve = require('resolve') // const printAST = require('ast-pretty-print') const macrosRegex = /[./]macro(\.c?js)?$/ const testMacrosRegex = v => macrosRegex.test(v) // https://stackoverflow.com/a/32749533/971592 class MacroError extends Error { constructor(message) { super(message) this.name = 'MacroError' /* istanbul ignore else */ if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor) } else if (!this.stack) { this.stack = new Error(message).stack } } } let _configExplorer = null function getConfigExplorer() { return (_configExplorer = _configExplorer || // Lazy load cosmiconfig since it is a relatively large bundle require('cosmiconfig').cosmiconfigSync('babel-plugin-macros', { searchPlaces: [ 'package.json', '.babel-plugin-macrosrc', '.babel-plugin-macrosrc.json', '.babel-plugin-macrosrc.yaml', '.babel-plugin-macrosrc.yml', '.babel-plugin-macrosrc.js', 'babel-plugin-macros.config.js', ], packageProp: 'babelMacros', })) } function createMacro(macro, options = {}) { if (options.configName === 'options') { throw new Error( `You cannot use the configName "options". It is reserved for babel-plugin-macros.`, ) } macroWrapper.isBabelMacro = true macroWrapper.options = options return macroWrapper function macroWrapper(args) { const {source, isBabelMacrosCall} = args if (!isBabelMacrosCall) { throw new MacroError( `The macro you imported from "${source}" is being executed outside the context of compilation with babel-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', ) } return macro(args) } } function nodeResolvePath(source, basedir) { return resolve.sync(source, { basedir, extensions: ['.js', '.ts', '.tsx', '.mjs', '.cjs', '.jsx'], // This is here to support the package being globally installed // read more: https://github.com/kentcdodds/babel-plugin-macros/pull/138 paths: [p.resolve(__dirname, '../../')], }) } function macrosPlugin( babel, // istanbul doesn't like the default of an object for the plugin options // but I think older versions of babel didn't always pass options // istanbul ignore next { require: _require = require, resolvePath = nodeResolvePath, isMacrosName = testMacrosRegex, ...options } = {}, ) { function interopRequire(path) { // eslint-disable-next-line import/no-dynamic-require const o = _require(path) return o && o.__esModule && o.default ? o.default : o } return { name: 'macros', visitor: { Program(progPath, state) { progPath.traverse({ ImportDeclaration(path) { const isMacros = looksLike(path, { node: { source: { value: v => isMacrosName(v), }, }, }) if (!isMacros) { return } const imports = path.node.specifiers.map(s => ({ localName: s.local.name, importedName: s.type === 'ImportDefaultSpecifier' ? 'default' : s.imported.name, })) const source = path.node.source.value const result = applyMacros({ path, imports, source, state, babel, interopRequire, resolvePath, options, }) if (!result || !result.keepImports) { path.remove() } }, VariableDeclaration(path) { const isMacros = child => looksLike(child, { node: { init: { callee: { type: 'Identifier', name: 'require', }, arguments: args => args.length === 1 && isMacrosName(args[0].value), }, }, }) path .get('declarations') .filter(isMacros) .forEach(child => { const imports = child.node.id.name ? [{localName: child.node.id.name, importedName: 'default'}] : child.node.id.properties.map(property => ({ localName: property.value.name, importedName: property.key.name, })) const call = child.get('init') const source = call.node.arguments[0].value const result = applyMacros({ path: call, imports, source, state, babel, interopRequire, resolvePath, options, }) if (!result || !result.keepImports) { child.remove() } }) }, }) }, }, } } // eslint-disable-next-line complexity function applyMacros({ path, imports, source, state, babel, interopRequire, resolvePath, options, }) { /* istanbul ignore next (pretty much only useful for astexplorer I think) */ const { file: { opts: {filename = ''}, }, } = state let hasReferences = false const referencePathsByImportName = imports.reduce( (byName, {importedName, localName}) => { const binding = path.scope.getBinding(localName) byName[importedName] = binding.referencePaths hasReferences = hasReferences || Boolean(byName[importedName].length) return byName }, {}, ) const isRelative = source.indexOf('.') === 0 const requirePath = resolvePath(source, p.dirname(getFullFilename(filename))) const macro = interopRequire(requirePath) if (!macro.isBabelMacro) { throw new Error( `The macro imported from "${source}" 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`, ) } const config = getConfig(macro, filename, source, options) let result try { /** * Other plugins that run before babel-plugin-macros might use path.replace, where a path is * put into its own replacement. Apparently babel does not update the scope after such * an operation. As a remedy, the whole scope is traversed again with an empty "Identifier" * visitor - this makes the problem go away. * * See: https://github.com/kentcdodds/import-all.macro/issues/7 */ state.file.scope.path.traverse({ Identifier() {}, }) result = macro({ references: referencePathsByImportName, source, state, babel, config, isBabelMacrosCall: true, }) } catch (error) { if (error.name === 'MacroError') { throw error } error.message = `${source}: ${error.message}` if (!isRelative) { error.message = `${ error.message } Learn more: https://www.npmjs.com/package/${source.replace( // remove everything after package name // @org/package/macro -> @org/package // package/macro -> package /^((?:@[^/]+\/)?[^/]+).*/, '$1', )}` } throw error } return result } function getConfigFromFile(configName, filename) { try { const loaded = getConfigExplorer().search(filename) if (loaded) { return { options: loaded.config[configName], path: loaded.filepath, } } } catch (e) { return {error: e} } return {} } function getConfigFromOptions(configName, options) { if (options.hasOwnProperty(configName)) { if (options[configName] && typeof options[configName] !== 'object') { // eslint-disable-next-line no-console console.error( `The macro plugin options' ${configName} property was not an object or null.`, ) } else { return {options: options[configName]} } } return {} } function getConfig(macro, filename, source, options) { const {configName} = macro.options if (configName) { const fileConfig = getConfigFromFile(configName, filename) const optionsConfig = getConfigFromOptions(configName, options) if ( optionsConfig.options === undefined && fileConfig.options === undefined && fileConfig.error !== undefined ) { // eslint-disable-next-line no-console console.error( `There was an error trying to load the config "${configName}" ` + `for the macro imported from "${source}. ` + `Please see the error thrown for more information.`, ) throw fileConfig.error } if ( fileConfig.options !== undefined && optionsConfig.options !== undefined && typeof fileConfig.options !== 'object' ) { throw new Error( `${fileConfig.path} specified a ${configName} config of type ` + `${typeof optionsConfig.options}, but the the macros plugin's ` + `options.${configName} did contain an object. Both configs must ` + `contain objects for their options to be mergeable.`, ) } return { ...optionsConfig.options, ...fileConfig.options, } } return undefined } /* istanbul ignore next because this is hard to test and not worth it... */ function getFullFilename(filename) { if (p.isAbsolute(filename)) { return filename } return p.join(process.cwd(), filename) } function looksLike(a, b) { return ( a && b && Object.keys(b).every(bKey => { const bVal = b[bKey] const aVal = a[bKey] if (typeof bVal === 'function') { return bVal(aVal) } return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal) }) ) } function isPrimitive(val) { // eslint-disable-next-line return val == null || /^[sbn]/.test(typeof val) } module.exports = macrosPlugin Object.assign(module.exports, { createMacro, MacroError, })