├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── client ├── index.d.ts ├── index.js ├── jsx.d.ts ├── jsx.js ├── package.json ├── react.d.ts └── react.js ├── other ├── 150.png ├── MAINTAINING.md ├── USERS.md ├── cjs-ify.js ├── manual-releases.md ├── sample-component.jsx └── sample.mdx ├── package.json ├── src ├── __tests__ │ ├── hono.js │ ├── index.js │ ├── preact.js │ ├── setup-tests.js │ └── vue.js ├── client │ ├── index.js │ ├── jsx.js │ └── react.js ├── dirname-messed-up.cjs ├── dirname-messed-up.cjs.d.ts ├── index.js └── types.d.ts └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "mdx-bundler", 3 | "projectOwner": "kentcdodds", 4 | "imageSize": 100, 5 | "commit": false, 6 | "contributorsPerLine": 7, 7 | "repoHost": "https://github.com", 8 | "repoType": "github", 9 | "skipCi": false, 10 | "files": [ 11 | "README.md" 12 | ], 13 | "contributors": [ 14 | { 15 | "login": "kentcdodds", 16 | "name": "Kent C. Dodds", 17 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 18 | "profile": "https://kentcdodds.com", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "infra", 23 | "test" 24 | ] 25 | }, 26 | { 27 | "login": "benwis", 28 | "name": "benwis", 29 | "avatar_url": "https://avatars.githubusercontent.com/u/6953353?v=4", 30 | "profile": "https://github.com/benwis", 31 | "contributions": [ 32 | "bug", 33 | "review" 34 | ] 35 | }, 36 | { 37 | "login": "Arcath", 38 | "name": "Adam Laycock", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/19609?v=4", 40 | "profile": "https://www.arcath.net", 41 | "contributions": [ 42 | "code", 43 | "test", 44 | "ideas", 45 | "review", 46 | "doc" 47 | ] 48 | }, 49 | { 50 | "login": "wooorm", 51 | "name": "Titus", 52 | "avatar_url": "https://avatars.githubusercontent.com/u/944406?v=4", 53 | "profile": "http://wooorm.com", 54 | "contributions": [ 55 | "ideas", 56 | "review", 57 | "code" 58 | ] 59 | }, 60 | { 61 | "login": "ChristianMurphy", 62 | "name": "Christian Murphy", 63 | "avatar_url": "https://avatars.githubusercontent.com/u/3107513?v=4", 64 | "profile": "https://github.com/ChristianMurphy", 65 | "contributions": [ 66 | "ideas" 67 | ] 68 | }, 69 | { 70 | "login": "peduarte", 71 | "name": "Pedro Duarte", 72 | "avatar_url": "https://avatars.githubusercontent.com/u/372831?v=4", 73 | "profile": "https://ped.ro", 74 | "contributions": [ 75 | "doc" 76 | ] 77 | }, 78 | { 79 | "login": "erikras", 80 | "name": "Erik Rasmussen", 81 | "avatar_url": "https://avatars.githubusercontent.com/u/4396759?v=4", 82 | "profile": "https://keybase.io/erikras", 83 | "contributions": [ 84 | "doc" 85 | ] 86 | }, 87 | { 88 | "login": "ozyxdev", 89 | "name": "Omar Syx", 90 | "avatar_url": "https://avatars.githubusercontent.com/u/83309085?v=4", 91 | "profile": "https://github.com/ozyxdev", 92 | "contributions": [ 93 | "bug" 94 | ] 95 | }, 96 | { 97 | "login": "gaelhameon", 98 | "name": "Gaël Haméon", 99 | "avatar_url": "https://avatars.githubusercontent.com/u/17253950?v=4", 100 | "profile": "https://github.com/gaelhameon", 101 | "contributions": [ 102 | "doc" 103 | ] 104 | }, 105 | { 106 | "login": "loiacon", 107 | "name": "Gabriel Loiácono", 108 | "avatar_url": "https://avatars.githubusercontent.com/u/32134586?v=4", 109 | "profile": "https://github.com/loiacon", 110 | "contributions": [ 111 | "code", 112 | "test" 113 | ] 114 | }, 115 | { 116 | "login": "skovy", 117 | "name": "Spencer Miskoviak", 118 | "avatar_url": "https://avatars.githubusercontent.com/u/5247455?v=4", 119 | "profile": "https://skovy.dev", 120 | "contributions": [ 121 | "doc" 122 | ] 123 | }, 124 | { 125 | "login": "Dev-CasperTheGhost", 126 | "name": "Casper", 127 | "avatar_url": "https://avatars.githubusercontent.com/u/53900565?v=4", 128 | "profile": "https://caspertheghost.me", 129 | "contributions": [ 130 | "code" 131 | ] 132 | }, 133 | { 134 | "login": "a7sc11u", 135 | "name": "Apostolos Christodoulou", 136 | "avatar_url": "https://avatars.githubusercontent.com/u/803868?v=4", 137 | "profile": "http://a7sc11u.dev", 138 | "contributions": [ 139 | "doc" 140 | ] 141 | }, 142 | { 143 | "login": "yordis", 144 | "name": "Yordis Prieto", 145 | "avatar_url": "https://avatars.githubusercontent.com/u/4237280?v=4", 146 | "profile": "https://github.com/yordis", 147 | "contributions": [ 148 | "code" 149 | ] 150 | }, 151 | { 152 | "login": "xoumi", 153 | "name": "xoumi", 154 | "avatar_url": "https://avatars.githubusercontent.com/u/24864287?v=4", 155 | "profile": "https://github.com/xoumi", 156 | "contributions": [ 157 | "code" 158 | ] 159 | }, 160 | { 161 | "login": "yasinmiran", 162 | "name": "Yasin", 163 | "avatar_url": "https://avatars.githubusercontent.com/u/25561152?v=4", 164 | "profile": "http://yasint.dev", 165 | "contributions": [ 166 | "code" 167 | ] 168 | }, 169 | { 170 | "login": "moniac", 171 | "name": "Mohammed 'Mo' Mulazada", 172 | "avatar_url": "https://avatars.githubusercontent.com/u/22095656?v=4", 173 | "profile": "https://moweb.dev", 174 | "contributions": [ 175 | "doc" 176 | ] 177 | }, 178 | { 179 | "login": "CanRau", 180 | "name": "Can Rau", 181 | "avatar_url": "https://avatars.githubusercontent.com/u/5196971?v=4", 182 | "profile": "https://www.canrau.com", 183 | "contributions": [ 184 | "doc" 185 | ] 186 | }, 187 | { 188 | "login": "HOSENUR", 189 | "name": "Hosenur Rahaman", 190 | "avatar_url": "https://avatars.githubusercontent.com/u/50978981?v=4", 191 | "profile": "http://hosenur.dev", 192 | "contributions": [ 193 | "doc" 194 | ] 195 | }, 196 | { 197 | "login": "sitek94", 198 | "name": "Maciek Sitkowski", 199 | "avatar_url": "https://avatars.githubusercontent.com/u/58401630?v=4", 200 | "profile": "https://macieksitkowski.com", 201 | "contributions": [ 202 | "doc" 203 | ] 204 | }, 205 | { 206 | "login": "priyang12", 207 | "name": "Priyang", 208 | "avatar_url": "https://avatars.githubusercontent.com/u/72823974?v=4", 209 | "profile": "https://github.com/priyang12", 210 | "contributions": [ 211 | "code", 212 | "doc" 213 | ] 214 | }, 215 | { 216 | "login": "theMosaad", 217 | "name": "Mosaad", 218 | "avatar_url": "https://avatars.githubusercontent.com/u/48773133?v=4", 219 | "profile": "https://github.com/theMosaad", 220 | "contributions": [ 221 | "doc" 222 | ] 223 | }, 224 | { 225 | "login": "stefanprobst", 226 | "name": "stefanprobst", 227 | "avatar_url": "https://avatars.githubusercontent.com/u/20753323?v=4", 228 | "profile": "https://github.com/stefanprobst", 229 | "contributions": [ 230 | "code", 231 | "test" 232 | ] 233 | }, 234 | { 235 | "login": "vladmoroz", 236 | "name": "Vlad Moroz", 237 | "avatar_url": "https://avatars.githubusercontent.com/u/8441036?v=4", 238 | "profile": "https://vladmoroz.com", 239 | "contributions": [ 240 | "code" 241 | ] 242 | } 243 | ], 244 | "commitConvention": "none" 245 | } 246 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `mdx-bundler` version: 15 | - `node` version: 16 | - `npm` version: 17 | 18 | Relevant code or config 19 | 20 | ```js 21 | 22 | ``` 23 | 24 | What you did: 25 | 26 | What happened: 27 | 28 | 29 | 30 | Reproduction repository: 31 | 32 | 36 | 37 | Problem description: 38 | 39 | Suggested solution: 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | 31 | **Checklist**: 32 | 33 | 34 | 35 | 36 | - [ ] Documentation 37 | - [ ] Tests 38 | - [ ] Ready to be merged 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - '+([0-9])?(.{+([0-9]),x}).x' 6 | - 'main' 7 | - 'next' 8 | - 'next-major' 9 | - 'beta' 10 | - 'alpha' 11 | - '!all-contributors/**' 12 | pull_request: 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | main: 20 | # ignore all-contributors PRs 21 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 22 | strategy: 23 | matrix: 24 | node: [18, 20] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: ⬇️ Checkout repo 28 | uses: actions/checkout@v2 29 | 30 | - name: ⎔ Setup node 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node }} 34 | 35 | - name: 📥 Download deps 36 | uses: bahmutov/npm-install@v1 37 | with: 38 | useLockFile: false 39 | env: 40 | HUSKY_SKIP_INSTALL: true 41 | 42 | - name: ▶️ Run validate script 43 | run: npm run validate 44 | 45 | - name: ⬆️ Upload coverage report 46 | uses: codecov/codecov-action@v1 47 | 48 | release: 49 | needs: main 50 | runs-on: ubuntu-latest 51 | if: 52 | ${{ github.repository == 'kentcdodds/mdx-bundler' && 53 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', 54 | github.ref) && github.event_name == 'push' }} 55 | steps: 56 | - name: ⬇️ Checkout repo 57 | uses: actions/checkout@v2 58 | 59 | - name: ⎔ Setup node 60 | uses: actions/setup-node@v2 61 | with: 62 | node-version: 20 63 | 64 | - name: 📥 Download deps 65 | uses: bahmutov/npm-install@v1 66 | with: 67 | useLockFile: false 68 | env: 69 | HUSKY_SKIP_INSTALL: true 70 | 71 | - name: 🏗 Run build script 72 | run: npm run build 73 | 74 | - name: 🚀 Release 75 | uses: cycjimmy/semantic-release-action@v2 76 | with: 77 | semantic_version: 17 78 | branches: | 79 | [ 80 | '+([0-9])?(.{+([0-9]),x}).x', 81 | 'main', 82 | 'next', 83 | 'next-major', 84 | {name: 'beta', prerelease: true}, 85 | {name: 'alpha', prerelease: true} 86 | ] 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | output/ 6 | 7 | # these cause more harm than good 8 | # when working with contributors 9 | package-lock.json 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make pull 15 | > requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/kentcdodds/mdx-bundler 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `main` branch 25 | > to use the upstream main branch whenever you run `git pull`. Then you can make 26 | > all of your pull request branches based on this `main` branch. Whenever you 27 | > want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Committing and Pushing changes 30 | 31 | Please make sure to run the tests before you commit your changes. You can run 32 | `npm run test` which will update any snapshots that need updating. Make sure to 33 | include those changes (if they exist) in your commit. 34 | 35 | ## Help needed 36 | 37 | Please checkout [the open issues][issues] 38 | 39 | Also, please watch the repo and respond to questions/bug reports/feature 40 | requests! Thanks! 41 | 42 | 43 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 44 | [issues]: https://github.com/kentcdodds/mdx-bundler/issues 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

mdx-bundler 🦤

3 | 4 |

Compile and bundle your MDX files and their dependencies. FAST.

5 |
6 | 7 | --- 8 | 9 | 10 | [![Build Status][build-badge]][build] 11 | [![Code Coverage][coverage-badge]][coverage] 12 | [![version][version-badge]][package] 13 | [![downloads][downloads-badge]][npmtrends] 14 | [![MIT License][license-badge]][license] 15 | [![All Contributors][all-contributors-badge]](#contributors-) 16 | [![PRs Welcome][prs-badge]][prs] 17 | [![Code of Conduct][coc-badge]][coc] 18 | 19 | 20 | ## The problem 21 | 22 | You have a string of MDX and various TS/JS files that it uses and you want to 23 | get a bundled version of these files to eval in the browser. 24 | 25 | ## This solution 26 | 27 | This is an async function that will compile and bundle your MDX files and their 28 | dependencies. It uses [MDX v3](https://mdxjs.com/blog/v3/) and 29 | [esbuild](https://esbuild.github.io/), so it's VERY fast and supports TypeScript 30 | files (for the dependencies of your MDX files). 31 | 32 | Your source files could be local, in a remote github repo, in a CMS, or wherever 33 | else and it doesn't matter. All `mdx-bundler` cares about is that you pass it 34 | all the files and source code necessary and it will take care of bundling 35 | everything for you. 36 | 37 | ### FAQ: 38 | 39 |
40 | 41 | 42 | "What's so cool about MDX?" 43 | 44 | 45 | 46 | [MDX](https://mdxjs.com/) enables you to combine terse markdown syntax for your 47 | content with the power of JSX components. For content-heavy sites, writing the 48 | content with straight-up HTML can be annoyingly verbose. Often people solve this 49 | using a WSYWIG editor, but too often those fall short in mapping the writer's 50 | intent to HTML. Many people prefer using markdown to express their content 51 | source and have that parsed into HTML to be rendered. 52 | 53 | The problem with using Markdown for your content is if you want to have some 54 | interactivity embedded into your content, you're pretty limited. You either need 55 | to insert an element that JavaScript targets (which is annoyingly indirect), or 56 | you can use an `iframe` or something. 57 | 58 | As previously stated, [MDX](https://mdxjs.com/) enables you to combine terse 59 | markdown syntax for your content with the power of JSX components. So you can 60 | import a JSX component and render it within the markdown itself. It's the best 61 | of both worlds. 62 | 63 |
64 | 65 |
66 | 67 | 68 | "How is this different from next-mdx-remote?" 69 | 70 | 71 | 72 | `mdx-bundler` actually bundles dependencies of your MDX files. For example, this 73 | won't work with `next-mdx-remote`, but it will with `mdx-bundler`: 74 | 75 | ```md 76 | --- 77 | title: Example Post 78 | published: 2021-02-13 79 | description: This is some description 80 | --- 81 | 82 | # Wahoo 83 | 84 | import Demo from './demo' 85 | 86 | Here's a **neat** demo: 87 | 88 | 89 | ``` 90 | 91 | `next-mdx-remote` chokes on that import because it's not a bundler, it's just a 92 | compiler. `mdx-bundler` is an MDX compiler and bundler. That's the difference. 93 | 94 |
95 | 96 |
97 | 98 | 99 | "How is this different from the mdx plugins for webpack or rollup?" 100 | 101 | 102 | 103 | Those tools are intended to be run "at build time" and then you deploy the built 104 | version of your files. This means if you have some content in MDX and want to 105 | make a typo change, you have to rebuild and redeploy the whole site. This also 106 | means that every MDX page you add to your site will increase your build-times, 107 | so it doesn't scale all that well. 108 | 109 | `mdx-bundler` can definitely be used at build-time, but it's more powerfully 110 | used as a runtime bundler. A common use case is to have a route for your MDX 111 | content and when that request comes in, you load the MDX content and hand that 112 | off to `mdx-bundler` for bundling. This means that `mdx-bundler` is infinitely 113 | scalable. Your build won't be any longer regardless of how much MDX content you 114 | have. Also, `mdx-bundler` is quite fast, but to make this on-demand bundling 115 | even faster, you can use appropriate cache headers to avoid unnecessary 116 | re-bundling. 117 | 118 | Webpack/rollup/etc also require that all your MDX files are on the local 119 | filesystem to work. If you want to store your MDX content in a separate repo or 120 | CMS, you're kinda out of luck or have to do some build-time gymnastics to get 121 | the files in place for the build. 122 | 123 | With `mdx-bundler`, it doesn't matter where your MDX content comes from, you can 124 | bundle files from anywhere, you're just responsible for getting the content into 125 | memory and then you hand that off to `mdx-bundler` for bundling. 126 | 127 |
128 | 129 |
130 | 131 | 132 | "Does this work with Remix/Gatsby/Next/CRA/etc?" 133 | 134 | 135 | 136 | Totally. It works with any of those tools. Depending on whether your 137 | meta-framework supports server-side rendering, you'll implement it differently. 138 | You might decide to go with a built-time approach (for Gatsby/CRA), but as 139 | mentioned, the true power of `mdx-bundler` comes in the form of on-demand 140 | bundling. So it's best suited for SSR frameworks like Remix/Next. 141 | 142 |
143 | 144 |
145 | 146 | 147 | "Can I use this other JSX libraries other than React?" 148 | 149 | 150 | 151 | Yes! If JSX runtime you want to use is mentioned here - https://mdxjs.com/docs/getting-started/#jsx, it's guaranteed to work. Libraries, such as `hono` which has `react` compatible API also works. Check to [Other JSX runtimes](#other-jsx-runtimes) to get started. 152 |
153 | 154 |
155 | 156 | 157 | "Why the dodo bird emoji? 🦤" 158 | 159 | 160 | 161 | Why not? 162 | 163 |
164 | 165 |
166 | 167 | 168 | "Why is esbuild a peer dependency?" 169 | 170 | 171 | 172 | esbuild provides a service written in GO that it interacts with. Only one 173 | instance of this service can run at a time and it must have an identical version 174 | to the npm package. If it was a hard dependency you would only be able to use 175 | the esbuild version mdx-bundler uses. 176 | 177 |
178 | 179 | ## Table of Contents 180 | 181 | 182 | 183 | 184 | - [Installation](#installation) 185 | - [Usage](#usage) 186 | - [Options](#options) 187 | - [Returns](#returns) 188 | - [Types](#types) 189 | - [Component Substitution](#component-substitution) 190 | - [Frontmatter and const](#frontmatter-and-const) 191 | - [Accessing named exports](#accessing-named-exports) 192 | - [Image Bundling](#image-bundling) 193 | - [Bundling a file.](#bundling-a-file) 194 | - [Custom Components in Downstream Files](#custom-components-in-downstream-files) 195 | - [Known Issues](#known-issues) 196 | - [Inspiration](#inspiration) 197 | - [Other Solutions](#other-solutions) 198 | - [Issues](#issues) 199 | - [🐛 Bugs](#-bugs) 200 | - [💡 Feature Requests](#-feature-requests) 201 | - [Contributors ✨](#contributors-) 202 | - [LICENSE](#license) 203 | 204 | 205 | 206 | ## Installation 207 | 208 | This module is distributed via [npm][npm] which is bundled with [node][node] and 209 | should be installed as one of your project's `dependencies`: 210 | 211 | ``` 212 | npm install --save mdx-bundler esbuild 213 | ``` 214 | 215 | One of mdx-bundler's dependencies requires a working [node-gyp][node-gyp] setup 216 | to be able to install correctly. 217 | 218 | ## Usage 219 | 220 | ```typescript 221 | import {bundleMDX} from 'mdx-bundler' 222 | 223 | const mdxSource = ` 224 | --- 225 | title: Example Post 226 | published: 2021-02-13 227 | description: This is some description 228 | --- 229 | 230 | # Wahoo 231 | 232 | import Demo from './demo' 233 | 234 | Here's a **neat** demo: 235 | 236 | 237 | `.trim() 238 | 239 | const result = await bundleMDX({ 240 | source: mdxSource, 241 | files: { 242 | './demo.tsx': ` 243 | import * as React from 'react' 244 | 245 | function Demo() { 246 | return
Neat demo!
247 | } 248 | 249 | export default Demo 250 | `, 251 | }, 252 | }) 253 | 254 | const {code, frontmatter} = result 255 | ``` 256 | 257 | From there, you send the `code` to your client, and then: 258 | 259 | ```jsx 260 | import * as React from 'react' 261 | import {getMDXComponent} from 'mdx-bundler/client' 262 | 263 | function Post({code, frontmatter}) { 264 | // it's generally a good idea to memoize this function call to 265 | // avoid re-creating the component every render. 266 | const Component = React.useMemo(() => getMDXComponent(code), [code]) 267 | return ( 268 | <> 269 |
270 |

{frontmatter.title}

271 |

{frontmatter.description}

272 |
273 |
274 | 275 |
276 | 277 | ) 278 | } 279 | ``` 280 | 281 | Ultimately, this gets rendered (basically): 282 | 283 | ```html 284 |
285 |

This is the title

286 |

This is some description

287 |
288 |
289 |
290 |

Wahoo

291 | 292 |

Here's a neat demo:

293 | 294 |
Neat demo!
295 |
296 |
297 | ``` 298 | 299 | ### Options 300 | 301 | #### source 302 | 303 | The `string` source of your MDX. 304 | 305 | _Can not be set if `file` is set_ 306 | 307 | #### file 308 | 309 | The path to the file on your disk with the MDX in. You will probably want to 310 | set [cwd](#cwd) as well. 311 | 312 | _Can not be set if `source` is set_ 313 | 314 | #### files 315 | 316 | The `files` config is an object of all the files you're bundling. The key is the 317 | path to the file (relative to the MDX source) and the value is the string of the 318 | file source code. You could get these from the filesystem or from a remote 319 | database. If your MDX doesn't reference other files (or only imports things from 320 | `node_modules`), then you can omit this entirely. 321 | 322 | #### mdxOptions 323 | 324 | This allows you to modify the built-in MDX configuration (passed to 325 | `@mdx-js/esbuild`). This can be helpful for specifying your own 326 | remarkPlugins/rehypePlugins. 327 | 328 | The function is passed the default mdxOptions and the frontmatter. 329 | 330 | ```ts 331 | bundleMDX({ 332 | source: mdxSource, 333 | mdxOptions(options, frontmatter) { 334 | // this is the recommended way to add custom remark/rehype plugins: 335 | // The syntax might look weird, but it protects you in case we add/remove 336 | // plugins in the future. 337 | options.remarkPlugins = [...(options.remarkPlugins ?? []), myRemarkPlugin] 338 | options.rehypePlugins = [...(options.rehypePlugins ?? []), myRehypePlugin] 339 | 340 | return options 341 | }, 342 | }) 343 | ``` 344 | 345 | #### esbuildOptions 346 | 347 | You can customize any of esbuild options with the option `esbuildOptions`. This 348 | takes a function which is passed the default esbuild options and the frontmatter 349 | and expects an options object to be returned. 350 | 351 | ```typescript 352 | bundleMDX({ 353 | source: mdxSource, 354 | esbuildOptions(options, frontmatter) { 355 | options.minify = false 356 | options.target = [ 357 | 'es2020', 358 | 'chrome58', 359 | 'firefox57', 360 | 'safari11', 361 | 'edge16', 362 | 'node12', 363 | ] 364 | 365 | return options 366 | }, 367 | }) 368 | ``` 369 | 370 | More information on the available options can be found in the 371 | [esbuild documentation](https://esbuild.github.io/api/#build-api). 372 | 373 | It's recommended to use this feature to configure the `target` to your desired 374 | output, otherwise, esbuild defaults to `esnext` which is to say that it doesn't 375 | compile any standardized features so it's possible users of older browsers will 376 | experience errors. 377 | 378 | #### globals 379 | 380 | This tells esbuild that a given module is externally available. For example, if 381 | your MDX file uses the d3 library and you're already using the d3 library in 382 | your app then you'll end up shipping `d3` to the user twice (once for your app 383 | and once for this MDX component). This is wasteful and you'd be better off just 384 | telling esbuild to _not_ bundle `d3` and you can pass it to the component 385 | yourself when you call `getMDXComponent`. 386 | 387 | Global external configuration options: 388 | https://www.npmjs.com/package/@fal-works/esbuild-plugin-global-externals 389 | 390 | Here's an example: 391 | 392 | ```tsx 393 | // server-side or build-time code that runs in Node: 394 | import {bundleMDX} from 'mdx-bundler' 395 | 396 | const mdxSource = ` 397 | # This is the title 398 | 399 | import leftPad from 'left-pad' 400 | 401 |
{leftPad("Neat demo!", 12, '!')}
402 | `.trim() 403 | 404 | const result = await bundleMDX({ 405 | source: mdxSource, 406 | // NOTE: this is *only* necessary if you want to share deps between your MDX 407 | // file bundle and the host app. Otherwise, all deps will just be bundled. 408 | // So it'll work either way, this is just an optimization to avoid sending 409 | // multiple copies of the same library to your users. 410 | globals: {'left-pad': 'myLeftPad'}, 411 | }) 412 | ``` 413 | 414 | ```tsx 415 | // server-rendered and/or client-side code that can run in the browser or Node: 416 | import * as React from 'react' 417 | import leftPad from 'left-pad' 418 | import {getMDXComponent} from 'mdx-bundler/client' 419 | 420 | function MDXPage({code}: {code: string}) { 421 | const Component = React.useMemo( 422 | () => getMDXComponent(result.code, {myLeftPad: leftPad}), 423 | [result.code, leftPad], 424 | ) 425 | return ( 426 |
427 | 428 |
429 | ) 430 | } 431 | ``` 432 | 433 | #### cwd 434 | 435 | Setting `cwd` (_current working directory_) to a directory will allow esbuild to 436 | resolve imports. This directory could be the directory the mdx content was read 437 | from or a directory that off-disk mdx should be _run_ in. 438 | 439 | _content/pages/demo.tsx_ 440 | 441 | ```typescript 442 | import * as React from 'react' 443 | 444 | function Demo() { 445 | return
Neat demo!
446 | } 447 | 448 | export default Demo 449 | ``` 450 | 451 | _src/build.ts_ 452 | 453 | ```typescript 454 | import {bundleMDX} from 'mdx-bundler' 455 | 456 | const mdxSource = ` 457 | --- 458 | title: Example Post 459 | published: 2021-02-13 460 | description: This is some description 461 | --- 462 | 463 | # Wahoo 464 | 465 | import Demo from './demo' 466 | 467 | Here's a **neat** demo: 468 | 469 | 470 | `.trim() 471 | 472 | const result = await bundleMDX({ 473 | source: mdxSource, 474 | cwd: '/users/you/site/_content/pages', 475 | }) 476 | 477 | const {code, frontmatter} = result 478 | ``` 479 | 480 | #### grayMatterOptions 481 | 482 | This allows you to configure the 483 | [gray-matter options](https://github.com/jonschlinkert/gray-matter#options). 484 | 485 | Your function is passed the current gray-matter configuration for you to modify. 486 | Return your modified configuration object for gray matter. 487 | 488 | ```js 489 | bundleMDX({ 490 | grayMatterOptions: options => { 491 | options.excerpt = true 492 | 493 | return options 494 | }, 495 | }) 496 | ``` 497 | 498 | #### bundleDirectory & bundlePath 499 | 500 | This allows you to set the output directory for the bundle and the public URL to 501 | the directory. If one option is set the other must be as well. 502 | 503 | _The Javascript bundle is not written to this directory and is still returned as 504 | a string from `bundleMDX`._ 505 | 506 | This feature is best used with tweaks to `mdxOptions` and `esbuildOptions`. In 507 | the example below `.png` files are written to the disk and then served from 508 | `/file/`. 509 | 510 | This allows you to store assets with your MDX and then have esbuild process them 511 | like anything else. 512 | 513 | _It is recommended that each bundle has its own `bundleDirectory` so that 514 | multiple bundles don't overwrite each others assets._ 515 | 516 | ```ts 517 | const {code} = await bundleMDX({ 518 | file: '/path/to/site/content/file.mdx', 519 | cwd: '/path/to/site/content', 520 | bundleDirectory: '/path/to/site/public/file', 521 | bundlePath: '/file/', 522 | mdxOptions: options => { 523 | options.remarkPlugins = [remarkMdxImages] 524 | 525 | return options 526 | }, 527 | esbuildOptions: options => { 528 | options.loader = { 529 | ...options.loader, 530 | '.png': 'file', 531 | } 532 | 533 | return options 534 | }, 535 | }) 536 | ``` 537 | 538 | ### Returns 539 | 540 | `bundleMDX` returns a promise for an object with the following properties. 541 | 542 | - `code` - The bundle of your mdx as a `string`. 543 | - `frontmatter` - The frontmatter `object` from gray-matter. 544 | - `matter` - The whole 545 | [object returned by gray-matter](https://github.com/jonschlinkert/gray-matter#returned-object) 546 | 547 | ### Types 548 | 549 | `mdx-bundler` supplies complete typings within its own package. 550 | 551 | `bundleMDX` has a single type parameter which is the type of your frontmatter. 552 | It defaults to `{[key: string]: any}` and must be an object. This is then used 553 | to type the returned `frontmatter` and the frontmatter passed to 554 | `esbuildOptions` and `mdxOptions`. 555 | 556 | ```ts 557 | const {frontmatter} = bundleMDX<{title: string}>({source}) 558 | 559 | frontmatter.title // has type string 560 | ``` 561 | 562 | ### Component Substitution 563 | 564 | MDX Bundler passes on 565 | [MDX's ability to substitute components](https://mdxjs.com/docs/using-mdx/#components) 566 | through the `components` prop on the component returned by `getMDXComponent`. 567 | 568 | Here's an example that removes _p_ tags from around images. 569 | 570 | ```tsx 571 | import * as React from 'react' 572 | import {getMDXComponent} from 'mdx-bundler/client' 573 | 574 | const Paragraph: React.FC = props => { 575 | if (typeof props.children !== 'string' && props.children.type === 'img') { 576 | return <>{props.children} 577 | } 578 | 579 | return

580 | } 581 | 582 | function MDXPage({code}: {code: string}) { 583 | const Component = React.useMemo(() => getMDXComponent(code), [code]) 584 | 585 | return ( 586 |

587 | 588 |
589 | ) 590 | } 591 | ``` 592 | 593 | ### Frontmatter and const 594 | 595 | You can reference frontmatter meta or consts in the mdx content. 596 | 597 | ```mdx 598 | --- 599 | title: Example Post 600 | --- 601 | 602 | export const exampleImage = 'https://example.com/image.jpg' 603 | 604 | # {frontmatter.title} 605 | 606 | Image alt text 607 | ``` 608 | 609 | ### Accessing named exports 610 | 611 | You can use `getMDXExport` instead of `getMDXComponent` to treat the mdx file as 612 | a module instead of just a component. It takes the same arguments that 613 | `getMDXComponent` does. 614 | 615 | ```mdx 616 | --- 617 | title: Example Post 618 | --- 619 | 620 | export const toc = [{depth: 1, value: 'The title'}] 621 | 622 | # The title 623 | ``` 624 | 625 | ```js 626 | import * as React from 'react' 627 | import {getMDXExport} from 'mdx-bundler/client' 628 | 629 | function MDXPage({code}: {code: string}) { 630 | const mdxExport = getMDXExport(code) 631 | console.log(mdxExport.toc) // [ { depth: 1, value: 'The title' } ] 632 | 633 | const Component = React.useMemo(() => mdxExport.default, [code]) 634 | 635 | return 636 | } 637 | ``` 638 | 639 | ### Image Bundling 640 | 641 | With the [cwd](#cwd) and the remark plugin 642 | [remark-mdx-images](https://www.npmjs.com/package/remark-mdx-images) you can 643 | bundle images in your mdx! 644 | 645 | There are two loaders in esbuild that can be used here. The easiest is `dataurl` 646 | which outputs the images as inline data urls in the returned code. 647 | 648 | ```js 649 | import {remarkMdxImages} from 'remark-mdx-images' 650 | 651 | const {code} = await bundleMDX({ 652 | source: mdxSource, 653 | cwd: '/users/you/site/_content/pages', 654 | mdxOptions: options => { 655 | options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkMdxImages] 656 | 657 | return options 658 | }, 659 | esbuildOptions: options => { 660 | options.loader = { 661 | ...options.loader, 662 | '.png': 'dataurl', 663 | } 664 | 665 | return options 666 | }, 667 | }) 668 | ``` 669 | 670 | The `file` loader requires a little more configuration to get working. With the 671 | `file` loader your images are copied to the output directory so esbuild needs to 672 | be set to write files and needs to know where to put them plus the url of the 673 | folder to be used in image sources. 674 | 675 | > Each call to `bundleMDX` is isolated from the others. If you set the directory 676 | > the same for everything `bundleMDX` will overwrite images without warning. As 677 | > a result each _bundle_ needs its own output directory. 678 | 679 | ```js 680 | // For the file `_content/pages/about.mdx` 681 | 682 | const {code} = await bundleMDX({ 683 | source: mdxSource, 684 | cwd: '/users/you/site/_content/pages', 685 | mdxOptions: options => { 686 | options.remarkPlugins = [...(options.remarkPlugins ?? []), remarkMdxImages] 687 | 688 | return options 689 | }, 690 | esbuildOptions: options => { 691 | // Set the `outdir` to a public location for this bundle. 692 | options.outdir = '/users/you/site/public/img/about' 693 | options.loader = { 694 | ...options.loader, 695 | // Tell esbuild to use the `file` loader for pngs 696 | '.png': 'file', 697 | } 698 | // Set the public path to /img/about 699 | options.publicPath = '/img/about' 700 | 701 | // Set write to true so that esbuild will output the files. 702 | options.write = true 703 | 704 | return options 705 | }, 706 | }) 707 | ``` 708 | 709 | ### Bundling a file. 710 | 711 | If your MDX file is on your disk you can save some time and code by having 712 | `mdx-bundler` read the file for you. Instead of supplying a `source` string you 713 | can set `file` to the path of the MDX on disk. Set `cwd` to its folder so that 714 | relative imports work. 715 | 716 | ```js 717 | import {bundleMDX} from 'mdx-bundler' 718 | 719 | const {code, frontmatter} = await bundleMDX({ 720 | file: '/users/you/site/content/file.mdx', 721 | cwd: '/users/you/site/content/', 722 | }) 723 | ``` 724 | 725 | ### Custom Components in Downstream Files 726 | 727 | To make sure custom components are accessible in downstream MDX files, you 728 | can use the `MDXProvider` from `@mdx-js/react` to pass custom components 729 | to your nested imports. 730 | 731 | ``` 732 | npm install --save @mdx-js/react 733 | ``` 734 | 735 | ```tsx 736 | const globals = { 737 | '@mdx-js/react': { 738 | varName: 'MdxJsReact', 739 | namedExports: ['useMDXComponents'], 740 | defaultExport: false, 741 | }, 742 | }; 743 | const { code } = bundleMDX({ 744 | source, 745 | globals, 746 | mdxOptions(options: Record) { 747 | return { 748 | ...options, 749 | providerImportSource: '@mdx-js/react', 750 | }; 751 | } 752 | }); 753 | ``` 754 | 755 | From there, you send the `code` to your client, and then: 756 | 757 | ```tsx 758 | import { MDXProvider, useMDXComponents } from '@mdx-js/react'; 759 | const MDX_GLOBAL_CONFIG = { 760 | MdxJsReact: { 761 | useMDXComponents, 762 | }, 763 | }; 764 | export const MDXComponent: React.FC<{ 765 | code: string; 766 | frontmatter: Record; 767 | }> = ({ code }) => { 768 | const Component = useMemo( 769 | () => getMDXComponent(code, MDX_GLOBAL_CONFIG), 770 | [code], 771 | ); 772 | 773 | return ( 774 |

{children}

}}> 775 | 776 |
777 | ); 778 | }; 779 | ``` 780 | 781 | ### Known Issues 782 | 783 | ### Other JSX runtimes 784 | JSX runtimes mentioned [here](https://mdxjs.com/docs/getting-started/#jsx) are guaranteed to be supported, however any JSX runtime should work without problem, as long as they export their own jsx runtime. For example, `hono` is not mentioned here, but as it has `react` compatible API, it can be used without any issues. 785 | 786 | To do so, you will have to pass a configuration object and use JSX Component factory. 787 | ```tsx 788 | const getMDX = (source) => { 789 | return bundleMDX({ 790 | source, 791 | jsxConfig: { 792 | jsxLib: { 793 | varName: 'HonoJSX', 794 | package: 'hono/jsx', 795 | }, 796 | jsxDom: { 797 | varName: 'HonoDOM', 798 | package: 'hono/jsx/dom', 799 | }, 800 | jsxRuntime: { 801 | varName: '_jsx_runtime', 802 | package: 'hono/jsx/jsx-runtime', 803 | }, 804 | } 805 | }); 806 | } 807 | 808 | // ... 809 | 810 | import { getMDXComponent } from "mdx-bundler/client/jsx"; 811 | 812 | import * as HonoJSX from "hono/jsx"; 813 | import * as HonoDOM from "hono/jsx/dom"; 814 | import * as _jsx_runtime from "hono/jsx/jsx-runtime"; 815 | const jsxConfig = { 816 | HonoJSX, 817 | HonoDOM, 818 | _jsx_runtime 819 | }; 820 | 821 | export const MDXComponent: React.FC<{ 822 | code: string; 823 | }> = ({ code }) => { 824 | const Component = useMemo( 825 | () => getMDXComponent(code, jsxConfig), 826 | [code], 827 | ); 828 | 829 | return ( 830 |

