UNPKG

jsx-dom-runtime

Version:

A tiny 500-byte library for JSX syntax templates targeting the DOM. Supports HTML, SVG, and MathML tags

247 lines (236 loc) 10.2 kB
'use strict'; const svgTags = new Set(['altGlyph', 'altGlyphDef', 'altGlyphItem', 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'color-profile', 'cursor', 'defs', 'desc', 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence', 'filter', 'font', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignObject', 'g', 'glyph', 'glyphRef', 'hkern', 'image', 'line', 'linearGradient', 'marker', 'mask', 'metadata', 'missing-glyph', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tref', 'tspan', 'use', 'view', 'vkern', 'discard', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'solidcolor']); const voidHTMLTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; const allHTMLTags = [...voidHTMLTags, 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'blockquote', 'body', 'button', 'canvas', 'caption', 'cite', 'code', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'menu', 'menuitem', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'search', 'section', 'select', 'slot', 'small', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'u', 'ul', 'var', 'video', 'fencedframe', 'selectedcontent', 'acronym', 'applet', 'basefont', 'bgsound', 'big', 'blink', 'center', 'noframes', 'tt', 'strike', 'xmp', 'isindex', 'keygen']; const htmlTags = new Set(allHTMLTags); const mathmlTags = new Set(['annotation', 'annotation-xml', 'maction', 'math', 'merror', 'mfrac', 'mi', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mprescripts', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'semantics', 'mfenced', 'menclose', 'mlabeledtr', 'maligngroup', 'malignmark']); const htmlDOMAttributes = new Map(Object.entries({ acceptCharset: 'accept-charset', className: 'class', httpEquiv: 'http-equiv', htmlFor: 'for', xlinkHref: 'href' })); const voidElements = new Set(voidHTMLTags); const isStandardElement = tag => htmlTags.has(tag) || svgTags.has(tag) || mathmlTags.has(tag); const isJSXIdentifier = node => 'JSXIdentifier' === node.type; const isStandardNode = node => 'JSXOpeningElement' === node.type && isJSXIdentifier(node.name) && isStandardElement(node.name.name); const isVoidElement = node => isJSXIdentifier(node.name) && voidElements.has(node.name.name); const hasJSXChildren = node => Array.isArray(node.children) && node.children.length > 0; const rule$5 = { defaultOptions: [], meta: { type: 'problem', docs: { description: 'Disallow JSX spread attributes in HTML, SVG, or MathML tags.' }, schema: [], messages: { noSpread: 'SyntaxError: JSX spread attributes in HTML, SVG, or MathML elements are not allowed and will cause your app to crash at runtime.' } }, create(context) { return { JSXSpreadAttribute(node) { if (isStandardNode(node.parent)) { context.report({ node, messageId: 'noSpread' }); } } }; } }; const rule$4 = { defaultOptions: [], meta: { type: 'problem', docs: { description: 'A void element is an element in HTML that cannot have any child nodes (i.e., nested elements or text nodes). Void elements only have a start tag; end tags must not be specified for void elements.', url: 'https://developer.mozilla.org/en-US/docs/Glossary/Void_element' }, fixable: 'code', schema: [], messages: { noChildren: 'Void elements cannot have children or a children attribute. A void element is an element in HTML that cannot have any child nodes (i.e., nested elements or text nodes). Void elements only have a start tag; end tags must not be specified for void elements.', mustSelfClose: 'The void elements must use self-closing-tag syntax in their start tag instead of a closing tag.' } }, create(context) { return { JSXElement(node) { if (isVoidElement(node.openingElement)) { if (hasJSXChildren(node)) { context.report({ node: node.openingElement.name, messageId: 'noChildren' }); } else if (node.closingElement) { context.report({ node: node.closingElement.name, messageId: 'mustSelfClose', fix: fixer => { const end = node.openingElement.range[1]; return [fixer.replaceTextRange([end - 1, end], ' />'), fixer.remove(node.closingElement)]; } }); } } }, JSXAttribute(node) { if ('children' === node.name.name && isVoidElement(node.parent)) { context.report({ node: node.name, messageId: 'noChildren' }); } } }; } }; const rule$3 = { defaultOptions: [], meta: { type: 'suggestion', docs: { description: 'Disallow legacy event handlers (e.g., "onclick", "onchange") and suggest using the event directive syntax ("on:click", "on:change") instead.' }, schema: [], messages: { legacyEventHandler: 'Legacy event handler "{{name}}" detected. Use the event directive syntax: "{{expected}}" instead.' } }, create(context) { return { JSXAttribute(node) { if (isJSXIdentifier(node.name)) { const name = node.name.name; if (name.startsWith('on') && name.length > 2 && isStandardNode(node.parent)) { context.report({ node: node.name, messageId: 'legacyEventHandler', data: { name, expected: 'on:' + name.slice(2) } }); } } } }; } }; const rule$2 = { defaultOptions: [], meta: { type: 'problem', docs: { description: 'Disallow use of JSX spread children (e.g., {...items} as a child). Use the value directly instead.' }, fixable: 'code', schema: [], messages: { noSpreadChildren: 'JSX spread children are not allowed. Use the value directly as a child instead.' } }, create(context) { return { JSXSpreadChild(node) { context.report({ node, messageId: 'noSpreadChildren', fix: fixer => fixer.replaceText(node, '{' + context.sourceCode.getText(node.expression) + '}') }); } }; } }; const rule$1 = { defaultOptions: [], meta: { type: 'suggestion', docs: { description: 'Prefer HTML, SVG, or MathML attributes over DOM property names in JSX. This rule auto-fixes common DOM property names (like className, htmlFor) to their standard attribute equivalents (class, for, etc.) for better compatibility and output. If you want to use the property instead of the attribute, use the syntax prop:className, prop:htmlFor, etc.' }, fixable: 'code', schema: [], messages: { preferAttribute: 'Use the standard attribute name (e.g., class, for) instead of the DOM property (e.g., className, htmlFor) in HTML, SVG, or MathML elements. If you want to use the property instead, use the directive syntax: prop:className, prop:htmlFor, etc.' } }, create(context) { return { JSXAttribute(node) { const name = htmlDOMAttributes.get(node.name.name); if (name && isStandardNode(node.parent)) { context.report({ node: node.name, messageId: 'preferAttribute', fix: fixer => fixer.replaceText(node.name, name) }); } } }; } }; const rule = { defaultOptions: [], meta: { type: 'suggestion', docs: { description: 'Enforce importing from "jsx-dom-runtime" instead of "jsx-dom-runtime/jsx-runtime".' }, fixable: 'code', schema: [], messages: { jsxImport: 'Use import from "jsx-dom-runtime" instead of "jsx-dom-runtime/jsx-runtime"' } }, create(context) { return { ImportDeclaration(node) { if ('jsx-dom-runtime/jsx-runtime' === node.source.value) { context.report({ node: node.source, messageId: 'jsxImport', fix: fixer => { const q = node.source.raw.at(0); return fixer.replaceText(node.source, `${q}jsx-dom-runtime${q}`); } }); } } }; } }; const config = { plugins: { 'jsx-dom-runtime': { rules: { 'no-spread-attribute-in-dom-element': rule$5, 'no-children-in-void-element': rule$4, 'no-spread-children': rule$2, 'no-legacy-event-handler': rule$3, 'prefer-attributes-over-properties': rule$1, 'jsx-import': rule } } }, rules: { 'jsx-dom-runtime/no-spread-attribute-in-dom-element': 'error', 'jsx-dom-runtime/no-children-in-void-element': 'error', 'jsx-dom-runtime/no-spread-children': 'error', 'jsx-dom-runtime/no-legacy-event-handler': 'warn', 'jsx-dom-runtime/prefer-attributes-over-properties': 'error', 'jsx-dom-runtime/jsx-import': 'warn' }, languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } }; module.exports = config;