eslint-plugin-mdx
Version:
ESLint Plugin for MDX
539 lines (426 loc) • 20.4 kB
Markdown
<p align="center">
<a href="https://eslint.org"><img src="https://eslint.org/icon.svg" height="50"></a>
<a href="#readme"><img src="https://rx-ts.github.io/assets/heart.svg" height="50"></a>
<a href="https://github.com/mdx-js/mdx"><img src="https://avatars.githubusercontent.com/u/37453691" height="50"></a>
</p>
[](https://github.com/mdx-js/eslint-mdx/actions/workflows/ci.yml?query=branch%3Amaster)
[](https://codecov.io/gh/mdx-js/eslint-mdx)
[](https://github.com/plantain-00/type-coverage)
[](https://github.com/mdx-js/eslint-mdx/releases)
[](https://renovatebot.com)
[](https://conventionalcommits.org)
[](https://github.com/prettier/prettier)
[](https://github.com/changesets/changesets)
> [ESLint][] Parser/Plugin for [MDX][], helps you lint all ES syntaxes.
> Linting `code` blocks can be enabled with `mdx/code-blocks` setting too!
> Work perfectly with `eslint-plugin-import`, `eslint-plugin-prettier` or any other eslint plugins.
> And also can be integrated with [remark-lint][] plugins to lint markdown syntaxes.
## TOC <!-- omit in toc -->
- [VSCode Extension](#vscode-extension)
- [Packages](#packages)
- [Install](#install)
- [Notice](#notice)
- [Usage](#usage)
- [Classic Config](#classic-config)
- [Flat Config](#flat-config)
- [Parser Options](#parser-options)
- [Parser API](#parser-api)
- [Markdown content nodes](#markdown-content-nodes)
- [MDX expression nodes](#mdx-expression-nodes)
- [Typings](#typings)
- [Rules](#rules)
- [mdx/remark](#mdxremark)
- [Prettier Integration](#prettier-integration)
- [Sponsors and Backers](#sponsors-and-backers)
- [Sponsors](#sponsors)
- [Backers](#backers)
- [Changelog](#changelog)
- [License](#license)
## VSCode Extension
[](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx)
[VSCode MDX][]: Integrates with [VSCode ESLint][], syntaxes highlighting and error reporting.
## Packages
This repository is a monorepo managed by [changesets][] what means we actually publish several packages to npm from same codebase, including:
| Package | Description | Version |
| -------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [`eslint-mdx`](/packages/eslint-mdx) | ESLint Parser for MDX | [](https://www.npmjs.com/package/eslint-mdx) |
| [`eslint-plugin-mdx`](/packages/eslint-plugin-mdx) | ESLint Plugin, Configuration and Rules for MDX | [](https://www.npmjs.com/package/eslint-plugin-mdx) |
## Install
```sh
# yarn
yarn add -D eslint-plugin-mdx
# npm
npm i -D eslint-plugin-mdx
```
## Notice
1. If you're using multi languages, `js/jsx/ts/tsx/vue`, etc for example, you'd better to always use [`overrides`](https://eslint.org/docs/user-guide/configuring/configuration-files#how-do-overrides-work) ([Classic Config](#classic-config)) or [`files`](https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-and-ignores) ([Flag Config](#flat-config)) feature of ESLint, because configs may be overridden by following configs.
See [#251](https://github.com/mdx-js/eslint-mdx/issues/251#issuecomment-736139224) for more details.
2. If you're using `{/* eslint-disable-line mdx/remark */}` with `prettier`, this won't work because `prettier` will add a blank line after the comment, which makes it invalid. You can use `{/* eslint-disable mdx/remark */}` paired with `{/* eslint-enable mdx/remark */}` instead:
<!-- eslint-skip -->
```mdx
{/* eslint-disable mdx/remark */}
# Heading
{/* eslint-enable mdx/remark */}
```
## Usage
### Classic Config
`.eslintrc` file:
```jsonc
{
"extends": ["plugin:mdx/recommended"],
// optional, if you want to lint code blocks at the same time
"settings": {
"mdx/code-blocks": true,
// optional, if you want to disable language mapper, set it to `false`
// if you want to override the default language mapper inside, you can provide your own
"mdx/language-mapper": {},
// optional, same as the `parserOptions.ignoreRemarkConfig`, you have to specify it twice unfortunately
"mdx/ignore-remark-config": true,
// optional, same as the `parserOptions.remarkConfigPath`, you have to specify it twice unfortunately
"mdx/remark-config-path": "path/to/your/remarkrc",
},
}
```
### Flat Config
`eslint.config.js` file:
```js
import * as mdx from 'eslint-plugin-mdx'
export default [
{
...mdx.flat,
// optional, if you want to lint code blocks at the same
processor: mdx.createRemarkProcessor({
lintCodeBlocks: true,
// optional, if you want to disable language mapper, set it to `false`
// if you want to override the default language mapper inside, you can provide your own
languageMapper: {},
// optional, same as the `parserOptions.ignoreRemarkConfig`, you have to specify it twice unfortunately
ignoreRemarkConfig: true,
// optional, same as the `parserOptions.remarkConfigPath`, you have to specify it twice unfortunately
remarkConfigPath: 'path/to/your/remarkrc',
}),
},
{
...mdx.flatCodeBlocks,
rules: {
...mdx.flatCodeBlocks.rules,
// if you want to override some rules for code blocks
'no-var': 'error',
'prefer-const': 'error',
},
},
]
```
Then, make sure ESLint knows to run on `.md` or `.mdx` files:
```sh
eslint . --ext js,md,mdx
```
## Parser Options
1. `extensions` (`string | string[]`): `eslint-mdx` will only resolve `.mdx` files by default, if you want to resolve other extensions as like `.mdx`, you can use this option.
2. `markdownExtensions` (`string | string[]`): `eslint-mdx` will only treat `.md` files as plain markdown by default, and will lint them via remark plugins. If you want to resolve other extensions as like `.md`, you can use this option.
3. `ignoreRemarkConfig` (`boolean`): Ignore the `remark` configuration defined in the project.
4. `remarkConfigPath` (`string`): Specify the path to the `remark` configuration file, could be relative to `CWD` or absolute path.
## Parser API
`eslint-mdx` returns an ESTree `Program`. In addition to standard ESTree and JSX nodes, it emits custom nodes for Markdown content and MDX expressions.
### Markdown content nodes
Markdown content uses the `MD` prefix, while MDX-specific nodes use `MDX`. The prefix avoids collisions with ESTree/JSX nodes. All nodes are exposed in `Program.body` or as children of other nodes and are traversed through the parser visitor keys.
| ESTree node | mdast node | Notes |
| ---------------------- | -------------------- | -------------------- |
| `MDBlockquote` | `blockquote` | |
| `MDBreak` | `break` | |
| `MDCode` | `code` | |
| `MDDefinition` | `definition` | |
| `MDDelete` | `delete` | `remark-gfm` |
| `MDEmphasis` | `emphasis` | |
| `MDFootnoteDefinition` | `footnoteDefinition` | `remark-gfm` |
| `MDFootnoteReference` | `footnoteReference` | `remark-gfm` |
| `MDHeading` | `heading` | |
| `MDImage` | `image` | |
| `MDImageReference` | `imageReference` | |
| `MDInlineCode` | `inlineCode` | |
| `MDInlineMath` | `inlineMath` | `remark-math` |
| `MDLink` | `link` | |
| `MDLinkReference` | `linkReference` | |
| `MDList` | `list` | |
| `MDListItem` | `listItem` | |
| `MDMath` | `math` | `remark-math` |
| `MDParagraph` | `paragraph` | |
| `MDStrong` | `strong` | |
| `MDTable` | `table` | `remark-gfm` |
| `MDTableCell` | `tableCell` | `remark-gfm` |
| `MDTableRow` | `tableRow` | `remark-gfm` |
| `MDText` | `text` | |
| `MDThematicBreak` | `thematicBreak` | |
| `MDToml` | `toml` | `remark-frontmatter` |
| `MDYaml` | `yaml` | `remark-frontmatter` |
For example:
````mdx
# Heading
Paragraph with **strong**, _emphasis_, `inlineCode`, [link](https://example.com), and .
> Blockquote
- list item
```js
const value = 1
```
````
> `MDXCode` and `MDXHeading` were renamed to `MDCode` and `MDHeading`; the old names are no longer emitted.
### MDX expression nodes
MDX expressions use the `MDX` prefix and expose their ESTree `expression`:
| ESTree node | mdast node |
| ------------------- | ------------------- |
| `MDXFlowExpression` | `mdxFlowExpression` |
| `MDXTextExpression` | `mdxTextExpression` |
When these expressions are used as JSX children, they are wrapped in `JSXExpressionContainer`. Empty or comment-only expressions use `JSXEmptyExpression`.
For example:
```mdx
{value}
> {value}
The value is {value}.
```
### Typings
```ts
import type { BaseNode, Expression } from 'estree'
import type {
JSXElement,
JSXEmptyExpression,
JSXExpressionContainer,
JSXFragment,
JSXSpreadChild,
JSXText,
} from 'estree-jsx'
export interface MDCode extends BaseNode {
type: 'MDCode'
value: string
lang?: string | null
meta?: string | null
}
export type HeadingDepth = 1 | 2 | 3 | 4 | 5 | 6
export interface MDHeading extends BaseNode {
type: 'MDHeading'
depth: HeadingDepth
children: MDContentNode[]
}
export interface MDXFlowExpression extends BaseNode {
type: 'MDXFlowExpression'
expression: Expression | JSXEmptyExpression
}
export interface MDXTextExpression extends BaseNode {
type: 'MDXTextExpression'
expression: Expression | JSXEmptyExpression
}
export interface MDText extends BaseNode {
type: 'MDText'
value: string
}
export interface MDParagraph extends BaseNode {
type: 'MDParagraph'
children: MDContentNode[]
}
export interface MDBlockquote extends BaseNode {
type: 'MDBlockquote'
children: MDContentNode[]
}
export interface MDList extends BaseNode {
type: 'MDList'
ordered?: boolean | null
start?: number | null
spread?: boolean | null
children: MDListItem[]
}
export interface MDListItem extends BaseNode {
type: 'MDListItem'
checked?: boolean | null
spread?: boolean | null
children: MDContentNode[]
}
export interface MDEmphasis extends BaseNode {
type: 'MDEmphasis'
children: MDContentNode[]
}
export interface MDStrong extends BaseNode {
type: 'MDStrong'
children: MDContentNode[]
}
export interface MDInlineCode extends BaseNode {
type: 'MDInlineCode'
value: string
}
export interface MDLink extends BaseNode {
type: 'MDLink'
url: string
title?: string | null
children: MDContentNode[]
}
export interface MDImage extends BaseNode {
type: 'MDImage'
url: string
title?: string | null
alt?: string | null
}
export interface MDBreak extends BaseNode {
type: 'MDBreak'
}
export interface MDThematicBreak extends BaseNode {
type: 'MDThematicBreak'
}
export interface MDDefinition extends BaseNode {
type: 'MDDefinition'
identifier: string
label?: string | null
url: string
title?: string | null
}
export interface MDDelete extends BaseNode {
type: 'MDDelete'
children: MDContentNode[]
}
export interface MDFootnoteDefinition extends BaseNode {
type: 'MDFootnoteDefinition'
identifier: string
label?: string | null
children: MDContentNode[]
}
export interface MDFootnoteReference extends BaseNode {
type: 'MDFootnoteReference'
identifier: string
label?: string | null
}
export interface MDImageReference extends BaseNode {
type: 'MDImageReference'
identifier: string
label?: string | null
referenceType: 'collapsed' | 'full' | 'shortcut'
alt?: string | null
}
export interface MDInlineMath extends BaseNode {
type: 'MDInlineMath'
value: string
}
export interface MDLinkReference extends BaseNode {
type: 'MDLinkReference'
identifier: string
label?: string | null
referenceType: 'collapsed' | 'full' | 'shortcut'
children: MDContentNode[]
}
export interface MDMath extends BaseNode {
type: 'MDMath'
value: string
meta?: string | null
}
export interface MDTable extends BaseNode {
type: 'MDTable'
align?: Array<'center' | 'left' | 'right' | null> | null
children: MDTableRow[]
}
export interface MDTableCell extends BaseNode {
type: 'MDTableCell'
children: MDContentNode[]
}
export interface MDTableRow extends BaseNode {
type: 'MDTableRow'
children: MDTableCell[]
}
export interface MDToml extends BaseNode {
type: 'MDToml'
value: string
}
export interface MDYaml extends BaseNode {
type: 'MDYaml'
value: string
}
export type MDContentNode =
| JSXElement
| JSXExpressionContainer
| JSXFragment
| JSXSpreadChild
| JSXText
| MDBlockquote
| MDBreak
| MDCode
| MDDefinition
| MDDelete
| MDEmphasis
| MDFootnoteDefinition
| MDFootnoteReference
| MDHeading
| MDImage
| MDImageReference
| MDInlineCode
| MDInlineMath
| MDLink
| MDLinkReference
| MDList
| MDListItem
| MDMath
| MDParagraph
| MDStrong
| MDTable
| MDTableCell
| MDTableRow
| MDText
| MDThematicBreak
| MDToml
| MDXFlowExpression
| MDXTextExpression
| MDYaml
```
## Rules
### mdx/remark
_possible fixable depends on your remark plugins_:
Integration with [remark-lint][] plugins, it will read [remark's configuration](https://github.com/remarkjs/remark/tree/master/packages/remark-cli#remark-cli) automatically via [unified-engine][]. But `.remarkignore` will not be respected, you should use `.eslintignore` instead.
If you want to disable or change severity of some related rules, it won't work by setting rules in eslint config like `'remark-lint-no-duplicate-headings': 0`, you should change your remark config instead like following:
```jsonc
{
"plugins": [
"@1stg/remark-config",
// change to error severity, notice `[]` is required
["lint-no-duplicate-headings", [2]],
// disable following plugin
[
"lint-no-multiple-toplevel-headings",
[0], // or false
],
],
}
```
## Prettier Integration
If you're using [remark-lint][] feature with [Prettier][] both together, you can try [remark-preset-prettier][] which helps you to _turn off all rules that are unnecessary or might conflict with [Prettier][]_.
```json
{
"plugins": [
"preset-lint-consistent",
"preset-lint-recommended",
"preset-lint-markdown-style-guide",
"preset-prettier"
]
}
```
## Sponsors and Backers
[](https://github.com/sponsors/JounQin)
### Sponsors
| 1stG | RxTS | UnRS | UnTS |
| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [](https://opencollective.com/1stG) | [](https://opencollective.com/rxts) | [](https://opencollective.com/unrs) | [](https://opencollective.com/unts) |
[](https://opencollective.com/unified)
### Backers
| 1stG | RxTS | UnRS | UnTS |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| [](https://opencollective.com/1stG) | [](https://opencollective.com/rxts) | [](https://opencollective.com/unrs) | [](https://opencollective.com/unts) |
[](https://opencollective.com/unified)
## Changelog
Detailed changes for each release are documented in [CHANGELOG.md](./CHANGELOG.md).
## License
[MIT][] © [JounQin][]@[1stG.me][]
[1stg.me]: https://www.1stG.me
[changesets]: https://github.com/changesets/changesets
[eslint]: https://eslint.org
[jounqin]: https://github.com/JounQin
[mdx]: https://github.com/mdx-js/mdx
[mit]: http://opensource.org/licenses/MIT
[prettier]: https://prettier.io
[remark-lint]: https://github.com/remarkjs/remark-lint
[remark-preset-prettier]: https://github.com/un-ts/remark-preset-prettier
[unified-engine]: https://github.com/unifiedjs/unified-engine
[vscode eslint]: https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
[vscode mdx]: https://github.com/mdx-js/mdx-analyzer/tree/main/packages/vscode-mdx