{children}

}} /> 831 | ); 832 | }; 833 | ``` 834 | 835 | To use it with others, adjust `jsxConfig` passed to bundler. 836 | ```ts 837 | const jsxConfig = { 838 | jsxLib: { 839 | varName: 'HonoJSX', 840 | package: 'hono/jsx', 841 | }, 842 | jsxDom: { 843 | varName: 'HonoDOM', 844 | package: 'hono/jsx/dom', 845 | }, 846 | jsxRuntime: { 847 | varName: '_jsx_runtime', 848 | package: 'hono/jsx/jsx-runtime', 849 | }, 850 | } 851 | ``` 852 | and to `getMDXComponent` 853 | ```ts 854 | const jsxConfig = { HonoJSX, HonoDOM, _jsx_runtime }; 855 | ``` 856 | 857 | 858 | #### Cloudflare Workers 859 | 860 | We'd _love_ for this to work in cloudflare workers. Unfortunately cloudflare workers 861 | have two limitations that prevent `mdx-bundler` from working in that 862 | environment: 863 | 864 | 1. Workers can't run binaries. `bundleMDX` uses `esbuild` (a binary) to bundle 865 | your MDX code. 866 | 2. Workers can't run `eval` or similar. `getMDXComponent` evaluates the bundled 867 | code using `new Function`. 868 | 869 | One workaround to this is to put your mdx-bundler related code in a different 870 | environment and call that environment from within the Cloudflare worker. IMO, 871 | this defeats the purpose of using Cloudflare workers. Another potential 872 | workaround is to use WASM from within the worker. There is 873 | [`esbuild-wasm`](https://esbuild.github.io/getting-started/#install-the-wasm-version) 874 | but there are some issues with that package explained at that link. Then there's 875 | [`wasm-jseval`](https://github.com/maple3142/wasm-jseval), but I couldn't get 876 | that to run code that was output from `mdx-bundler` without error. 877 | 878 | If someone would like to dig into this, that would be stellar, but unfortunately 879 | it's unlikely I'll ever work on it. 880 | 881 | #### Next.JS esbuild ENOENT 882 | 883 | esbuild relies on `__dirname` to work out where is executable is, Next.JS and 884 | Webpack can sometimes break this and esbuild needs to be told manually where to 885 | look. 886 | 887 | Adding the following code before your `bundleMDX` will point esbuild directly at 888 | the correct executable for your platform. 889 | 890 | ```js 891 | import path from 'path' 892 | 893 | if (process.platform === 'win32') { 894 | process.env.ESBUILD_BINARY_PATH = path.join( 895 | process.cwd(), 896 | 'node_modules', 897 | 'esbuild', 898 | 'esbuild.exe', 899 | ) 900 | } else { 901 | process.env.ESBUILD_BINARY_PATH = path.join( 902 | process.cwd(), 903 | 'node_modules', 904 | 'esbuild', 905 | 'bin', 906 | 'esbuild', 907 | ) 908 | } 909 | ``` 910 | 911 | More information on this issue can be found 912 | [in this article](https://www.arcath.net/2021/03/mdx-bundler#esbuild-executable). 913 | 914 | ## Inspiration 915 | 916 | As I was rewriting [kentcdodds.com](https://kentcdodds.com) to 917 | [remix](https://remix.run), I decided I wanted to keep my blog posts as MDX, but 918 | I didn't want to have to compile them all at build time or be required to 919 | redeploy every time I fix a typo. So I made this which allows my server to 920 | compile on demand. 921 | 922 | ## Other Solutions 923 | 924 | There's [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) but it's 925 | more of an mdx-compiler than a bundler (can't bundle your mdx for dependencies). 926 | Also it's focused on Next.js whereas this is meta-framework agnostic. 927 | 928 | ## Issues 929 | 930 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 931 | label._ 932 | 933 | ### 🐛 Bugs 934 | 935 | Please file an issue for bugs, missing documentation, or unexpected behavior. 936 | 937 | [**See Bugs**][bugs] 938 | 939 | ### 💡 Feature Requests 940 | 941 | Please file an issue to suggest new features. Vote on feature requests by adding 942 | a 👍. This helps maintainers prioritize what to work on. 943 | 944 | [**See Feature Requests**][requests] 945 | 946 | ## Contributors ✨ 947 | 948 | Thanks goes to these people ([emoji key][emojis]): 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 |
Kent C. Dodds
Kent C. Dodds

💻 📖 🚇 ⚠️
benwis
benwis

🐛 👀
Adam Laycock
Adam Laycock

💻 ⚠️ 🤔 👀 📖
Titus
Titus

🤔 👀 💻
Christian Murphy
Christian Murphy

🤔
Pedro Duarte
Pedro Duarte

📖
Erik Rasmussen
Erik Rasmussen

📖
Omar Syx
Omar Syx

🐛
Gaël Haméon
Gaël Haméon

📖
Gabriel Loiácono
Gabriel Loiácono

💻 ⚠️
Spencer Miskoviak
Spencer Miskoviak

📖
Casper
Casper

💻
Apostolos Christodoulou
Apostolos Christodoulou

📖
Yordis Prieto
Yordis Prieto

💻
xoumi
xoumi

💻
Yasin
Yasin

💻
Mohammed 'Mo' Mulazada
Mohammed 'Mo' Mulazada

📖
Can Rau
Can Rau

📖
Hosenur Rahaman
Hosenur Rahaman

📖
Maciek Sitkowski
Maciek Sitkowski

📖
Priyang
Priyang

💻 📖
Mosaad
Mosaad

📖
stefanprobst
stefanprobst

💻 ⚠️
Vlad Moroz
Vlad Moroz

💻
989 | 990 | 991 | 992 | 993 | 994 | 995 | This project follows the [all-contributors][all-contributors] specification. 996 | Contributions of any kind welcome! 997 | 998 | ## LICENSE 999 | 1000 | MIT 1001 | 1002 | 1003 | [npm]: https://www.npmjs.com 1004 | [node]: https://nodejs.org 1005 | [build-badge]: https://img.shields.io/github/workflow/status/kentcdodds/mdx-bundler/validate?logo=github&style=flat-square 1006 | [build]: https://github.com/kentcdodds/mdx-bundler/actions?query=workflow%3Avalidate 1007 | [coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/mdx-bundler.svg?style=flat-square 1008 | [coverage]: https://codecov.io/github/kentcdodds/mdx-bundler 1009 | [version-badge]: https://img.shields.io/npm/v/mdx-bundler.svg?style=flat-square 1010 | [package]: https://www.npmjs.com/package/mdx-bundler 1011 | [downloads-badge]: https://img.shields.io/npm/dm/mdx-bundler.svg?style=flat-square 1012 | [npmtrends]: https://www.npmtrends.com/mdx-bundler 1013 | [license-badge]: https://img.shields.io/npm/l/mdx-bundler.svg?style=flat-square 1014 | [license]: https://github.com/kentcdodds/mdx-bundler/blob/main/LICENSE 1015 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 1016 | [prs]: https://makeapullrequest.com 1017 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 1018 | [coc]: https://github.com/kentcdodds/mdx-bundler/blob/main/CODE_OF_CONDUCT.md 1019 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key 1020 | [all-contributors]: https://github.com/all-contributors/all-contributors 1021 | [all-contributors-badge]: https://img.shields.io/github/all-contributors/kentcdodds/mdx-bundler?color=orange&style=flat-square 1022 | [bugs]: https://github.com/kentcdodds/mdx-bundler/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Acreated-desc+label%3Abug 1023 | [requests]: https://github.com/kentcdodds/mdx-bundler/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement 1024 | [good-first-issue]: https://github.com/kentcdodds/mdx-bundler/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3Aenhancement+label%3A%22good+first+issue%22 1025 | [node-gyp]: https://github.com/nodejs/node-gyp#installation 1026 | 1027 | -------------------------------------------------------------------------------- /client/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/client' 2 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../dist/client/index.js') 2 | -------------------------------------------------------------------------------- /client/jsx.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/client/jsx' -------------------------------------------------------------------------------- /client/jsx.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../dist/client/jsx') 2 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "main": "./index.js", 4 | "types": "./index.d.ts", 5 | "exports": { 6 | "react": { 7 | "types": "./react.d.ts", 8 | "default": "./react.js" 9 | }, 10 | "jsx": { 11 | "types": "./jsx.d.ts", 12 | "default": "./jsx.js" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/react.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/client/react' -------------------------------------------------------------------------------- /client/react.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../dist/client/index.js') 2 | -------------------------------------------------------------------------------- /other/150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/mdx-bundler/1fac27347d7139dab27bcd96d0a89d8432a44fce/other/150.png -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | 4 | 5 | 6 | **Table of Contents** 7 | 8 | - [Code of Conduct](#code-of-conduct) 9 | - [Issues](#issues) 10 | - [Pull Requests](#pull-requests) 11 | - [Release](#release) 12 | - [Thanks!](#thanks) 13 | 14 | 15 | 16 | This is documentation for maintainers of this project. 17 | 18 | ## Code of Conduct 19 | 20 | Please review, understand, and be an example of it. Violations of the code of 21 | conduct are taken seriously, even (especially) for maintainers. 22 | 23 | ## Issues 24 | 25 | We want to support and build the community. We do that best by helping people 26 | learn to solve their own problems. We have an issue template and hopefully most 27 | folks follow it. If it's not clear what the issue is, invite them to create a 28 | minimal reproduction of what they're trying to accomplish or the bug they think 29 | they've found. 30 | 31 | Once it's determined that a code change is necessary, point people to 32 | [makeapullrequest.com](https://makeapullrequest.com) and invite them to make a 33 | pull request. If they're the one who needs the feature, they're the one who can 34 | build it. If they need some hand holding and you have time to lend a hand, 35 | please do so. It's an investment into another human being, and an investment 36 | into a potential maintainer. 37 | 38 | Remember that this is open source, so the code is not yours, it's ours. If 39 | someone needs a change in the codebase, you don't have to make it happen 40 | yourself. Commit as much time to the project as you want/need to. Nobody can ask 41 | any more of you than that. 42 | 43 | ## Pull Requests 44 | 45 | As a maintainer, you're fine to make your branches on the main repo or on your 46 | own fork. Either way is fine. 47 | 48 | When we receive a pull request, a GitHub Action is kicked off automatically (see 49 | the `.github/workflows/validate.yml` for what runs in the Action). We avoid 50 | merging anything that breaks the GitHub Action. 51 | 52 | Please review PRs and focus on the code rather than the individual. You never 53 | know when this is someone's first ever PR and we want their experience to be as 54 | positive as possible, so be uplifting and constructive. 55 | 56 | When you merge the pull request, 99% of the time you should use the 57 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) 58 | feature. This keeps our git history clean, but more importantly, this allows us 59 | to make any necessary changes to the commit message so we release what we want 60 | to release. See the next section on Releases for more about that. 61 | 62 | ## Release 63 | 64 | Our releases are automatic. They happen whenever code lands into `main`. A 65 | GitHub Action gets kicked off and if it's successful, a tool called 66 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is 67 | used to automatically publish a new release to npm as well as a changelog to 68 | GitHub. It is only able to determine the version and whether a release is 69 | necessary by the git commit messages. With this in mind, **please brush up on 70 | [the commit message convention][commit] which drives our releases.** 71 | 72 | > One important note about this: Please make sure that commit messages do NOT 73 | > contain the words "BREAKING CHANGE" in them unless we want to push a major 74 | > version. I've been burned by this more than once where someone will include 75 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not 76 | > a huge deal honestly, but kind of annoying... 77 | 78 | ## Thanks! 79 | 80 | Thank you so much for helping to maintain this project! 81 | 82 | 83 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 84 | 85 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | If you or your company uses this project, add your name to this list! Eventually 4 | we may have a website to showcase these (wanna build it!?) 5 | 6 | > No users have been added yet! 7 | 8 | 13 | -------------------------------------------------------------------------------- /other/cjs-ify.js: -------------------------------------------------------------------------------- 1 | // This file exists because we want to develop our package with Native ESM 2 | // but distribute our package as CommonJS. We need to use native ESM because 3 | // several deps use ESM and it's just easier to integrate with them using native 4 | // ESM. But we want to expose CommonJS because our package consumers aren't ready 5 | // to consume native ESM packages yet... 6 | 7 | // This is hopefully temporary... 8 | import fs from 'fs' 9 | import url from 'url' 10 | import path from 'path' 11 | 12 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 13 | const distPath = path.join(__dirname, '../dist') 14 | const pkgPath = path.join(distPath, 'package.json') 15 | 16 | const cjsPkgInfo = { 17 | type: 'commonjs', 18 | main: './index.js', 19 | types: './index.d.ts', 20 | } 21 | fs.writeFileSync(pkgPath, JSON.stringify(cjsPkgInfo)) 22 | 23 | // when babel compiles this file, it renames it from `.cjs` to `.js` but our 24 | // code imports it with the extension (becuase during dev we're native ESM so we 25 | // have to) and it's easier to update the extension than it would be to update 26 | // the import in the code during the build. 27 | fs.renameSync( 28 | path.join(distPath, 'dirname-messed-up.js'), 29 | path.join(distPath, 'dirname-messed-up.cjs'), 30 | ) 31 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when 4 | there are useful changes in the code that justify a release. But sometimes 5 | things get messed up one way or another and we need to trigger the release 6 | ourselves. When this happens, simply bump the number below and commit that with 7 | the following commit message based on your needs: 8 | 9 | **Major** 10 | 11 | ``` 12 | fix(release): manually release a major version 13 | 14 | There was an issue with a major release, so this manual-releases.md 15 | change is to release a new major version. 16 | 17 | Reference: # 18 | 19 | BREAKING CHANGE: 20 | ``` 21 | 22 | **Minor** 23 | 24 | ``` 25 | feat(release): manually release a minor version 26 | 27 | There was an issue with a minor release, so this manual-releases.md 28 | change is to release a new minor version. 29 | 30 | Reference: # 31 | ``` 32 | 33 | **Patch** 34 | 35 | ``` 36 | fix(release): manually release a patch version 37 | 38 | There was an issue with a patch release, so this manual-releases.md 39 | change is to release a new patch version. 40 | 41 | Reference: # 42 | ``` 43 | 44 | The number of times we've had to do a manual release is: 0 45 | -------------------------------------------------------------------------------- /other/sample-component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import image from './150.png' 4 | 5 | /** @type React.FC */ 6 | export const Sample = () => { 7 | return ( 8 |
9 | Sample! 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /other/sample.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Sample' 3 | --- 4 | 5 | import {Sample} from './sample-component' 6 | 7 | This is a sample mdx file that should demo mdx-bundlers features. 8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdx-bundler", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Compile and bundle your MDX files and their dependencies. FAST.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "keywords": [ 9 | "mdx", 10 | "bundler", 11 | "mdx-bundler", 12 | "esbuild" 13 | ], 14 | "author": "Kent C. Dodds (https://kentcdodds.com)", 15 | "license": "MIT", 16 | "engines": { 17 | "node": ">=18", 18 | "npm": ">=6" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/kentcdodds/mdx-bundler" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/kentcdodds/mdx-bundler/issues" 26 | }, 27 | "homepage": "https://github.com/kentcdodds/mdx-bundler#readme", 28 | "files": [ 29 | "dist", 30 | "client" 31 | ], 32 | "scripts": { 33 | "build": "kcd-scripts build", 34 | "postbuild": "node ./other/cjs-ify.js", 35 | "lint": "kcd-scripts lint", 36 | "setup": "npm install && npm run validate -s", 37 | "test": "c8 -r text -r lcov uvu -i setup-tests.js src/__tests__", 38 | "typecheck": "kcd-scripts typecheck", 39 | "validate": "kcd-scripts validate" 40 | }, 41 | "dependencies": { 42 | "@babel/runtime": "^7.23.2", 43 | "@esbuild-plugins/node-resolve": "^0.2.2", 44 | "@fal-works/esbuild-plugin-global-externals": "^2.1.2", 45 | "@mdx-js/esbuild": "^3.0.0", 46 | "gray-matter": "^4.0.3", 47 | "remark-frontmatter": "^5.0.0", 48 | "remark-mdx-frontmatter": "^4.0.0", 49 | "uuid": "^9.0.1", 50 | "vfile": "^6.0.1" 51 | }, 52 | "peerDependencies": { 53 | "esbuild": "0.*" 54 | }, 55 | "devDependencies": { 56 | "@testing-library/preact": "3.2.4", 57 | "@testing-library/react": "^14.1.0", 58 | "@testing-library/vue": "8.1.0", 59 | "@types/jsdom": "^21.1.5", 60 | "@types/mdx": "^2.0.10", 61 | "@types/react": "^18.2.37", 62 | "@types/react-dom": "^18.2.15", 63 | "@types/uuid": "^9.0.7", 64 | "babel-eslint": "^10.1.0", 65 | "c8": "^8.0.1", 66 | "cross-env": "^7.0.3", 67 | "esbuild": "^0.19.5", 68 | "hono": "4.6.14", 69 | "jsdom": "^22.1.0", 70 | "kcd-scripts": "^14.0.1", 71 | "left-pad": "^1.3.0", 72 | "mdx-test-data": "^1.0.1", 73 | "preact": "10.25.3", 74 | "react": "^18.2.0", 75 | "react-dom": "^18.2.0", 76 | "remark-mdx-images": "^3.0.0", 77 | "typescript": "^5.2.2", 78 | "uvu": "^0.5.6", 79 | "vue": "3.5.13" 80 | }, 81 | "eslintConfig": { 82 | "extends": "./node_modules/kcd-scripts/eslint.js", 83 | "parserOptions": { 84 | "sourceType": "module", 85 | "ecmaFeatures": { 86 | "modules": true 87 | }, 88 | "ecmaVersion": 2022 89 | }, 90 | "rules": { 91 | "import/extensions": "off", 92 | "@typescript-eslint/no-unsafe-assignment": "off", 93 | "max-lines-per-function": "off" 94 | } 95 | }, 96 | "eslintIgnore": [ 97 | "node_modules", 98 | "coverage", 99 | "dist", 100 | "*.d.ts" 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /src/__tests__/hono.js: -------------------------------------------------------------------------------- 1 | import './setup-tests.js' 2 | import { Hono } from "hono"; 3 | /* eslint-disable import/no-unresolved -- 4 | * imports paths are there in node_modules/hono/package.json 5 | * but it doesn't get resolved 6 | */ 7 | import * as HonoJSX from "hono/jsx"; 8 | import * as HonoDOM from "hono/jsx/dom"; 9 | import * as _jsx_runtime from "hono/jsx/jsx-runtime"; 10 | /* eslint-enable import/no-unresolved */ 11 | import {suite} from 'uvu' 12 | import * as assert from 'uvu/assert' 13 | import {bundleMDX} from '../index.js' 14 | import {getMDXComponent} from '../client/jsx.js' 15 | 16 | const test = suite("hono"); 17 | 18 | const jsxBundlerConfig = { 19 | jsxLib: { 20 | varName: 'HonoJSX', 21 | package: 'hono/jsx', 22 | }, 23 | jsxDom: { 24 | varName: 'HonoDOM', 25 | package: 'hono/jsx/dom', 26 | }, 27 | jsxRuntime: { 28 | varName: '_jsx_runtime', 29 | package: 'hono/jsx/jsx-runtime', 30 | }, 31 | } 32 | const jsxComponentConfig = { HonoJSX, HonoDOM, _jsx_runtime } 33 | 34 | const mdxSource = ` 35 | --- 36 | title: Example Post 37 | published: 2021-02-13 38 | description: This is some meta-data 39 | --- 40 | import Demo from './demo' 41 | 42 | # This is the title 43 | 44 | Here's a **neat** demo: 45 | 46 | `.trim(); 47 | 48 | const demoTsx = ` 49 | export default function Demo() { 50 | return
mdx-bundler with hono's runtime!
51 | } 52 | `.trim(); 53 | 54 | 55 | test('smoke test for hono', async () => { 56 | 57 | const result = await bundleMDX({ 58 | source: mdxSource, 59 | jsxConfig: jsxBundlerConfig, 60 | files: { 61 | './demo.tsx': demoTsx 62 | } 63 | }); 64 | 65 | /** @param {HonoJSX.DOMAttributes} props */ 66 | const SpanBold = ({ children }) => { 67 | return HonoJSX.createElement('span', { className: "strong" }, children) 68 | } 69 | 70 | const app = new Hono() 71 | .get("/", (c) => { 72 | const Component = getMDXComponent(result.code, jsxComponentConfig); 73 | return c.html(HonoJSX.jsx(Component, { components: { strong: SpanBold } }).toString()); 74 | }); 75 | 76 | const req = new Request("http://localhost/"); 77 | const res = await app.fetch(req); 78 | assert.equal(await res.text(), `

This is the title

79 |

Here's a neat demo:

80 |
mdx-bundler with hono's runtime!
`); 81 | }) 82 | 83 | test.run() -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import './setup-tests.js' 2 | import path from 'path' 3 | import {suite} from 'uvu' 4 | import * as assert from 'uvu/assert' 5 | import React from 'react' 6 | import rtl from '@testing-library/react' 7 | import leftPad from 'left-pad' 8 | import remarkMdxImages from 'remark-mdx-images' 9 | import {VFile} from 'vfile' 10 | import {bundleMDX} from '../index.js' 11 | import {getMDXComponent, getMDXExport} from '../client/react.js' 12 | 13 | const test = suite("react"); 14 | const {render} = rtl 15 | 16 | test('smoke test', async () => { 17 | const mdxSource = ` 18 | --- 19 | title: Example Post 20 | published: 2021-02-13 21 | description: This is some meta-data 22 | --- 23 | 24 | # This is the title 25 | 26 | import Demo from './demo' 27 | 28 | Here's a **neat** demo: 29 | 30 | 31 | `.trim() 32 | 33 | const result = await bundleMDX({ 34 | source: mdxSource, 35 | files: { 36 | './demo.tsx': ` 37 | import * as React from 'react' 38 | import leftPad from 'left-pad' 39 | import SubDir from './sub/dir.tsx' 40 | import data from './data.json' 41 | import jsInfo from './js-info.js' 42 | import JsxComp from './jsx-comp.jsx' 43 | import MdxComp from './mdx-comp.mdx' 44 | 45 | function Demo() { 46 | return ( 47 |
48 | {leftPad("Neat demo!", 12, '!')} 49 | Sub dir! 50 |

JSON: {data.package}

51 |
{jsInfo}
52 | 53 | 54 |
55 | ) 56 | } 57 | 58 | export default Demo 59 | `.trim(), 60 | './sub/dir.tsx': ` 61 | import * as React from 'react' 62 | 63 | export default ({children}) =>
{children}
64 | `.trim(), 65 | './js-info.js': 'export default "this is js info"', 66 | './jsx-comp.jsx': 'export default () =>
jsx comp
', 67 | './mdx-comp.mdx': ` 68 | --- 69 | title: This is frontmatter 70 | --- 71 | 72 | # Frontmatter title: {frontmatter.title} 73 | `.trim(), 74 | './data.json': `{"package": "mdx-bundler"}`, 75 | }, 76 | globals: {'left-pad': 'myLeftPad'}, 77 | }) 78 | 79 | const frontmatter = 80 | /** @type { title: string, description: string, published: string } */ result.frontmatter 81 | 82 | /** 83 | * This creates a custom left pad which uses a different filler character to the one supplied. 84 | * If it is not substituted the original will be used and we will get "!" instead of "$" 85 | * 86 | * @param {string} string 87 | * @param {number} length 88 | * @returns {string} 89 | */ 90 | const myLeftPad = (string, length) => { 91 | return leftPad(string, length, '$') 92 | } 93 | 94 | const Component = getMDXComponent(result.code, {myLeftPad}) 95 | 96 | /** @param {React.HTMLAttributes} props */ 97 | const SpanBold = props => React.createElement('span', props) 98 | 99 | assert.equal(frontmatter, { 100 | title: 'Example Post', 101 | published: new Date('2021-02-13'), 102 | description: 'This is some meta-data', 103 | }) 104 | 105 | const {container} = render( 106 | React.createElement(Component, {components: {strong: SpanBold}}), 107 | ) 108 | 109 | assert.equal( 110 | container.innerHTML, 111 | `

This is the title

112 | 113 |

Here's a neat demo:

114 |
$$Neat demo!
Sub dir!

JSON: mdx-bundler

this is js info
jsx comp

Frontmatter title: This is frontmatter

`, 115 | ) 116 | }) 117 | 118 | test('bundles 3rd party deps', async () => { 119 | const mdxSource = ` 120 | import Demo from './demo' 121 | 122 | 123 | `.trim() 124 | 125 | const result = await bundleMDX({ 126 | source: mdxSource, 127 | files: { 128 | './demo.tsx': ` 129 | import leftPad from 'left-pad' 130 | 131 | export default () => leftPad("Neat demo!", 12, '!') 132 | `.trim(), 133 | }, 134 | }) 135 | 136 | // this test ensures that *not* passing leftPad as a global here 137 | // will work because I didn't externalize the left-pad module 138 | const Component = getMDXComponent(result.code) 139 | render(React.createElement(Component)) 140 | }) 141 | 142 | test('gives a handy error when the entry imports a module that cannot be found', async () => { 143 | const mdxSource = ` 144 | import Demo from './demo' 145 | 146 | 147 | `.trim() 148 | 149 | const error = /** @type Error */ ( 150 | await bundleMDX({ 151 | source: mdxSource, 152 | files: {}, 153 | }).catch(e => e) 154 | ) 155 | 156 | assert.match(error.message, `ERROR: Could not resolve "./demo"`) 157 | }) 158 | 159 | test('gives a handy error when importing a module that cannot be found', async () => { 160 | const mdxSource = ` 161 | import Demo from './demo' 162 | 163 | 164 | `.trim() 165 | 166 | const error = /** @type Error */ ( 167 | await bundleMDX({ 168 | source: mdxSource, 169 | files: { 170 | './demo.tsx': `import './blah-blah'`, 171 | }, 172 | }).catch(e => e) 173 | ) 174 | 175 | assert.equal( 176 | error.message, 177 | `Build failed with 1 error: 178 | demo.tsx:1:7: ERROR: Could not resolve "./blah-blah"`, 179 | ) 180 | }) 181 | 182 | test('gives a handy error when a file of an unsupported type is provided', async () => { 183 | const mdxSource = ` 184 | import Demo from './demo.blah' 185 | 186 | 187 | `.trim() 188 | 189 | const error = /** @type Error */ ( 190 | await bundleMDX({ 191 | source: mdxSource, 192 | files: { 193 | './demo.blah': `what even is this?`, 194 | }, 195 | }).catch(e => e) 196 | ) 197 | 198 | assert.match( 199 | error.message, 200 | `ERROR: [plugin: inMemory] Invalid loader value: "blah"`, 201 | ) 202 | }) 203 | 204 | test('files is optional', async () => { 205 | await bundleMDX({source: 'hello'}) 206 | }) 207 | 208 | test('uses the typescript loader where needed', async () => { 209 | const mdxSource = ` 210 | import Demo from './demo' 211 | 212 | 213 | `.trim() 214 | 215 | const {code} = await bundleMDX({ 216 | source: mdxSource, 217 | files: { 218 | './demo.tsx': ` 219 | import * as React from 'react' 220 | import {left} from './left' 221 | 222 | const Demo: React.FC = () => { 223 | return

{left("TypeScript")}

224 | } 225 | 226 | export default Demo 227 | `.trim(), 228 | './left.ts': ` 229 | import leftPad from 'left-pad' 230 | 231 | export const left = (s: string): string => { 232 | return leftPad(s, 12, '!') 233 | } 234 | `.trim(), 235 | }, 236 | }) 237 | 238 | const Component = getMDXComponent(code) 239 | 240 | const {container} = render(React.createElement(Component)) 241 | assert.match(container.innerHTML, '!!TypeScript') 242 | }) 243 | 244 | test('can specify "node_modules" in the files', async () => { 245 | const mdxSource = ` 246 | import LeftPad from 'left-pad-js' 247 | 248 | Hi 249 | `.trim() 250 | 251 | const {code} = await bundleMDX({ 252 | source: mdxSource, 253 | files: { 254 | 'left-pad-js': `export default () =>
this is left pad
`, 255 | }, 256 | }) 257 | 258 | const Component = getMDXComponent(code) 259 | 260 | const {container} = render(React.createElement(Component)) 261 | 262 | assert.match(container.innerHTML, 'this is left pad') 263 | }) 264 | 265 | test('should respect the configured loader for files', async () => { 266 | const mdxSource = ` 267 | # Title 268 | 269 | import {Demo} from './demo' 270 | 271 | 272 | `.trim() 273 | 274 | const files = { 275 | './demo.ts': ` 276 | import React from 'react' 277 | 278 | export const Demo: React.FC = () => { 279 | return

Sample

280 | } 281 | `.trim(), 282 | } 283 | 284 | const {code} = await bundleMDX({ 285 | source: mdxSource, 286 | files, 287 | esbuildOptions: options => { 288 | options.loader = { 289 | ...options.loader, 290 | '.ts': 'tsx', 291 | } 292 | 293 | return options 294 | }, 295 | }) 296 | 297 | const Component = getMDXComponent(code) 298 | 299 | const {container} = render(React.createElement(Component)) 300 | 301 | assert.match(container.innerHTML, 'Sample') 302 | }) 303 | 304 | test('require from current directory', async () => { 305 | const mdxSource = ` 306 | # Title 307 | 308 | import {Sample} from './sample-component' 309 | 310 | 311 | 312 | ![A Sample Image](./150.png) 313 | `.trim() 314 | 315 | const {code} = await bundleMDX({ 316 | source: mdxSource, 317 | cwd: path.join(process.cwd(), 'other'), 318 | mdxOptions: options => { 319 | options.remarkPlugins = [remarkMdxImages] 320 | 321 | return options 322 | }, 323 | esbuildOptions: options => { 324 | options.loader = { 325 | ...options.loader, 326 | '.png': 'dataurl', 327 | } 328 | 329 | return options 330 | }, 331 | }) 332 | 333 | const Component = getMDXComponent(code) 334 | 335 | const {container} = render(React.createElement(Component)) 336 | 337 | assert.match(container.innerHTML, 'Sample!') 338 | // Test that the React components image is imported correctly. 339 | assert.match(container.innerHTML, 'img src="data:image/png') 340 | // Test that the markdowns image is imported correctly. 341 | assert.match( 342 | container.innerHTML, 343 | 'img alt="A Sample Image" src="data:image/png', 344 | ) 345 | }) 346 | 347 | test('should output assets', async () => { 348 | const mdxSource = ` 349 | # Sample Post 350 | 351 | ![Sample Image](./150.png) 352 | `.trim() 353 | 354 | const {code} = await bundleMDX({ 355 | source: mdxSource, 356 | cwd: path.join(process.cwd(), 'other'), 357 | bundleDirectory: path.join(process.cwd(), 'output'), 358 | bundlePath: '/img/', 359 | mdxOptions: options => { 360 | options.remarkPlugins = [remarkMdxImages] 361 | 362 | return options 363 | }, 364 | esbuildOptions: options => { 365 | options.loader = { 366 | ...options.loader, 367 | '.png': 'file', 368 | } 369 | 370 | return options 371 | }, 372 | }) 373 | 374 | const Component = getMDXComponent(code) 375 | 376 | const {container} = render(React.createElement(Component)) 377 | 378 | assert.match(container.innerHTML, 'src="/img/150') 379 | 380 | const writeError = /** @type Error */ ( 381 | await bundleMDX({ 382 | source: mdxSource, 383 | cwd: path.join(process.cwd(), 'other'), 384 | mdxOptions: options => { 385 | options.remarkPlugins = [remarkMdxImages] 386 | 387 | return options 388 | }, 389 | esbuildOptions: options => { 390 | options.loader = { 391 | ...options.loader, 392 | // esbuild will throw its own error if we try to use `file` loader without `outdir` 393 | '.png': 'dataurl', 394 | } 395 | options.write = true 396 | 397 | return options 398 | }, 399 | }).catch(e => e) 400 | ) 401 | 402 | assert.equal( 403 | writeError.message, 404 | "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options", 405 | ) 406 | 407 | const optionError = /** @type Error */ ( 408 | await bundleMDX({ 409 | source: mdxSource, 410 | cwd: path.join(process.cwd(), 'other'), 411 | bundleDirectory: path.join(process.cwd(), 'output'), 412 | }).catch(e => e) 413 | ) 414 | 415 | assert.equal( 416 | optionError.message, 417 | 'When using `bundleDirectory` or `bundlePath` the other must be set.', 418 | ) 419 | }) 420 | 421 | test('should support importing named exports', async () => { 422 | const mdxSource = ` 423 | --- 424 | title: Example Post 425 | published: 2021-02-13 426 | description: This is some meta-data 427 | --- 428 | 429 | export const uncle = 'Bob' 430 | 431 | # {uncle} was indeed the uncle 432 | `.trim() 433 | 434 | const result = await bundleMDX({source: mdxSource}) 435 | 436 | /** @type {import('../types').MDXExport<{uncle: string}, {title: string, published: string, description: string}>} */ 437 | const mdxExport = getMDXExport(result.code) 438 | 439 | // remark-mdx-frontmatter exports frontmatter 440 | assert.equal(mdxExport.frontmatter, { 441 | title: 'Example Post', 442 | published: '2021-02-13', 443 | description: 'This is some meta-data', 444 | }) 445 | 446 | assert.equal(mdxExport.uncle, 'Bob') 447 | 448 | const {container} = render(React.createElement(mdxExport.default)) 449 | 450 | assert.equal(container.innerHTML, `

Bob was indeed the uncle

`) 451 | }) 452 | 453 | test('should support mdx from node_modules', async () => { 454 | const mdxSource = ` 455 | import MdxData from 'mdx-test-data' 456 | 457 | Local Content 458 | 459 | 460 | `.trim() 461 | 462 | const {code} = await bundleMDX({source: mdxSource}) 463 | 464 | const Component = getMDXComponent(code) 465 | 466 | const {container} = render(React.createElement(Component)) 467 | 468 | assert.match( 469 | container.innerHTML, 470 | 'Mdx file published as an npm package, for testing purposes.', 471 | ) 472 | }) 473 | 474 | test('should support mdx from VFile', async () => { 475 | const mdxSource = `# Heading` 476 | 477 | const vfile = new VFile({value: mdxSource, path: '/data/mdx/my-post.mdx'}) 478 | 479 | const {code} = await bundleMDX({source: vfile}) 480 | 481 | const Component = getMDXComponent(code) 482 | 483 | const {container} = render(React.createElement(Component)) 484 | 485 | assert.is(container.innerHTML, '

Heading

') 486 | }) 487 | 488 | test('should support mdx from VFile without path', async () => { 489 | const mdxSource = `# Heading` 490 | 491 | const vfile = new VFile({value: mdxSource}) 492 | 493 | const {code} = await bundleMDX({source: vfile}) 494 | 495 | const Component = getMDXComponent(code) 496 | 497 | const {container} = render(React.createElement(Component)) 498 | 499 | assert.is(container.innerHTML, '

Heading

') 500 | }) 501 | 502 | test('should provide VFile path to plugins', async () => { 503 | const mdxSource = `# Heading` 504 | 505 | const vfile = new VFile({value: mdxSource, path: '/data/mdx/my-post.mdx'}) 506 | 507 | /** @type {import('unified').Plugin} */ 508 | function plugin() { 509 | return function transformer(tree, file) { 510 | assert.is(file.path, '/data/mdx/my-post.mdx') 511 | } 512 | } 513 | 514 | const {code} = await bundleMDX({ 515 | source: vfile, 516 | mdxOptions(options) { 517 | options.remarkPlugins = [plugin] 518 | return options 519 | }, 520 | }) 521 | 522 | const Component = getMDXComponent(code) 523 | 524 | const {container} = render(React.createElement(Component)) 525 | 526 | assert.is(container.innerHTML, '

Heading

') 527 | }) 528 | 529 | test('should work with react-dom api', async () => { 530 | const mdxSource = ` 531 | import Demo from './demo' 532 | 533 | 534 | `.trim() 535 | 536 | const result = await bundleMDX({ 537 | source: mdxSource, 538 | files: { 539 | './demo.tsx': ` 540 | import * as ReactDOM from 'react-dom' 541 | 542 | function Demo() { 543 | return ReactDOM.createPortal( 544 |
Portal!
, 545 | document.body 546 | ) 547 | } 548 | 549 | export default Demo 550 | `.trim(), 551 | }, 552 | }) 553 | 554 | const Component = getMDXComponent(result.code) 555 | 556 | const {container} = render(React.createElement(Component), { 557 | container: document.body, 558 | }) 559 | 560 | assert.match(container.innerHTML, 'Portal!') 561 | }) 562 | 563 | test('should allow gray matter options to be accessed', async () => { 564 | const mdxSource = ` 565 | --- 566 | title: Sample 567 | date: 2021-07-27 568 | --- 569 | 570 | Some excerpt 571 | 572 | --- 573 | 574 | This is the rest of the content 575 | 576 | `.trim() 577 | 578 | const {matter} = await bundleMDX({ 579 | source: mdxSource, 580 | grayMatterOptions: options => { 581 | options.excerpt = true 582 | 583 | return options 584 | }, 585 | }) 586 | 587 | assert.equal((matter.excerpt ? matter.excerpt : '').trim(), 'Some excerpt') 588 | }) 589 | 590 | test('specify a file using bundleMDX', async () => { 591 | const {frontmatter} = await bundleMDX({ 592 | file: path.join(process.cwd(), 'other', 'sample.mdx'), 593 | cwd: path.join(process.cwd(), 'other'), 594 | esbuildOptions: options => { 595 | options.loader = { 596 | ...options.loader, 597 | '.png': 'dataurl', 598 | } 599 | 600 | return options 601 | }, 602 | }) 603 | 604 | assert.equal(frontmatter.title, 'Sample') 605 | }) 606 | 607 | test('let you use the front matter in config', async () => { 608 | await bundleMDX({ 609 | file: path.join(process.cwd(), 'other', 'sample.mdx'), 610 | cwd: path.join(process.cwd(), 'other'), 611 | esbuildOptions: (options, frontmatter) => { 612 | assert.equal(frontmatter.title, 'Sample') 613 | 614 | options.loader = { 615 | ...options.loader, 616 | '.png': 'dataurl', 617 | } 618 | 619 | return options 620 | }, 621 | }) 622 | }) 623 | 624 | test.run() 625 | -------------------------------------------------------------------------------- /src/__tests__/preact.js: -------------------------------------------------------------------------------- 1 | import './setup-tests.js' 2 | import * as Preact from "preact"; 3 | import * as PreactDOM from "preact/compat"; 4 | import * as _jsx_runtime from 'preact/jsx-runtime'; 5 | import {suite} from 'uvu' 6 | import * as assert from 'uvu/assert' 7 | import { render } from '@testing-library/preact' 8 | import {bundleMDX} from '../index.js' 9 | import {getMDXComponent} from '../client/jsx.js' 10 | 11 | const test = suite("preact"); 12 | 13 | const jsxBundlerConfig = { 14 | jsxLib: { 15 | varName: 'Preact', 16 | package: 'preact', 17 | }, 18 | jsxDom: { 19 | varName: 'PreactDom', 20 | package: 'preact/compat', 21 | }, 22 | jsxRuntime: { 23 | varName: '_jsx_runtime', 24 | package: 'preact/jsx-runtime', 25 | }, 26 | } 27 | const jsxComponentConfig = { Preact, PreactDOM, _jsx_runtime } 28 | 29 | const mdxSource = ` 30 | --- 31 | title: Example Post 32 | published: 2021-02-13 33 | description: This is some meta-data 34 | --- 35 | import Demo from './demo' 36 | 37 | # This is the title 38 | 39 | Here's a **neat** demo: 40 | 41 | `.trim(); 42 | 43 | const demoTsx = ` 44 | export default function Demo() { 45 | return
mdx-bundler with Preact's runtime!
46 | } 47 | `.trim(); 48 | 49 | 50 | test('smoke test for preact', async () => { 51 | 52 | const result = await bundleMDX({ 53 | source: mdxSource, 54 | jsxConfig: jsxBundlerConfig, 55 | files: { 56 | './demo.tsx': demoTsx 57 | } 58 | }); 59 | 60 | /** 61 | * @type {import('preact').FunctionComponent<{ components?: Record }>} 62 | */ 63 | const Component = getMDXComponent(result.code, jsxComponentConfig) 64 | 65 | /** @type {Preact.FunctionComponent<{ children:Preact.ComponentChildren }>} props */ 66 | const SpanBold = ({children}) => { 67 | return Preact.createElement('span', { className: "strong" }, children) 68 | } 69 | 70 | assert.equal(result.frontmatter, { 71 | title: 'Example Post', 72 | published: new Date('2021-02-13'), 73 | description: 'This is some meta-data', 74 | }) 75 | 76 | const {container} = render( 77 | Preact.h(Component, {components: {strong: SpanBold}}) 78 | ) 79 | 80 | assert.equal( 81 | container.innerHTML, 82 | `

This is the title

83 |

Here's a neat demo:

84 |
mdx-bundler with Preact's runtime!
`, 85 | ) 86 | }) 87 | 88 | test.run() -------------------------------------------------------------------------------- /src/__tests__/setup-tests.js: -------------------------------------------------------------------------------- 1 | import jsdomPkg from 'jsdom' 2 | process.env.NODE_ENV = 'test' 3 | 4 | const {JSDOM} = jsdomPkg 5 | const jsdom = new JSDOM('') 6 | const {window} = jsdom 7 | 8 | /** 9 | * @param {Object} src 10 | * @param {Object} target 11 | */ 12 | function copyProps(src, target) { 13 | Object.defineProperties(target, { 14 | ...Object.getOwnPropertyDescriptors(src), 15 | ...Object.getOwnPropertyDescriptors(target), 16 | }) 17 | } 18 | 19 | // @ts-expect-error TS2322 🤷‍♂️ 20 | global.window = window 21 | global.document = window.document 22 | // @ts-expect-error TS2740 🤷‍♂️ 23 | global.navigator = {userAgent: 'node.js'} 24 | global.requestAnimationFrame = callback => setTimeout(callback, 0) 25 | global.cancelAnimationFrame = id => clearTimeout(id) 26 | copyProps(window, global) 27 | -------------------------------------------------------------------------------- /src/__tests__/vue.js: -------------------------------------------------------------------------------- 1 | import './setup-tests.js' 2 | import * as Vue from "vue"; 3 | import * as _jsx_runtime from 'vue/jsx-runtime'; 4 | import { suite } from 'uvu' 5 | import * as assert from 'uvu/assert' 6 | import { render } from '@testing-library/vue' 7 | import { bundleMDX } from '../index.js' 8 | import { getMDXComponent } from '../client/jsx.js' 9 | 10 | 11 | const test = suite("vue"); 12 | 13 | const jsxBundlerConfig = { 14 | jsxLib: { 15 | varName: 'Vue', 16 | package: 'vue', 17 | }, 18 | jsxRuntime: { 19 | varName: '_jsx_runtime', 20 | package: 'vue/jsx-runtime', 21 | }, 22 | } 23 | const jsxComponentConfig = { Vue, _jsx_runtime } 24 | 25 | const mdxSource = ` 26 | --- 27 | title: Example Post 28 | published: 2021-02-13 29 | description: This is some meta-data 30 | --- 31 | import Demo from './demo' 32 | 33 | # This is the title 34 | 35 | Here's a **neat** demo: 36 | 37 | `.trim(); 38 | 39 | const demoTsx = ` 40 | export default function Demo() { 41 | return
mdx-bundler with Vue's runtime!
42 | } 43 | `.trim(); 44 | 45 | 46 | test('smoke test for vue', async () => { 47 | 48 | const result = await bundleMDX({ 49 | source: mdxSource, 50 | jsxConfig: jsxBundlerConfig, 51 | files: { 52 | './demo.tsx': demoTsx 53 | } 54 | }); 55 | /** 56 | * @type {Vue.FunctionalComponent<{ components?: Record }>} 57 | */ 58 | const Component = getMDXComponent(result.code, jsxComponentConfig) 59 | 60 | 61 | const SpanBold = Vue.defineComponent({ 62 | setup(_, { slots }) { 63 | return () => Vue.h('span', { "class": "strong" }, slots.default?.()) 64 | } 65 | }) 66 | 67 | assert.equal(result.frontmatter, { 68 | title: 'Example Post', 69 | published: new Date('2021-02-13'), 70 | description: 'This is some meta-data', 71 | }) 72 | 73 | const WrappedComponent = Vue.defineComponent({ 74 | setup() { 75 | return () => Vue.h(Component, { 76 | 'components': { strong: SpanBold } 77 | }) 78 | } 79 | }) 80 | 81 | 82 | const { container } = render(WrappedComponent) 83 | 84 | assert.equal( 85 | container.innerHTML, 86 | `

