├── .gitignore ├── package-lock.json ├── package.json ├── readme.md └── src ├── index.js └── tests ├── isCompName.test.js ├── tester.js └── validation.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-validate-jsx-nesting", 3 | "version": "1.0.2", 4 | "description": "compile time JSX nesting validation", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "keywords": [ 10 | "jsx validator", 11 | "dom validator", 12 | "html validator" 13 | ], 14 | "author": "Manan Tank", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/MananTank/validate-jsx-nesting.git" 18 | }, 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@babel/plugin-syntax-jsx": "^7.16.7", 22 | "@types/jest": "^27.5.1", 23 | "jest": "^28.1.0" 24 | }, 25 | "dependencies": { 26 | "@babel/core": "^7.17.10", 27 | "validate-html-nesting": "^1.2.2" 28 | }, 29 | "peerDependencies": { 30 | "@babel/plugin-syntax-jsx": "latest" 31 | } 32 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-validate-jsx-nesting 2 | 3 | ### Compile time JSX nesting validation 4 | 5 | Example 6 | 7 | ```html 8 | Failed to Compile. 9 | 10 | Error: Invalid HTML nesting:
11 | 1 |
12 | 2 | Hello 13 | > 3 |
` ) and thus the rendered DOM will have a different structure than JSX structure.
25 |
26 | This is a big issue for frameworks that rely on JSX rendering the exact same elements in DOM. This can lead to unexpected behaviors.
27 |
28 |
29 |
30 | ### Validation library
31 |
32 | This babel plugin uses the [validate-html-nesting](https://github.com/MananTank/validate-html-nesting) library for validating HTML element nesting
33 |
34 |
35 |
36 | ## Install
37 |
38 | ```
39 | npm i -D babel-plugin-validate-jsx-nesting
40 | ```
41 |
42 |
43 |
44 | ## babel config
45 |
46 | Refer to the [babel config](https://babeljs.io/docs/en/configuration) guide to learn about configuring babel
47 |
48 | ## no options
49 |
50 | with this config, the plugin throws an error when invalid JSX nesting is found
51 |
52 | ```json
53 | {
54 | "plugins": ["validate-jsx-nesting"]
55 | }
56 | ```
57 |
58 |
59 |
60 | ### with `warnOnly: true` option
61 |
62 | With this config, the plugin logs a warning when invalid JSX nesting is found
63 |
64 | ```json
65 | {
66 | "plugins": [["validate-jsx-nesting", { "warnOnly": true }]]
67 | }
68 | ```
69 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const { isValidHTMLNesting } = require('validate-html-nesting');
2 |
3 | /** @typedef {{ warnOnly: boolean} | undefined} PluginOptions */
4 |
5 | /**
6 | * returns true if the tag is component's name
7 | * @param {string} tag
8 | * @returns
9 | */
10 |
11 | function isCompName(tag) {
12 | return tag[0] === tag[0].toUpperCase();
13 | }
14 |
15 | /**
16 | * jsx nesting validator plugin
17 | * @param {import('@babel/core')} babel
18 | */
19 | module.exports = function (babel) {
20 | const { types: t } = babel;
21 |
22 | /**
23 | * @type {import('@babel/core').Visitor<{}>} path
24 | */
25 | const domValidator = {
26 | JSXElement(path, state) {
27 | const elName = path.node.openingElement.name;
28 | if (t.isJSXIdentifier(elName)) {
29 | const elTagName = elName.name;
30 | // if not a component
31 | if (!isCompName(elTagName)) {
32 | const parent = path.parent;
33 | if (t.isJSXElement(parent)) {
34 | const parentElName = parent.openingElement.name;
35 | if (t.isJSXIdentifier(parentElName)) {
36 | const parentElTagName = parentElName.name;
37 | // if parent is not a component
38 | if (!isCompName(parentElTagName)) {
39 | if (!isValidHTMLNesting(parentElName.name, elName.name)) {
40 | // @ts-ignore
41 | const pluginOptions = /** @type {PluginOptions} */ (state && state.opts);
42 |
43 | const error = path.buildCodeFrameError(
44 | `Invalid HTML nesting: <${elName.name}> can not be child of <${parentElName.name}>`
45 | );
46 |
47 | if (pluginOptions && pluginOptions.warnOnly) {
48 | console.warn(error.message);
49 | } else {
50 | throw error;
51 | }
52 | }
53 | }
54 | }
55 | }
56 | }
57 | }
58 | },
59 | };
60 |
61 | return {
62 | name: 'validate-jsx-nesting', // not required
63 | visitor: domValidator,
64 | };
65 | };
66 |
67 | module.exports.isCompName = isCompName;
68 |
--------------------------------------------------------------------------------
/src/tests/isCompName.test.js:
--------------------------------------------------------------------------------
1 | const { isCompName } = require('../index');
2 |
3 | test('isCompName', () => {
4 | expect(isCompName('Foo')).toBe(true);
5 | expect(isCompName('foo')).toBe(false);
6 | });
7 |
--------------------------------------------------------------------------------
/src/tests/tester.js:
--------------------------------------------------------------------------------
1 | const { transform } = require('@babel/core');
2 | const syntaxJSX = require('@babel/plugin-syntax-jsx');
3 | const plugin = require('../index');
4 |
5 | /**
6 | * @param {string} inputCode
7 | * @returns
8 | */
9 | function testPlugin(inputCode) {
10 | return transform(inputCode, {
11 | plugins: [syntaxJSX, plugin],
12 | }).code;
13 | }
14 |
15 | /**
16 | * @param {string} inputCode
17 | * @returns
18 | */
19 | function testPluginWarnOnly(inputCode) {
20 | return transform(inputCode, {
21 | plugins: [syntaxJSX, [plugin, { warnOnly: true }]],
22 | }).code;
23 | }
24 |
25 | module.exports = {
26 | testPlugin,
27 | testPluginWarnOnly,
28 | };
29 |
--------------------------------------------------------------------------------
/src/tests/validation.test.js:
--------------------------------------------------------------------------------
1 | const { testPlugin, testPluginWarnOnly } = require('./tester');
2 |
3 | // validation logic is tested in `validate-html-nesting` package
4 |
5 | test('elements are tested', () => {
6 | expect(() => testPlugin('