diff --git a/README.md b/README.md index ee8013c..80070b3 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ JSON ESLint config example: - [No DOM Traversal in Connectedcallback](./docs/rules/no-dom-traversal-in-connectedcallback.md) - [No Exports with Element](./docs/rules/no-exports-with-element.md) - [No Method Prefixed with on](./docs/rules/no-method-prefixed-with-on.md) +- [No Unchecked Define](./docs/rules/no-unchecked-define.md) - [One Element Per File](./docs/rules/one-element-per-file.md) - [Tag Name Matches Class](./docs/rules/tag-name-matches-class.md) - [Valid Tag Name](./docs/rules/valid-tag-name.md) diff --git a/docs/rules/no-unchecked-define.md b/docs/rules/no-unchecked-define.md new file mode 100644 index 0000000..71ff71a --- /dev/null +++ b/docs/rules/no-unchecked-define.md @@ -0,0 +1,31 @@ +# No Unchecked Define + +Registering a custom element under the same tag as another defined custom element will cause a runtime exception in browsers. + +Some JavaScript might be inadvertently loaded twice on the same page in a web application, causing run time errors when the same element is registered twice. + +## Rule Details + +This rule ensures that all custom element definition calls are wrapped in guards if that element is already defined. + +👎 Examples of **incorrect** code for this rule: + +```js +window.customElements.define('foo-bar', class extends HTMLElement {}) +``` + +👍 Examples of **correct** code for this rule: + +```js +if (!window.customElements.get('foo-bar')) { + window.customElements.define('foo-bar', class extends HTMLElement {}) +} +``` + +## When Not To Use It + +If you are comfortable with the trade-offs of not checking if a custom element exists before defining it. + +## Version + +This rule was introduced in v0.0.1 diff --git a/lib/rules.js b/lib/rules.js index 076fa37..d8b7043 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -8,6 +8,7 @@ module.exports = { 'no-dom-traversal-in-connectedcallback': require('./rules/no-dom-traversal-in-connectedcallback'), 'no-exports-with-element': require('./rules/no-exports-with-element'), 'no-method-prefixed-with-on': require('./rules/no-method-prefixed-with-on'), + 'no-unchecked-define': require('./rules/no-unchecked-define'), 'one-element-per-file': require('./rules/one-element-per-file'), 'tag-name-matches-class': require('./rules/tag-name-matches-class'), 'valid-tag-name': require('./rules/valid-tag-name') diff --git a/lib/rules/no-unchecked-define.js b/lib/rules/no-unchecked-define.js new file mode 100644 index 0000000..807f189 --- /dev/null +++ b/lib/rules/no-unchecked-define.js @@ -0,0 +1,41 @@ +const s = require('../custom-selectors') + +let definedCustomElements = new Map() + +module.exports = { + meta: { + type: 'layout', + docs: {description: '', url: require('../url')(module)} + }, + schema: [], + create(context) { + definedCustomElements = new Map() + return { + [`IfStatement:matches([test.type=UnaryExpression],[test.type=BinaryExpression]) ${s.customElements.get}`](node) { + if (node.parent.type === 'UnaryExpression') { + let unaryCounter = 0 + let parent = node.parent + while (parent.type === 'UnaryExpression') { + unaryCounter++ + parent = parent.parent + } + if (unaryCounter % 2 !== 0) { + definedCustomElements.set(node.arguments[0].value, node) + } + } else { + definedCustomElements.set(node.arguments[0].value, node) + } + }, + [s.customElements.define](node) { + if (definedCustomElements.has(node.arguments[0].value)) { + definedCustomElements.delete(node.arguments[0].value) + } else { + context.report( + node, + 'Make sure to wrap customElements.define calls in checks to see if the element has already been defined' + ) + } + } + } + } +} diff --git a/test/no-unchecked-define.js b/test/no-unchecked-define.js new file mode 100644 index 0000000..4ca091f --- /dev/null +++ b/test/no-unchecked-define.js @@ -0,0 +1,83 @@ +const rule = require('../lib/rules/no-unchecked-define') +const RuleTester = require('eslint').RuleTester + +const ruleTester = new RuleTester({env: {es2020: true}}) +ruleTester.run('no-unchecked-define', rule, { + valid: [ + { + code: + 'if (!window.customElements.get("foo-bar")) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ' + }, + { + code: + 'if (!customElements.get("foo-bar")) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ' + }, + { + code: + 'if (customElements.get("foo-bar") == null) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ' + }, + { + code: + 'if (customElements.get("foo-bar") == undefined) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ' + }, + { + code: + 'if (!!!customElements.get("foo-bar")) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ' + } + ], + invalid: [ + { + code: 'window.customElements.define("foo-bar", class extends HTMLElement {})', + errors: [ + { + message: + 'Make sure to wrap customElements.define calls in checks to see if the element has already been defined', + type: 'CallExpression' + } + ] + }, + { + code: + 'if (customElements.get("foo-bar")) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ', + errors: [ + { + message: + 'Make sure to wrap customElements.define calls in checks to see if the element has already been defined', + type: 'CallExpression' + } + ] + }, + { + code: + 'if (!customElements.get("bar-foo")) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ', + errors: [ + { + message: + 'Make sure to wrap customElements.define calls in checks to see if the element has already been defined', + type: 'CallExpression' + } + ] + }, + { + code: 'customElements.define("foo-bar", class extends HTMLElement {})', + errors: [ + { + message: + 'Make sure to wrap customElements.define calls in checks to see if the element has already been defined', + type: 'CallExpression' + } + ] + }, + { + code: + 'if (!!customElements.get("foo-bar")) { window.customElements.define("foo-bar", class extends HTMLElement {}) } ', + errors: [ + { + message: + 'Make sure to wrap customElements.define calls in checks to see if the element has already been defined', + type: 'CallExpression' + } + ] + } + ] +})