This is the title

87 |

Here's a neat demo:

88 |
mdx-bundler with Vue's runtime!
`, 89 | ) 90 | }) 91 | 92 | test.run() -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | export * from "./react" -------------------------------------------------------------------------------- /src/client/jsx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('../types').MDXContentProps} MDXContentProps 3 | */ 4 | 5 | /** 6 | * 7 | * @param {string} code - The string of code you got from bundleMDX 8 | * @param {Record} jsxGlobals - JSX globals 9 | * @param {Record} [globals] - Any variables your MDX needs to have accessible when it runs 10 | * @return {(props: MDXContentProps) => JSX.Element} 11 | */ 12 | function getMDXComponent(code, jsxGlobals, globals) { 13 | const mdxExport = getMDXExport(code, jsxGlobals, globals) 14 | return mdxExport.default 15 | } 16 | 17 | /** 18 | * @template {{}} ExportedObject 19 | * @template {{}} Frontmatter 20 | * @type {import('../types').MDXJsxExportFunction} 21 | * @param {string} code - The string of code you got from bundleMDX 22 | * @param {Record} jsxGlobals - JSX globals 23 | * @param {Record} [globals] - Any variables your MDX needs to have accessible when it runs 24 | * 25 | */ 26 | function getMDXExport(code, jsxGlobals, globals) { 27 | const scope = {...jsxGlobals, ...globals} 28 | // eslint-disable-next-line 29 | const fn = new Function(...Object.keys(scope), code) 30 | return fn(...Object.values(scope)) 31 | } 32 | 33 | export {getMDXComponent, getMDXExport} 34 | -------------------------------------------------------------------------------- /src/client/react.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import * as _jsx_runtime from 'react/jsx-runtime' 4 | 5 | /** 6 | * @typedef {import('../types').MDXContentProps} MDXContentProps 7 | */ 8 | 9 | /** 10 | * 11 | * @param {string} code - The string of code you got from bundleMDX 12 | * @param {Record} [globals] - Any variables your MDX needs to have accessible when it runs 13 | * @return {(props: MDXContentProps) => JSX.Element} 14 | */ 15 | function getMDXComponent(code, globals) { 16 | const mdxExport = getMDXExport(code, globals) 17 | return mdxExport.default 18 | } 19 | 20 | /** 21 | * @template {{}} ExportedObject 22 | * @template {{}} Frontmatter 23 | * @type {import('../types').MDXExportFunction} 24 | * @param {string} code - The string of code you got from bundleMDX 25 | * @param {Record} [globals] - Any variables your MDX needs to have accessible when it runs 26 | * 27 | */ 28 | function getMDXExport(code, globals) { 29 | const jsxGlobals = {React, ReactDOM, _jsx_runtime} 30 | const scope = {...jsxGlobals, ...globals} 31 | // eslint-disable-next-line 32 | const fn = new Function(...Object.keys(scope), code) 33 | return fn(...Object.values(scope)) 34 | } 35 | 36 | export {getMDXComponent, getMDXExport} 37 | -------------------------------------------------------------------------------- /src/dirname-messed-up.cjs: -------------------------------------------------------------------------------- 1 | // __dirname isn't supported in ESM files 2 | // we could use import.meta, but that may not be supported by whatever bundler 3 | // folks may be using, so we'll just go with this... 4 | module.exports = !__dirname.includes('mdx-bundler') 5 | -------------------------------------------------------------------------------- /src/dirname-messed-up.cjs.d.ts: -------------------------------------------------------------------------------- 1 | export default boolean 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import {StringDecoder} from 'string_decoder' 4 | import grayMatter from 'gray-matter' 5 | import * as esbuild from 'esbuild' 6 | import {NodeResolvePlugin} from '@esbuild-plugins/node-resolve' 7 | import {globalExternals} from '@fal-works/esbuild-plugin-global-externals' 8 | import {v4 as uuid} from 'uuid' 9 | import dirnameMessedUp from './dirname-messed-up.cjs' 10 | 11 | const {readFile, unlink} = fs.promises 12 | 13 | /** 14 | * @type {import('./types').JsxConfig} 15 | */ 16 | const defaultJSXConfig = { 17 | jsxLib: { 18 | varName: 'React', 19 | package: 'react', 20 | }, 21 | jsxDom: { 22 | varName: 'ReactDOM', 23 | package: 'react-dom' 24 | }, 25 | jsxRuntime: { 26 | varName: '_jsx_runtime', 27 | package: 'react/jsx-runtime' 28 | } 29 | } 30 | 31 | /** 32 | * @template {{[key: string]: any}} Frontmatter 33 | * @param {import('./types').BundleMDX} options 34 | * @returns 35 | */ 36 | async function bundleMDX({ 37 | file, 38 | source, 39 | files = {}, 40 | mdxOptions = options => options, 41 | esbuildOptions = options => options, 42 | globals = {}, 43 | cwd = path.join(process.cwd(), `__mdx_bundler_fake_dir__`), 44 | grayMatterOptions = options => options, 45 | bundleDirectory, 46 | bundlePath, 47 | jsxConfig = defaultJSXConfig 48 | }) { 49 | /* c8 ignore start */ 50 | if (dirnameMessedUp && !process.env.ESBUILD_BINARY_PATH) { 51 | console.warn( 52 | `mdx-bundler warning: esbuild maybe unable to find its binary, if your build fails you'll need to set ESBUILD_BINARY_PATH. Learn more: https://github.com/kentcdodds/mdx-bundler/blob/main/README.md#nextjs-esbuild-enoent`, 53 | ) 54 | } 55 | /* c8 ignore stop */ 56 | 57 | // @mdx-js/esbuild is a native ESM, and we're running in a CJS context. This is the 58 | // only way to import ESM within CJS 59 | const [ 60 | {default: mdxESBuild}, 61 | {default: remarkFrontmatter}, 62 | {default: remarkMdxFrontmatter}, 63 | ] = await Promise.all([ 64 | import('@mdx-js/esbuild'), 65 | import('remark-frontmatter'), 66 | import('remark-mdx-frontmatter'), 67 | ]) 68 | 69 | let /** @type string */ code, 70 | /** @type string */ entryPath, 71 | /** @type Omit, "data"> & {data: Frontmatter} */ matter 72 | 73 | /** @type Record */ 74 | const absoluteFiles = {} 75 | 76 | const isWriting = typeof bundleDirectory === 'string' 77 | 78 | if (typeof bundleDirectory !== typeof bundlePath) { 79 | throw new Error( 80 | 'When using `bundleDirectory` or `bundlePath` the other must be set.', 81 | ) 82 | } 83 | 84 | /** @type {(vfile: unknown) => vfile is import('vfile').VFile} */ 85 | function isVFile(vfile) { 86 | return typeof vfile === 'object' && vfile !== null && 'value' in vfile 87 | } 88 | 89 | if (typeof source === 'string') { 90 | // The user has supplied MDX source. 91 | /** @type any */ // Slight type hack to get the graymatter front matter typed correctly. 92 | const gMatter = grayMatter(source, grayMatterOptions({})) 93 | matter = gMatter 94 | entryPath = path.join(cwd, `./_mdx_bundler_entry_point-${uuid()}.mdx`) 95 | absoluteFiles[entryPath] = source 96 | } else if (isVFile(source)) { 97 | const value = String(source.value) 98 | /** @type any */ // Slight type hack to get the graymatter front matter typed correctly. 99 | const gMatter = grayMatter(value, grayMatterOptions({})) 100 | matter = gMatter 101 | entryPath = source.path 102 | ? path.isAbsolute(source.path) 103 | ? source.path 104 | : path.join(source.cwd, source.path) 105 | : path.join(cwd, `./_mdx_bundler_entry_point-${uuid()}.mdx`) 106 | absoluteFiles[entryPath] = value 107 | } else if (typeof file === 'string') { 108 | // The user has supplied a file. 109 | /** @type any */ // Slight type hack to get the graymatter front matter typed correctly. 110 | const gMatter = grayMatter.read(file, grayMatterOptions({})) 111 | matter = gMatter 112 | entryPath = file 113 | /* c8 ignore start */ 114 | } else { 115 | // The user supplied neither file or source. 116 | // The typings should prevent reaching this point. 117 | // It is ignored from coverage as the tests wouldn't run in a way that can get here. 118 | throw new Error('`source` or `file` must be defined') 119 | } 120 | /* c8 ignore end*/ 121 | 122 | for (const [filepath, fileCode] of Object.entries(files)) { 123 | absoluteFiles[path.join(cwd, filepath)] = fileCode 124 | } 125 | 126 | /** @type import('esbuild').Plugin */ 127 | const inMemoryPlugin = { 128 | name: 'inMemory', 129 | setup(build) { 130 | build.onResolve({filter: /.*/}, ({path: filePath, importer}) => { 131 | if (filePath === entryPath) { 132 | return { 133 | path: filePath, 134 | pluginData: {inMemory: true, contents: absoluteFiles[filePath]}, 135 | } 136 | } 137 | 138 | const modulePath = path.resolve(path.dirname(importer), filePath) 139 | 140 | if (modulePath in absoluteFiles) { 141 | return { 142 | path: modulePath, 143 | pluginData: {inMemory: true, contents: absoluteFiles[modulePath]}, 144 | } 145 | } 146 | 147 | for (const ext of ['.js', '.ts', '.jsx', '.tsx', '.json', '.mdx']) { 148 | const fullModulePath = `${modulePath}${ext}` 149 | if (fullModulePath in absoluteFiles) { 150 | return { 151 | path: fullModulePath, 152 | pluginData: { 153 | inMemory: true, 154 | contents: absoluteFiles[fullModulePath], 155 | }, 156 | } 157 | } 158 | } 159 | 160 | // Return an empty object so that esbuild will handle resolving the file itself. 161 | return {} 162 | }) 163 | 164 | build.onLoad({filter: /.*/}, async ({path: filePath, pluginData}) => { 165 | if (pluginData === undefined || !pluginData.inMemory) { 166 | // Return an empty object so that esbuild will load & parse the file contents itself. 167 | return null 168 | } 169 | 170 | // the || .js allows people to exclude a file extension 171 | const fileType = (path.extname(filePath) || '.jsx').slice(1) 172 | const contents = absoluteFiles[filePath] 173 | 174 | if (fileType === 'mdx') return null 175 | 176 | /** @type import('esbuild').Loader */ 177 | let loader 178 | 179 | if ( 180 | build.initialOptions.loader && 181 | build.initialOptions.loader[`.${fileType}`] 182 | ) { 183 | loader = build.initialOptions.loader[`.${fileType}`] 184 | } else { 185 | loader = /** @type import('esbuild').Loader */ (fileType) 186 | } 187 | 188 | return { 189 | contents, 190 | loader, 191 | } 192 | }) 193 | }, 194 | } 195 | 196 | const buildOptions = esbuildOptions( 197 | { 198 | entryPoints: [entryPath], 199 | write: isWriting, 200 | outdir: isWriting ? bundleDirectory : undefined, 201 | publicPath: isWriting ? bundlePath : undefined, 202 | absWorkingDir: cwd, 203 | define: { 204 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'), 205 | }, 206 | jsx: "automatic", 207 | jsxImportSource: jsxConfig.jsxLib.package, 208 | plugins: [ 209 | globalExternals({ 210 | ...globals, 211 | [jsxConfig.jsxLib.package]: { 212 | varName: jsxConfig.jsxLib.varName, 213 | type: 'cjs', 214 | }, 215 | [jsxConfig.jsxRuntime.package]: { 216 | varName: jsxConfig.jsxRuntime.varName, 217 | type: 'cjs', 218 | }, 219 | ...(jsxConfig.jsxDom ? {[jsxConfig.jsxDom.package]: { 220 | varName: jsxConfig.jsxDom.varName, 221 | type: 'cjs', 222 | }} : {}) 223 | }), 224 | // eslint-disable-next-line new-cap 225 | NodeResolvePlugin({ 226 | extensions: ['.js', '.ts', '.jsx', '.tsx'], 227 | resolveOptions: {basedir: cwd}, 228 | }), 229 | inMemoryPlugin, 230 | mdxESBuild( 231 | mdxOptions( 232 | { 233 | remarkPlugins: [ 234 | remarkFrontmatter, 235 | [remarkMdxFrontmatter, {name: 'frontmatter'}], 236 | ], 237 | jsxImportSource: jsxConfig.jsxLib.package 238 | }, 239 | matter.data, 240 | ), 241 | ), 242 | ], 243 | bundle: true, 244 | format: 'iife', 245 | globalName: 'Component', 246 | minify: true, 247 | }, 248 | matter.data, 249 | ) 250 | 251 | const bundled = await esbuild.build(buildOptions) 252 | 253 | if (bundled.outputFiles) { 254 | const decoder = new StringDecoder('utf8') 255 | 256 | code = decoder.write(Buffer.from(bundled.outputFiles[0].contents)) 257 | } else if (buildOptions.outdir && buildOptions.write) { 258 | // We know that this has to be an array of entry point strings, with a single entry 259 | const entryFile = /** @type {{entryPoints: string[]}} */ (buildOptions) 260 | .entryPoints[0] 261 | 262 | const fileName = path.basename(entryFile).replace(/\.[^/.]+$/, '.js') 263 | 264 | code = (await readFile(path.join(buildOptions.outdir, fileName))).toString() 265 | 266 | await unlink(path.join(buildOptions.outdir, fileName)) 267 | } else { 268 | throw new Error( 269 | "You must either specify `write: false` or `write: true` and `outdir: '/path'` in your esbuild options", 270 | ) 271 | } 272 | 273 | return { 274 | code: `${code};return Component;`, 275 | frontmatter: matter.data, 276 | errors: bundled.errors, 277 | matter, 278 | } 279 | } 280 | 281 | export {bundleMDX} 282 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | // This file is for defining types that are annoying to define with jsdoc syntax 2 | // It has to be at the root so when the src directory is compiled into the dist 3 | // directory they both reference the same thing (because babel compiles away the 4 | // types from the .d.ts files and typescript doesn't copy the .d.ts file). 5 | // kcd-scripts could/should be updated to copy all .d.ts files to the dist directory. 6 | 7 | import type {Plugin, BuildOptions, Loader} from 'esbuild' 8 | import type {ModuleInfo} from '@fal-works/esbuild-plugin-global-externals' 9 | import type {Options} from '@mdx-js/esbuild/lib' 10 | import type {GrayMatterOption, Input, GrayMatterFile} from 'gray-matter' 11 | import type {MDXComponents} from 'mdx/types' 12 | import type {VFile,VFileOptions} from 'vfile' 13 | 14 | type ESBuildOptions = BuildOptions 15 | 16 | export type MDXContentProps = { 17 | [props: string]: unknown 18 | components?: MDXComponents 19 | } 20 | 21 | export type BundleMDX = 22 | | BundleMDXSource 23 | | BundleMDXFile 24 | 25 | export type BundleMDXSource = { 26 | /** 27 | * Your MDX source. 28 | */ 29 | source: string | VFile | VFileOptions 30 | file?: undefined 31 | } & BundleMDXOptions 32 | 33 | export type BundleMDXFile = { 34 | /** 35 | * The path to the mdx file on disk. 36 | */ 37 | file: string 38 | source?: undefined 39 | } & BundleMDXOptions 40 | 41 | type BundleMDXOptions = { 42 | /** 43 | * The dependencies of the MDX code to be bundled 44 | * 45 | * @example 46 | * ``` 47 | * bundleMDX({ 48 | * source: mdxString, 49 | * files: { 50 | * './components.tsx': ` 51 | * import * as React from 'react' 52 | * 53 | * type CounterProps = {initialCount: number, step: number} 54 | * 55 | * function Counter({initialCount = 0, step = 1}: CounterProps) { 56 | * const [count, setCount] = React.useState(initialCount) 57 | * const increment = () => setCount(c => c + step) 58 | * return 59 | * } 60 | * ` 61 | * }, 62 | * }) 63 | * ``` 64 | */ 65 | files?: Record 66 | /** 67 | * This allows you to modify the built-in MDX configuration (passed to @mdx-js/mdx compile). 68 | * This can be helpful for specifying your own remarkPlugins/rehypePlugins. 69 | * 70 | * @param vfileCompatible the path and contents of the mdx file being compiled 71 | * @param options the default options which you are expected to modify and return 72 | * @returns the options to be passed to @mdx-js/mdx compile 73 | * 74 | * @example 75 | * ``` 76 | * bundleMDX({ 77 | * source: mdxString, 78 | * mdxOptions(options) { 79 | * // this is the recommended way to add custom remark/rehype plugins: 80 | * // The syntax might look weird, but it protects you in case we add/remove 81 | * // plugins in the future. 82 | * options.remarkPlugins = [...(options.remarkPlugins ?? []), myRemarkPlugin] 83 | * options.rehypePlugins = [...(options.rehypePlugins ?? []), myRehypePlugin] 84 | * 85 | * return options 86 | * } 87 | * }) 88 | * ``` 89 | */ 90 | mdxOptions?: ( 91 | options: Options, 92 | frontmatter: Frontmatter, 93 | ) => Options 94 | /** 95 | * This allows you to modify the built-in esbuild configuration. This can be 96 | * especially helpful for specifying the compilation target. 97 | * 98 | * @example 99 | * ``` 100 | * bundleMDX({ 101 | * source: mdxString, 102 | * esbuildOptions(options) { 103 | * options.target = [ 104 | * 'es2020', 105 | * 'chrome58', 106 | * 'firefox57', 107 | * 'safari11', 108 | * 'edge16', 109 | * 'node12', 110 | * ] 111 | * return options 112 | * } 113 | * }) 114 | * ``` 115 | */ 116 | esbuildOptions?: ( 117 | options: ESBuildOptions, 118 | frontmatter: Frontmatter, 119 | ) => ESBuildOptions 120 | /** 121 | * Any variables you want treated as global variables in the bundling. 122 | * 123 | * NOTE: These do not have to be technically global as you will be providing 124 | * their values when you use getMDXComponent, but as far as esbuild is concerned 125 | * it will treat these values as global variables so they will not be included 126 | * in the bundle. 127 | * 128 | * @example 129 | * ``` 130 | * bundlMDX({ 131 | * source: mdxString, 132 | * globals: {'left-pad': 'myLeftPad'}, 133 | * }) 134 | * 135 | * // on the client side 136 | * 137 | * import leftPad from 'left-pad' 138 | * 139 | * const Component = getMDXComponent(result.code, {myLeftPad: leftPad}) 140 | * ``` 141 | */ 142 | globals?: Record 143 | /** 144 | * The current working directory for the mdx bundle. Supplying this allows 145 | * esbuild to resolve paths itself instead of using `files`. 146 | * 147 | * This could be the directory the mdx content was read from or in the case 148 | * of off-disk content a common root directory. 149 | * 150 | * @example 151 | * ``` 152 | * bundleMDX({ 153 | * source: mdxString 154 | * cwd: '/users/you/site/mdx_root' 155 | * }) 156 | * ``` 157 | */ 158 | cwd?: string 159 | /** 160 | * This allows you to configure the gray matter options. 161 | * 162 | * @example 163 | * ``` 164 | * bundleMDX({ 165 | * source: mdxString, 166 | * grayMatterOptions: (options) => { 167 | * options.excerpt = true 168 | * 169 | * return options 170 | * } 171 | * }) 172 | * ``` 173 | */ 174 | grayMatterOptions?: ( 175 | options: GrayMatterOption, 176 | ) => GrayMatterOption 177 | /** 178 | * This allows you to set the output directory of the bundle. You will need 179 | * to set `bundlePath` as well to give esbuild the public url to the folder. 180 | * 181 | * *Note, the javascrpt bundle will not be placed here, only assets 182 | * that can't be part of the main bundle.* 183 | * 184 | * @example 185 | * ``` 186 | * bundleMDX({ 187 | * file: '/path/to/file.mdx', 188 | * bundleDirectory: '/path/to/bundle' 189 | * bundlePath: '/path/to/public/bundle' 190 | * }) 191 | * ``` 192 | */ 193 | bundleDirectory?: string 194 | /** 195 | * @see bundleDirectory 196 | */ 197 | bundlePath?: string 198 | /** 199 | * Allows this to output code other than react. 200 | * Follow https://mdxjs.com/docs/getting-started/#jsx and JSX library's documentation to use 201 | * 202 | * @example 203 | * ``` 204 | * bundleMDX({ 205 | * jsxConfig: { 206 | * jsxLib: { 207 | * varName: 'HonoJSX', 208 | * package: 'hono/jsx', 209 | * }, 210 | * jsxDom: { 211 | * varName: 'HonoDOM', 212 | * package: 'hono/jsx/dom', 213 | * }, 214 | * jsxRuntime: { 215 | * varName: '_jsx_runtime', 216 | * package: 'hono/jsx/jsx-runtime', 217 | * }, 218 | * } 219 | * }) 220 | * ``` 221 | */ 222 | jsxConfig?: JsxConfig; 223 | }; 224 | 225 | export type JsxConfig = { 226 | jsxLib: { 227 | /** @default 'React' */ 228 | varName: string; 229 | /** @default 'react' */ 230 | package: string; 231 | } 232 | jsxDom?: { 233 | /** @default 'ReactDOM' */ 234 | varName: string; 235 | /** @default 'react-dom' */ 236 | package: string; 237 | } 238 | jsxRuntime: { 239 | /** @default '_jsx_runtime' */ 240 | varName: string; 241 | /** @default 'react/jsx-runtime' */ 242 | package: string; 243 | } 244 | } 245 | 246 | type MDXExport< 247 | ExportObject extends {}, 248 | Frontmatter = {[key: string]: unknown}, 249 | > = { 250 | default: (props: MDXContentProps) => JSX.Element, 251 | frontmatter: Frontmatter 252 | } & ExportObject 253 | 254 | type MDXJsxExportFunction< 255 | ExportedObject extends {}, 256 | Frontmatter extends Record, 257 | > = ( 258 | code: string, 259 | jsxGlobals: Record, 260 | globals?: Record, 261 | ) => ReturnType> 262 | 263 | type MDXExportFunction< 264 | ExportedObject extends {}, 265 | Frontmatter extends Record, 266 | > = ( 267 | code: string, 268 | globals?: Record, 269 | ) => ReturnType> 270 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/kcd-scripts/shared-tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "target": "ES2020", 6 | "module": "ES2020", 7 | "allowJs": true, 8 | "checkJs": true, 9 | "lib": [ 10 | "ES2021.String" 11 | ] 12 | } 13 | } 14 | --------------------------------------------------------------------------------