├── .husky └── pre-commit ├── README.md ├── .prettierrc ├── examples ├── react │ ├── src │ │ ├── Sample.mdx │ │ ├── ambient.d.ts │ │ ├── main.tsx │ │ └── App.tsx │ ├── .stackblitzrc │ ├── README.md │ ├── vite.config.ts │ ├── tsconfig.json │ ├── index.html │ └── package.json ├── solid │ ├── src │ │ ├── Sample.mdx │ │ ├── main.tsx │ │ ├── ambient.d.ts │ │ └── App.tsx │ ├── .stackblitzrc │ ├── README.md │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ └── package.json └── preact │ ├── src │ ├── Sample.mdx │ ├── main.tsx │ ├── ambient.d.ts │ └── App.tsx │ ├── .stackblitzrc │ ├── README.md │ ├── vite.config.ts │ ├── tsconfig.json │ ├── index.html │ └── package.json ├── lint-staged.config.mjs ├── .npmrc ├── pnpm-workspace.yaml ├── packages └── vite-plugin-mdx │ ├── lint-staged.config.mjs │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── eslint.config.js │ ├── README.md │ ├── package.json │ └── src │ └── index.ts ├── ci ├── package.json └── ci.test.ts ├── version ├── .github ├── renovate.json └── workflows │ ├── cq.yml │ ├── ci.yml │ └── publish.yml ├── package.json ├── LICENSE └── .gitignore /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run precommit -- 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/vite-plugin-mdx/README.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } 4 | -------------------------------------------------------------------------------- /examples/react/src/Sample.mdx: -------------------------------------------------------------------------------- 1 | ## @cyco130/vite-plugin-mdx 2 | 3 | Hello React! 4 | -------------------------------------------------------------------------------- /examples/solid/src/Sample.mdx: -------------------------------------------------------------------------------- 1 | ## @cyco130/vite-plugin-mdx 2 | 3 | Hello Solid! 4 | -------------------------------------------------------------------------------- /examples/preact/src/Sample.mdx: -------------------------------------------------------------------------------- 1 | ## @cyco130/vite-plugin-mdx 2 | 3 | Hello Preact! 4 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "*": "prettier --ignore-unknown --write", 3 | }; 4 | -------------------------------------------------------------------------------- /examples/preact/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/react/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /examples/solid/.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = false 2 | strict-peer-dependencies=false 3 | side-effects-cache=false 4 | link-workspace-packages=true 5 | -------------------------------------------------------------------------------- /examples/preact/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | import App from "./App"; 3 | 4 | render(, document.getElementById("app")!); 5 | -------------------------------------------------------------------------------- /examples/solid/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web"; 2 | import App from "./App"; 3 | 4 | render(() => , document.getElementById("app")!); 5 | -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | # `@cyco130/vite-plugin-mdx` React example 2 | 3 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vite-plugin-mdx/tree/main/examples/react) 4 | -------------------------------------------------------------------------------- /examples/solid/README.md: -------------------------------------------------------------------------------- 1 | # `@cyco130/vite-plugin-mdx` Solid example 2 | 3 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vite-plugin-mdx/tree/main/examples/solid) 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - examples/* 4 | - ci 5 | 6 | onlyBuiltDependencies: 7 | - esbuild 8 | - puppeteer 9 | - unrs-resolver 10 | -------------------------------------------------------------------------------- /examples/preact/README.md: -------------------------------------------------------------------------------- 1 | # `@cyco130/vite-plugin-mdx` Preact example 2 | 3 | > [Try on StackBlitz](https://stackblitz.com/github/cyco130/vite-plugin-mdx/tree/main/examples/preact) 4 | -------------------------------------------------------------------------------- /examples/solid/src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.mdx" { 4 | const Component: import("solid-js").Component; 5 | export default Component; 6 | } 7 | -------------------------------------------------------------------------------- /examples/react/src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.mdx" { 4 | const Component: React.ComponentType<{ children?: never }>; 5 | export default Component; 6 | } 7 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | "**/*.ts?(x)": [ 3 | () => "tsc -p tsconfig.json --noEmit", 4 | "eslint --max-warnings 0 --ignore-pattern dist", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /examples/preact/src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import JSX = preact.JSX; 4 | 5 | declare module "*.mdx" { 6 | const Component: preact.ComponentType<{ children?: never }>; 7 | export default Component; 8 | } 9 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ["./src/index.ts"], 6 | format: ["esm"], 7 | platform: "node", 8 | target: "node18", 9 | dts: true, 10 | }, 11 | ]); 12 | -------------------------------------------------------------------------------- /examples/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /examples/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { mdx } from "@cyco130/vite-plugin-mdx"; 3 | import react from "@vitejs/plugin-react"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | // This should come _before_ plugin-react 8 | mdx(), 9 | react(), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /examples/preact/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import preact from "@preact/preset-vite"; 3 | import { mdx } from "@cyco130/vite-plugin-mdx"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | // This should come _before_ preact 8 | mdx({ jsxImportSource: "preact" }), 9 | preact(), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /examples/solid/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solid from "vite-plugin-solid"; 3 | import { mdx } from "@cyco130/vite-plugin-mdx"; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | // This should come _before_ solid 8 | mdx({ jsxImportSource: "solid-jsx" }), 9 | solid({ extensions: [".mdx"] }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /examples/preact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "resolveJsonModule": true, 11 | "jsx": "preserve", 12 | "noUncheckedIndexedAccess": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Bundler", 10 | "resolveJsonModule": true, 11 | "jsx": "react-jsx", 12 | "noUncheckedIndexedAccess": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/preact/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/solid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": false, 9 | "moduleResolution": "Bundler", 10 | "resolveJsonModule": true, 11 | "jsx": "preserve", 12 | "jsxImportSource": "solid-js", 13 | "noUncheckedIndexedAccess": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Sample from "./Sample.mdx"; 3 | 4 | export default function App() { 5 | const [count, setCount] = useState(0); 6 | 7 | return ( 8 | <> 9 |

