├── .gitignore
├── .husky
└── pre-commit
├── LICENSE
├── README.md
├── assets
└── demo.png
├── package-lock.json
├── package.json
├── prettier.config.js
├── rollup.config.js
├── src
├── index.ts
├── rules
│ └── noInvalidJSXNesting.ts
└── tests
│ ├── isCompName.test.ts
│ ├── isJSXElement.test.ts
│ └── rules.test.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run prettier
5 | npm run test
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Manan Tank
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # eslint-plugin-validate-jsx-nesting
2 |
3 | Find Invalid HTML Nesting in JSX, like this:
4 |
5 |
6 |
7 | ## Why this validation is important?
8 |
9 | Without such validation, When JSX is converted to HTML and rendered in the DOM, the browser will try to fix the invalid nestings ( such as `
` inside `
` ) and thus the rendered DOM will have a different structure than the JSX structure.
10 |
11 | This is a big issue for frameworks that rely on JSX rendering the exact same elements in DOM. This can lead to unexpected behaviors.
12 |
13 | This plugin uses the [validate-html-nesting](https://github.com/MananTank/validate-html-nesting) library for validating HTML element nesting
14 |
15 |
16 |
17 | ### Framework agnostic
18 |
19 | This ESLint plugin works with any framework that uses JSX
20 |
21 |
22 |
23 | ## Install
24 |
25 | ```bash
26 | npm i -D eslint-plugin-validate-jsx-nesting
27 | ```
28 |
29 | ## Usage
30 |
31 | ### Step 1: Add the plugin in ESLint Config
32 |
33 | Add `"eslint-plugin-validate-jsx-nesting"` to the plugins section of your ESLint configuration file. You can omit the `"eslint-plugin-"` prefix if you want.
34 |
35 | ```json
36 | {
37 | "plugins": ["validate-jsx-nesting"]
38 | }
39 | ```
40 |
41 | ### Step 2: Add the Plugin's rule
42 |
43 | This plugin only has one rule `"no-invalid-jsx-nesting"`.
44 |
45 | Add the `"validate-jsx-nesting/no-invalid-jsx-nesting"` rule in your ESLint config file as shown below
46 |
47 | ```json
48 | "rules": {
49 | "validate-jsx-nesting/no-invalid-jsx-nesting": "error"
50 | }
51 | ```
52 |
53 |
54 |
55 | ## Testing Suite
56 |
57 | The core validation logic is in [validate-html-nesting](https://github.com/MananTank/validate-html-nesting) library and you can checkout the testing suite [here](https://github.com/MananTank/validate-html-nesting/blob/main/tests/validation.test.js).
58 |
59 |
60 |
61 |
62 | ## See also: Related Libraries
63 |
64 | - [babel-plugin-validate-jsx-nesting](https://github.com/MananTank/validate-jsx-nesting)
65 | - [validate-html-nesting](https://github.com/MananTank/validate-html-nesting)
66 |
--------------------------------------------------------------------------------
/assets/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MananTank/eslint-plugin-validate-jsx-nesting/072d30108ff4366240f44142a270820de923a250/assets/demo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-validate-jsx-nesting",
3 | "description": "ESLint Plugin for Validating JSX Nesting",
4 | "version": "0.1.0",
5 | "author": "Manan Tank",
6 | "main": "dist/index.js",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/MananTank/eslint-plugin-validate-jsx-nesting.git"
11 | },
12 | "scripts": {
13 | "clean": "rimraf dist",
14 | "build": "rollup -c",
15 | "prettier": "prettier . -w",
16 | "test": "jest",
17 | "prepublishOnly": "npm run clean && npm run build",
18 | "prepare": "husky install"
19 | },
20 | "files": [
21 | "README.md",
22 | "package.json",
23 | "dist",
24 | "assets"
25 | ],
26 | "keywords": [
27 | "eslint",
28 | "typescript",
29 | "jsx",
30 | "html validation",
31 | "jsx validation",
32 | "jsx nesting validation"
33 | ],
34 | "dependencies": {
35 | "validate-html-nesting": "^1.2.2"
36 | },
37 | "devDependencies": {
38 | "@rollup/plugin-commonjs": "^22.0.1",
39 | "@rollup/plugin-node-resolve": "^13.3.0",
40 | "@types/eslint": "7.2.6",
41 | "@types/estree": "0.0.46",
42 | "@types/estree-jsx": "^0.0.2",
43 | "@types/node": "14.14.21",
44 | "@typescript-eslint/parser": "4.13.0",
45 | "eslint": "7.18.0",
46 | "husky": "^8.0.1",
47 | "jest": "26.6.3",
48 | "prettier": "2.2.1",
49 | "pretty-quick": "3.1.0",
50 | "rimraf": "3.0.2",
51 | "rollup": "^2.76.0",
52 | "rollup-plugin-ts": "^3.0.2",
53 | "ts-jest": "26.4.4",
54 | "typescript": "4.1.3"
55 | },
56 | "peerDependencies": {
57 | "eslint": ">=4.0.0"
58 | },
59 | "jest": {
60 | "globals": {
61 | "ts-jest": {
62 | "diagnostics": false
63 | }
64 | },
65 | "transform": {
66 | "^.+\\.ts$": "ts-jest"
67 | },
68 | "testRegex": "(src/.*\\.test)\\.ts$",
69 | "testPathIgnorePatterns": [
70 | "/node_modules/",
71 | "\\.d\\.ts$",
72 | "lib/.*"
73 | ],
74 | "moduleFileExtensions": [
75 | "js",
76 | "ts",
77 | "json"
78 | ]
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'avoid',
3 | bracketSpacing: true,
4 | endOfLine: 'auto',
5 | printWidth: 80,
6 | semi: false,
7 | singleQuote: true,
8 | tabWidth: 2,
9 | useTabs: true,
10 | }
11 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from 'rollup-plugin-ts'
2 | import { nodeResolve } from '@rollup/plugin-node-resolve'
3 | import commonjs from '@rollup/plugin-commonjs'
4 |
5 | export default {
6 | input: 'src/index.ts',
7 | output: {
8 | file: 'dist/index.js',
9 | format: 'commonjs',
10 | exports: 'auto',
11 | },
12 | plugins: [typescript(), nodeResolve(), commonjs()],
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { noInvalidJSXNesting } from './rules/noInvalidJSXNesting'
2 |
3 | export default {
4 | rules: {
5 | 'no-invalid-jsx-nesting': noInvalidJSXNesting,
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/src/rules/noInvalidJSXNesting.ts:
--------------------------------------------------------------------------------
1 | import type { Rule } from 'eslint'
2 | import type { JSXElement, JSXIdentifier } from 'estree-jsx'
3 | import { isValidHTMLNesting } from 'validate-html-nesting'
4 |
5 | /** If the name starts with uppercase, it's a component name */
6 | export function isCompName(str: string) {
7 | return str[0] === str[0].toUpperCase()
8 | }
9 |
10 | /** node is JSXElement if it's type is JSXElement */
11 | export function isJSXElement(node: any): node is JSXElement {
12 | return typeof node === 'object' && node !== null && node.type === 'JSXElement'
13 | }
14 |
15 | export const noInvalidJSXNesting: Rule.RuleModule = {
16 | meta: {
17 | type: 'problem',
18 | },
19 | create(context) {
20 | return {
21 | JSXElement(node: any) {
22 | // get node and it's parent
23 | const jsxElement = node as JSXElement
24 | const parent = node.parent
25 |
26 | // return if node is not a native element
27 | const elName = jsxElement.openingElement.name
28 | if (elName.type !== 'JSXIdentifier') return
29 | if (isCompName(elName.name)) return
30 |
31 | // return if parent is not a native element
32 | if (!isJSXElement(parent)) return
33 | const parentElName = parent.openingElement.name
34 | if (parentElName.type !== 'JSXIdentifier') return
35 | if (isCompName(parentElName.name)) return
36 |
37 | // if both are native elements, check if the nesting is valid
38 | // return if nesting is valid
39 | if (isValidHTMLNesting(parentElName.name, elName.name)) return
40 |
41 | // report error if nesting is invalid
42 | context.report({
43 | node,
44 | message: `Invalid HTML nesting: <${elName.name}> can not be child of <${parentElName.name}>`,
45 | })
46 | },
47 | }
48 | },
49 | }
50 |
--------------------------------------------------------------------------------
/src/tests/isCompName.test.ts:
--------------------------------------------------------------------------------
1 | import { isCompName } from '../rules/noInvalidJSXNesting'
2 |
3 | it('works', () => {
4 | expect(isCompName('div')).toBe(false)
5 | expect(isCompName('p')).toBe(false)
6 | expect(isCompName('Foo')).toBe(true)
7 | })
8 |
--------------------------------------------------------------------------------
/src/tests/isJSXElement.test.ts:
--------------------------------------------------------------------------------
1 | import { isJSXElement } from '../rules/noInvalidJSXNesting'
2 |
3 | it('works', () => {
4 | expect(isJSXElement(null)).toBe(false)
5 | expect(isJSXElement(undefined)).toBe(false)
6 | expect(isJSXElement(10)).toBe(false)
7 | expect(isJSXElement({})).toBe(false)
8 | expect(isJSXElement({ type: 'JSXElement' })).toBe(true)
9 | expect(isJSXElement({ type: 'VariableDeclaration' })).toBe(false)
10 | })
11 |
--------------------------------------------------------------------------------
/src/tests/rules.test.ts:
--------------------------------------------------------------------------------
1 | import { RuleTester } from 'eslint'
2 | import { noInvalidJSXNesting } from '../rules/noInvalidJSXNesting'
3 |
4 | const tester = new RuleTester({
5 | parserOptions: {
6 | ecmaVersion: 2015,
7 | sourceType: 'module',
8 | ecmaFeatures: {
9 | jsx: true,
10 | },
11 | },
12 | })
13 |
14 | tester.run('no-invalid-jsx-nesting', noInvalidJSXNesting, {
15 | valid: [
16 | { code: '
foo
' }, 17 | { code: '<>foo
foo
' }, 19 | { code: '<> foo >' }, 20 | { code: '
' }, 24 | ], 25 | invalid: [ 26 | { 27 | code: '
', 31 | }, 32 | ], 33 | }, 34 | { 35 | code: '
hi
', 36 | errors: [ 37 | { 38 | message: 'Invalid HTML nesting:can not be child of
', 39 | }, 40 | ], 41 | }, 42 | ], 43 | }) 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "esModuleInterop": true 7 | } 8 | } 9 | --------------------------------------------------------------------------------