Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Sep 18, 2023. It is now read-only.

Expose class on global #5

Merged
merged 10 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ JSON ESLint config example:
### Rules

- [Define Tag After Class Definition](./docs/rules/define-tag-after-class-definition.md)
- [Expose Class on Global](./docs/rules/expose-class-on-global.md)
56 changes: 56 additions & 0 deletions docs/rules/expose-class-on-global.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Expose Class on Global

It is good practice to have any element which is accessible via a tag-name to also have the class definition assigned to the global object. This is similar to how the built-in elements operate, for example the `div` element class definition is available under the `HTMLDivElement` global.

## Rule Details

This rule enforces that any class extending from `HTMLElement` also be assigned to a global variable matching that name.

👎 Examples of **incorrect** code for this rule:

```js
class FooBarElement extends HTMLElement {
// ...
}

// No assignment to `window.FooBarElement`
```
```js
class FooBarElement extends HTMLElement {
// ...
}

// Assigned, but using a different name to the class name
window.FooBar = FooBarElement
```

```js
// Assigned but as an Anonymous Class Expression
window.FooBarElement = class extends HTMLElement {
// ...
}
```

👍 Examples of **correct** code for this rule:

```js
class FooBarElement extends HTMLElement {
// ...
}

window.FooBarElement = FooBarElement
```

```js
window.FooBarElement = class FooBarElement extends HTMLElement {
// ...
}
```

## When Not To Use It

If you don't want your elements available as globals, you may disable this rule.

## Version

This rule was introduced in 0.0.1
17 changes: 12 additions & 5 deletions lib/class-ref-tracker.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
class ClassRefTracker {
constructor(context) {
constructor(context, extendsFrom) {
this.context = context
this.classes = new Set()
this.assignments = new Map()
this.extendsFrom = extendsFrom || (() => true)
}

add(node) {
if (!this.extendsFrom(node.superClass)) return false
if (node.type === 'ClassExpression') {
if (node.parent.type === 'AssignmentExpression') {
const expr = node.parent.left
Expand All @@ -15,20 +17,21 @@ class ClassRefTracker {
}
}
this.classes.add(node)
return true
}

get(node) {
if (node.type === 'ClassExpression' || node.type === 'ClassDeclaration') {
this.classes.add(node)
return node
if (node && (node.type === 'ClassExpression' || node.type === 'ClassDeclaration')) {
return this.classes.add(node) ? node : null
}
const name = this.context.getSourceCode().getText(node)
if (this.assignments.has(name)) {
return this.assignments.get(name)
} else {
const classVar = this.context.getScope().set.get(name)
if (classVar && classVar.defs.length === 1) {
return classVar.defs[0].node
const classDef = classVar.defs[0].node
return this.add(classDef) ? classDef : null
}
}
}
Expand All @@ -41,6 +44,10 @@ class ClassRefTracker {
[Symbol.iterator]() {
return this.classes[Symbol.iterator]()
}

static customElements(context) {
return new ClassRefTracker(context, superClassRef => /^HTML.*Element$/.test(superClassRef.name))
}
}

module.exports = ClassRefTracker
3 changes: 2 additions & 1 deletion lib/rules.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module.exports = {
'define-tag-after-class-definition': require('./rules/define-tag-after-class-definition')
'define-tag-after-class-definition': require('./rules/define-tag-after-class-definition'),
'expose-class-on-global': require('./rules/expose-class-on-global')
}
2 changes: 1 addition & 1 deletion lib/rules/define-tag-after-class-definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
},
schema: [],
create(context) {
const classes = new ClassRefTracker(context)
const classes = ClassRefTracker.customElements(context)
return {
[s.HTMLElementClass](node) {
classes.add(node)
Expand Down
29 changes: 29 additions & 0 deletions lib/rules/expose-class-on-global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const s = require('../custom-selectors')
const ClassRefTracker = require('../class-ref-tracker')
module.exports = {
meta: {
type: 'suggestion',
docs: {description: '', url: require('../url')(module)}
},
schema: [],
create(context) {
const classes = ClassRefTracker.customElements(context)
return {
[s.HTMLElementClass](node) {
classes.add(node)
},
['AssignmentExpression[left.object.name=window]:exit']: function (node) {
const classDef = classes.get(node.right)
classes.delete(classDef)
if (classDef && (!classDef.id || classDef.id.name !== node.left.property.name)) {
context.report(node.left.property, 'Custom Element global assignment must match class name')
}
},
['Program:exit']: function () {
for (const classDef of classes) {
context.report(classDef, 'Custom Element has not been exported onto `window`')
}
}
}
}
}
60 changes: 60 additions & 0 deletions test/expose-class-on-global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const rule = require('../lib/rules/expose-class-on-global')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester({env: {es2020: true}})

ruleTester.run('expose-class-on-global', rule, {
valid: [
{code: 'class SomeMap extends Map {}'},
{code: 'class SomeMap extends Map {}\nwindow.Other = SomeMap'},
{code: 'class FooBar extends HTMLElement {}\nwindow.FooBar = FooBar'},
{code: 'window.FooBar = class FooBar extends HTMLElement {}'}
],
invalid: [
{
code: 'class FooBar extends HTMLElement {}',
errors: [
{
message: 'Custom Element has not been exported onto `window`',
type: 'ClassDeclaration'
}
]
},
{
code: 'window.customElements.define("foo-bar", FooBar)\nclass FooBar extends HTMLElement {}',
errors: [
{
message: 'Custom Element has not been exported onto `window`',
type: 'ClassDeclaration'
}
]
},
{
code: 'class FooBar extends HTMLElement {}\nwindow.Other = FooBar',
errors: [
{
message: 'Custom Element global assignment must match class name',
type: 'Identifier'
}
]
},
{
code: 'window.Other = class FooBar extends HTMLElement {}',
errors: [
{
message: 'Custom Element global assignment must match class name',
type: 'Identifier'
}
]
},
{
code: 'window.Other = class extends HTMLElement {}',
errors: [
{
message: 'Custom Element global assignment must match class name',
type: 'Identifier'
}
]
}
]
})