Test

10 | 11 |

12 | 15 |

16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/preact/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | import Sample from "./Sample.mdx"; 3 | 4 | export default function App() { 5 | const [count, setCount] = useState(0); 6 | 7 | return ( 8 | <> 9 |

Test

10 | 11 |

12 | 15 |

16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /examples/solid/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "solid-js"; 2 | import Sample from "./Sample.mdx"; 3 | 4 | export default function App() { 5 | const [count, setCount] = createSignal(0); 6 | 7 | return ( 8 | <> 9 |

Test

10 | 11 |

12 | 15 |

16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /ci/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ci-react-vite-plugin-mdx", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "ci": "vitest --reporter=verbose run" 7 | }, 8 | "dependencies": { 9 | "kill-port": "^2.0.1", 10 | "node-fetch": "^3.3.2", 11 | "ps-tree": "^1.2.0", 12 | "puppeteer": "^24.17.1", 13 | "typescript": "^5.9.2", 14 | "vitest": "^3.2.4" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^24.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es2020", 5 | "module": "ESNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "resolveJsonModule": true, 12 | "noUncheckedIndexedAccess": true, 13 | "checkJs": true 14 | }, 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | WHITE='\033[1;37m' 3 | GREEN='\033[0;32m' 4 | BLUE='\033[1;34m' 5 | NC='\033[0m' 6 | 7 | echo -e ${WHITE}Updating package versions${NC} 8 | pnpm -r --filter=./packages/* exec -- bash -c "echo -e ${BLUE@Q}\$PNPM_PACKAGE_NAME${NC@Q}@${GREEN@Q}\`npm version --allow-same-version --no-git-tag-version $@\`${NC@Q}" 9 | echo -e ${WHITE}Updating dependency versions in examples${NC} 10 | pnpm -r --filter=./examples/* update --workspace --save-workspace-protocol=false @cyco130/vite-plugin-mdx 11 | -------------------------------------------------------------------------------- /examples/solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-solid-vite-plugin-mdx", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite serve", 7 | "build": "vite build", 8 | "start": "vite preview" 9 | }, 10 | "dependencies": { 11 | "solid-js": "^1.9.9" 12 | }, 13 | "devDependencies": { 14 | "@cyco130/vite-plugin-mdx": "2.2.0", 15 | "solid-jsx": "^1.1.4", 16 | "typescript": "^5.9.2", 17 | "vite": "^7.1.3", 18 | "vite-plugin-solid": "^2.11.8" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-preact-vite-plugin-mdx", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite serve", 7 | "build": "vite build", 8 | "start": "vite preview" 9 | }, 10 | "dependencies": { 11 | "preact": "^10.27.1" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.28.3", 15 | "@cyco130/vite-plugin-mdx": "2.2.0", 16 | "@preact/preset-vite": "^2.10.2", 17 | "typescript": "^5.9.2", 18 | "vite": "^7.1.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from "@cyco130/eslint-config/node"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const tsconfigRootDir = 6 | import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | /** @type {typeof config} */ 9 | export default [ 10 | ...config, 11 | { 12 | ignores: ["dist/", "node_modules/"], 13 | }, 14 | { 15 | languageOptions: { 16 | parserOptions: { 17 | projectService: true, 18 | tsconfigRootDir, 19 | }, 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-react-vite-plugin-mdx", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite serve", 7 | "build": "vite build", 8 | "start": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^19.1.1", 12 | "react-dom": "^19.1.1" 13 | }, 14 | "devDependencies": { 15 | "@cyco130/vite-plugin-mdx": "2.2.0", 16 | "@types/react": "^19.1.12", 17 | "@types/react-dom": "^19.1.9", 18 | "@vitejs/plugin-react": "5.0.2", 19 | "typescript": "^5.9.2", 20 | "vite": "^7.1.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "schedule:weekly", "group:allNonMajor"], 4 | "ignorePresets": ["ignorePaths"], 5 | "ignorePaths": ["**/node_modules/**", "**/dist/**"], 6 | "labels": ["dependencies"], 7 | "rangeStrategy": "bump", 8 | "packageRules": [ 9 | { 10 | "depTypeList": ["peerDependencies"], 11 | "enabled": false 12 | }, 13 | { 14 | "matchPackageNames": ["vfile"], 15 | "allowedVersions": "5" 16 | }, 17 | { 18 | "matchPackagePatterns": ["^@types/node$"], 19 | "allowedVersions": "18" 20 | } 21 | ], 22 | "ignoreDeps": [] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-mdx-workspace-root", 3 | "private": "true", 4 | "scripts": { 5 | "dev": "pnpm -r --parallel --filter \"./packages/*\" run dev", 6 | "build": "pnpm -r --filter \"./packages/*\" run build", 7 | "prepare": "husky install", 8 | "precommit": "lint-staged", 9 | "test": "pnpm run \"/^(ci|cq)$/\"", 10 | "cq": "pnpm run /^test:/", 11 | "ci": "cd ci && pnpm run ci", 12 | "test:packages": "pnpm -r --stream run test", 13 | "test:prettier": "prettier --check --ignore-path .gitignore --ignore-unknown . '!pnpm-lock.yaml'", 14 | "format": "prettier --ignore-path .gitignore --ignore-unknown . '!pnpm-lock.yaml' --write" 15 | }, 16 | "devDependencies": { 17 | "husky": "^9.1.7", 18 | "lint-staged": "^16.1.5", 19 | "prettier": "^3.6.2", 20 | "typescript": "^5.9.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Fatih Aygün 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/cq.yml: -------------------------------------------------------------------------------- 1 | name: Code quality checks 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | 8 | defaults: 9 | run: 10 | working-directory: . 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node_version: [22] 19 | fail-fast: false 20 | name: "Code quality checks on node-${{ matrix.node_version }}, ${{ matrix.os }}" 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v5 24 | 25 | - name: Install pnpm 26 | uses: pnpm/action-setup@v4 27 | with: 28 | version: 10 29 | 30 | - name: Set node version to ${{ matrix.node_version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node_version }} 34 | check-latest: true 35 | cache: "pnpm" 36 | 37 | - name: Install 38 | run: pnpm install 39 | 40 | - name: Build 41 | run: pnpm run build 42 | 43 | - name: Run code quality checks and tests 44 | run: pnpm run cq 45 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/README.md: -------------------------------------------------------------------------------- 1 | # @cyco130/vite-plugin-mdx 2 | 3 | This is a plugin for using [MDX](https://mdxjs.com/) in [Vite](https://vitejs.dev/) applications. It is tested with React, Preact and Solid but should work with any JSX implementation. 4 | 5 | ## Why not [`vite-plugin-mdx`](https://github.com/brillout/vite-plugin-mdx)? 6 | 7 | `vite-plugin-mdx` is currently unmaintained and only supports MDX version 1 whereas `@cyco130/vite-plugin-mdx` supports MDX version 3. In fact, this plugin may one day replace `vite-plugin-mdx`. 8 | 9 | ## Why not [`@mdx-js/rollup`](https://mdxjs.com/packages/rollup/)? 10 | 11 | In some cases it can interfere with Vite's dependency scanning. 12 | 13 | ## Examples 14 | 15 | - [React](./examples/react) ([StackBlitz](https://stackblitz.com/github/cyco130/vite-plugin-mdx/tree/main/examples/react)) 16 | - [Preact](./examples/preact) ([StackBlitz](https://stackblitz.com/github/cyco130/vite-plugin-mdx/tree/main/examples/preact)) 17 | - [Solid](./examples/solid) ([StackBlitz](https://stackblitz.com/github/cyco130/vite-plugin-mdx/tree/main/examples/solid)) 18 | 19 | ## Credits 20 | 21 | This plugin is heavily based on [`@mdx-js/rollup`](https://mdxjs.com/packages/rollup/). 22 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cyco130/vite-plugin-mdx", 3 | "version": "2.2.0", 4 | "type": "module", 5 | "files": [ 6 | "dist" 7 | ], 8 | "exports": { 9 | ".": "./dist/index.js" 10 | }, 11 | "typesVersions": { 12 | "*": { 13 | "*": [ 14 | "dist/*.d.ts" 15 | ] 16 | } 17 | }, 18 | "scripts": { 19 | "build": "tsup", 20 | "dev": "tsup --watch", 21 | "prepack": "rm -rf dist && pnpm build", 22 | "test": "pnpm test:typecheck && pnpm test:lint && pnpm test:package", 23 | "test:typecheck": "tsc -p tsconfig.json --noEmit", 24 | "test:lint": "eslint . --max-warnings 0 --ignore-pattern dist", 25 | "test:package": "publint --strict" 26 | }, 27 | "description": "Vite plugin for using MDX", 28 | "license": "MIT", 29 | "repository": "github:cyco130/vite-plugin-mdx", 30 | "keywords": [ 31 | "vite", 32 | "mdx" 33 | ], 34 | "peerDependencies": { 35 | "vite": "3 || 4 || 5 || 6" 36 | }, 37 | "devDependencies": { 38 | "@cyco130/eslint-config": "^6.0.2", 39 | "@types/node": "^24.3.0", 40 | "eslint": "^9.34.0", 41 | "publint": "^0.3.12", 42 | "tsup": "^8.5.0", 43 | "typescript": "^5.9.2", 44 | "vite": "^7.1.3" 45 | }, 46 | "dependencies": { 47 | "@mdx-js/mdx": "^3.1.0", 48 | "source-map": "^0.7.6", 49 | "vfile": "^6.0.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | pull_request: 7 | 8 | defaults: 9 | run: 10 | working-directory: . 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | node_version: [22] 19 | include: 20 | - os: ubuntu-latest 21 | node_version: 20 22 | - os: ubuntu-latest 23 | node_version: 24 24 | fail-fast: false 25 | name: "CI tests on node-${{ matrix.node_version }}, ${{ matrix.os }}" 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v5 29 | 30 | - name: Install pnpm 31 | uses: pnpm/action-setup@v4 32 | with: 33 | version: 10 34 | 35 | - name: Set node version to ${{ matrix.node_version }} 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node_version }} 39 | check-latest: true 40 | cache: "pnpm" 41 | 42 | - name: Install 43 | run: pnpm install 44 | 45 | - name: Build 46 | run: pnpm run build 47 | 48 | - name: Disable AppArmor on Ubuntu 49 | if: runner.os == 'Linux' 50 | run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns 51 | 52 | - name: Run CI tests 53 | run: pnpm run ci 54 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version to publish" 8 | required: true 9 | type: "string" 10 | tag: 11 | description: "Tag to publish" 12 | required: true 13 | type: "string" 14 | default: "latest" 15 | commit: 16 | description: "Should we commit the version bump?" 17 | required: false 18 | type: "boolean" 19 | default: true 20 | 21 | defaults: 22 | run: 23 | working-directory: . 24 | 25 | jobs: 26 | publish: 27 | runs-on: ubuntu-latest 28 | name: "Publish to NPM" 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Install pnpm 34 | uses: pnpm/action-setup@v4 35 | with: 36 | version: 9 37 | 38 | - name: Set node version to 20 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: 20 42 | registry-url: "https://registry.npmjs.org" 43 | cache: "pnpm" 44 | 45 | - name: Install 46 | run: pnpm install 47 | 48 | - name: Set up git user 49 | run: | 50 | git config --global user.name "GitHub Action Bot" 51 | git config --global user.email "<>" 52 | 53 | - name: Bump version 54 | run: "./version ${{ github.event.inputs.version }}" 55 | 56 | - name: "Publish to NPM" 57 | env: 58 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | run: pnpm -r publish --access public --no-git-checks --tag ${{ github.event.inputs.tag }} 60 | 61 | - name: "Commit version bump" 62 | if: ${{ github.event.inputs.commit == 'true' }} 63 | run: | 64 | git status 65 | git commit -am "release: ${{ github.event.inputs.version }}" 66 | git push 67 | git tag ${{ github.event.inputs.version }} 68 | git push --tags 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | .vscode 122 | 123 | # yarn v2 124 | .yarn/cache 125 | .yarn/unplugged 126 | .yarn/build-state.yml 127 | .yarn/install-state.gz 128 | .pnp.* 129 | 130 | # Local files 131 | *.local.* 132 | 133 | -------------------------------------------------------------------------------- /packages/vite-plugin-mdx/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { CompileOptions } from "@mdx-js/mdx"; 2 | import { Plugin, ResolvedConfig, createFilter, FilterPattern } from "vite"; 3 | import { SourceMapGenerator } from "source-map"; 4 | import fs from "node:fs"; 5 | import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors"; 6 | import { VFile } from "vfile"; 7 | 8 | export interface Options extends CompileOptions { 9 | /** 10 | * List of picomatch patterns to include 11 | */ 12 | include?: FilterPattern; 13 | /** 14 | * List of picomatch patterns to exclude 15 | */ 16 | exclude?: FilterPattern; 17 | /** 18 | * Use legacy resolution for older versions of Vite and React/Preact/Solid 19 | * plugins. 20 | * 21 | * @default false 22 | */ 23 | legacyResolution?: boolean; 24 | } 25 | 26 | export function mdx(options: Options = {}): Plugin { 27 | const { include, exclude, legacyResolution = false, ...rest } = options; 28 | 29 | const { extnames, process } = createFormatAwareProcessors({ 30 | SourceMapGenerator, 31 | ...rest, 32 | }); 33 | 34 | const filter = createFilter(include, exclude); 35 | 36 | return { 37 | name: "vite-plugin-mdx", 38 | 39 | enforce: "pre", 40 | 41 | config() { 42 | // A minimal ESBuild plugin to allow optimizedDeps scanning 43 | const esbuildOptions: ResolvedConfig["optimizeDeps"]["esbuildOptions"] = { 44 | plugins: [ 45 | { 46 | name: "mdx", 47 | setup(build) { 48 | build.onLoad({ filter: /\.mdx?$/ }, async (args) => { 49 | const contents = await fs.promises.readFile(args.path, "utf8"); 50 | 51 | const compiled = await process(contents); 52 | 53 | return { 54 | contents: String(compiled.value), 55 | loader: "jsx", 56 | }; 57 | }); 58 | }, 59 | }, 60 | ], 61 | }; 62 | 63 | return { 64 | optimizeDeps: { 65 | esbuildOptions, 66 | }, 67 | ssr: { 68 | optimizeDeps: { 69 | esbuildOptions, 70 | }, 71 | }, 72 | }; 73 | }, 74 | 75 | async resolveId(id, importer, options) { 76 | if (!legacyResolution) return; 77 | 78 | const { name, searchParams } = parseId(id); 79 | const extname = name.match(/(\.[^.]+)$/)?.[1]; 80 | if (extname && extnames.includes(extname)) { 81 | // Make sure ext=.jsx is at the very end 82 | searchParams.delete("ext"); 83 | searchParams.set("ext", ".jsx"); 84 | const query = "?" + searchParams.toString(); 85 | 86 | const resolved = await this.resolve(name + query, importer, { 87 | ...options, 88 | skipSelf: true, 89 | }); 90 | 91 | return resolved; 92 | } 93 | }, 94 | 95 | async transform(code, id) { 96 | const name = id.split("?", 1)[0]; 97 | const file = new VFile({ value: code, path: name }); 98 | 99 | if ( 100 | file.extname && 101 | filter(file.path) && 102 | extnames.includes(file.extname) 103 | ) { 104 | const compiled = await process(file); 105 | return { 106 | code: String(compiled.value), 107 | map: compiled.map, 108 | }; 109 | } 110 | }, 111 | }; 112 | } 113 | 114 | function parseId(id: string) { 115 | const questionMarkPos = id.indexOf("?"); 116 | let name: string; 117 | let query: string; 118 | 119 | if (questionMarkPos === -1) { 120 | name = id; 121 | query = ""; 122 | } else { 123 | name = id.slice(0, questionMarkPos); 124 | query = id.slice(questionMarkPos + 1); 125 | } 126 | 127 | return { 128 | name, 129 | searchParams: new URLSearchParams(query), 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /ci/ci.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, beforeAll, afterAll } from "vitest"; 2 | import puppeteer, { ElementHandle } from "puppeteer"; 3 | import path from "path"; 4 | import fs from "fs"; 5 | import { spawn, ChildProcess } from "child_process"; 6 | import fetch from "node-fetch"; 7 | import psTreeCb from "ps-tree"; 8 | import { promisify } from "util"; 9 | import { kill } from "process"; 10 | 11 | const psTree = promisify(psTreeCb); 12 | 13 | const TEST_HOST = "http://localhost:5174"; 14 | 15 | const browser = await puppeteer.launch({ 16 | headless: true, 17 | defaultViewport: { width: 1200, height: 800 }, 18 | }); 19 | 20 | const pages = await browser.pages(); 21 | const page = pages[0]; 22 | 23 | const cases = [ 24 | { framework: "React", env: "development" }, 25 | { framework: "React", env: "production" }, 26 | 27 | { framework: "Preact", env: "development" }, 28 | { framework: "Preact", env: "production" }, 29 | 30 | { framework: "Solid", env: "development" }, 31 | { framework: "Solid", env: "production" }, 32 | ] as const; 33 | 34 | describe.each(cases)("$framework - $env", ({ framework, env }) => { 35 | const dir = path.resolve( 36 | __dirname, 37 | "..", 38 | "examples", 39 | framework.toLowerCase(), 40 | ); 41 | 42 | let cp: ChildProcess | undefined; 43 | 44 | beforeAll(async () => { 45 | const command = 46 | env === "development" 47 | ? "pnpm exec vite serve --strictPort --port 5174" 48 | : "pnpm exec vite build && pnpm exec vite preview --strictPort --port 5174"; 49 | 50 | cp = spawn(command, { 51 | shell: true, 52 | stdio: "inherit", 53 | cwd: dir, 54 | }); 55 | 56 | // eslint-disable-next-line no-async-promise-executor 57 | await new Promise(async (resolve, reject) => { 58 | cp!.on("error", (error) => { 59 | cp = undefined; 60 | reject(error); 61 | }); 62 | 63 | cp!.on("exit", (code) => { 64 | if (code !== 0) { 65 | cp = undefined; 66 | reject(new Error(`Process exited with code ${code}`)); 67 | } 68 | }); 69 | 70 | for (;;) { 71 | let doBreak = false; 72 | await fetch(TEST_HOST) 73 | .then(async (r) => { 74 | if (r.status === 200) { 75 | resolve(); 76 | doBreak = true; 77 | } 78 | }) 79 | .catch(() => { 80 | // Ignore error 81 | }); 82 | 83 | if (doBreak) { 84 | break; 85 | } 86 | 87 | await new Promise((resolve) => setTimeout(resolve, 250)); 88 | } 89 | }).catch((error) => { 90 | console.error(error); 91 | process.exit(1); 92 | }); 93 | }, 60_000); 94 | 95 | afterAll(async () => { 96 | if (!cp || cp.exitCode || !cp.pid) { 97 | return; 98 | } 99 | 100 | const tree = await psTree(cp.pid); 101 | const pids = [cp.pid, ...tree.map((p) => +p.PID)]; 102 | 103 | for (const pid of pids) { 104 | kill(+pid, "SIGINT"); 105 | } 106 | 107 | await new Promise((resolve) => { 108 | cp!.on("exit", resolve); 109 | }); 110 | }); 111 | 112 | test("renders MDX", async () => { 113 | await page.goto(TEST_HOST + "/"); 114 | await page.waitForFunction( 115 | (framework) => document.body.innerText.includes(`Hello ${framework}!`), 116 | undefined, 117 | framework, 118 | ); 119 | }); 120 | 121 | if (env === "development") { 122 | test("hot reloads page", async () => { 123 | await page.goto(TEST_HOST); 124 | 125 | const button: ElementHandle = 126 | (await page.waitForSelector("button"))!; 127 | 128 | await button.click(); 129 | 130 | await page.waitForFunction( 131 | () => document.querySelector("button")?.textContent === "Clicked: 1", 132 | ); 133 | 134 | const filePath = path.resolve(dir, "src/Sample.mdx"); 135 | const oldContent = await fs.promises.readFile(filePath, "utf8"); 136 | const newContent = oldContent.replace("Hello", "Hot reloadin'"); 137 | 138 | if (process.platform === "win32") { 139 | await new Promise((resolve) => setTimeout(resolve, 1000)); 140 | } 141 | 142 | await fs.promises.writeFile(filePath, newContent); 143 | 144 | try { 145 | await page.waitForFunction( 146 | () => document.body.textContent?.includes("Hot reloadin'"), 147 | { timeout: 60_000 }, 148 | ); 149 | await page.waitForFunction( 150 | () => document.querySelector("button")?.textContent === "Clicked: 1", 151 | ); 152 | } finally { 153 | await fs.promises.writeFile(filePath, oldContent); 154 | } 155 | }, 60_000); 156 | } 157 | }); 158 | 159 | afterAll(async () => { 160 | await browser.close(); 161 | }); 162 | --------------------------------------------------------------------------------