@discourse/lint-configs 2.45.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -6
- package/eslint-rules/no-at-class.mjs +46 -0
- package/eslint-rules/no-template-lint-directives.mjs +103 -0
- package/eslint-rules/no-unnecessary-tracked.mjs +10 -2
- package/eslint-rules/plugin-outlet-lazy-hash.mjs +43 -0
- package/eslint-rules/template-tag-no-self-this.mjs +23 -6
- package/eslint-rules/ui-kit-imports.mjs +543 -0
- package/eslint.mjs +58 -9
- package/package.json +3 -10
- package/template-lint-rules/index.mjs +0 -15
- package/template-lint-rules/no-at-class.mjs +0 -33
- package/template-lint-rules/no-implicit-this.mjs +0 -174
- package/template-lint-rules/plugin-outlet-lazy-hash.mjs +0 -27
- package/template-lint.config.cjs +0 -65
package/README.md
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const AFFECTED_COMPONENTS = new Set([
|
|
2
|
+
"DButton",
|
|
3
|
+
"DModal",
|
|
4
|
+
"TableHeaderToggle",
|
|
5
|
+
"Textarea",
|
|
6
|
+
"TextArea",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Disallow `@class` on Discourse components that own their root element.",
|
|
15
|
+
},
|
|
16
|
+
fixable: "code",
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
noAtClass: "Use `class` instead of `@class` for {{tag}}.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
GlimmerElementNode(node) {
|
|
26
|
+
if (!AFFECTED_COMPONENTS.has(node.tag) || !node.attributes) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const attribute of node.attributes) {
|
|
30
|
+
if (attribute.name !== "@class") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
context.report({
|
|
34
|
+
node: attribute,
|
|
35
|
+
messageId: "noAtClass",
|
|
36
|
+
data: { tag: node.tag },
|
|
37
|
+
fix(fixer) {
|
|
38
|
+
const [start] = attribute.range;
|
|
39
|
+
return fixer.replaceTextRange([start, start + 1], "");
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const DIRECTIVE_COMMENT =
|
|
2
|
+
/^(?<open>\{\{!(?:--)?)\s*template-lint-(?<action>disable|enable)(?<rules>\s+[^]*?)?\s*(?:--)?\}\}$/;
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "suggestion",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"Convert `{{! template-lint-disable }}` comments to `{{! eslint-disable }}` equivalents.",
|
|
10
|
+
},
|
|
11
|
+
fixable: "code",
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
convert: "Use `eslint-{{action}}` instead of `template-lint-{{action}}`.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
create(context) {
|
|
19
|
+
const sourceCode = context.sourceCode;
|
|
20
|
+
// Glimmer parks comments that appear inside an element's opening tag
|
|
21
|
+
// (between attributes) on `element.comments`, not in the children. We
|
|
22
|
+
// collect them so the Program:exit pass can lift a converted directive
|
|
23
|
+
// to before its enclosing element — ESLint scopes line-based directives
|
|
24
|
+
// from where they appear, and the violation typically lives on the
|
|
25
|
+
// element's start line, not on the in-attribute line.
|
|
26
|
+
const elementByCommentStart = new Map();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
GlimmerElementNode(node) {
|
|
30
|
+
for (const c of node.comments || []) {
|
|
31
|
+
if (c.range) {
|
|
32
|
+
elementByCommentStart.set(c.range[0], node);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"Program:exit"() {
|
|
38
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
39
|
+
const raw = sourceCode.text.slice(...comment.range);
|
|
40
|
+
const converted = convertComment(raw);
|
|
41
|
+
if (!converted) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const enclosingElement = elementByCommentStart.get(comment.range[0]);
|
|
45
|
+
context.report({
|
|
46
|
+
node: comment,
|
|
47
|
+
messageId: "convert",
|
|
48
|
+
data: { action: converted.action },
|
|
49
|
+
fix: (fixer) =>
|
|
50
|
+
enclosingElement
|
|
51
|
+
? liftBeforeElement(
|
|
52
|
+
fixer,
|
|
53
|
+
comment,
|
|
54
|
+
converted.newComment,
|
|
55
|
+
enclosingElement,
|
|
56
|
+
sourceCode
|
|
57
|
+
)
|
|
58
|
+
: fixer.replaceTextRange(comment.range, converted.newComment),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function convertComment(rawComment) {
|
|
67
|
+
const match = rawComment.match(DIRECTIVE_COMMENT);
|
|
68
|
+
if (!match) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const { open, action, rules: rulesPart } = match.groups;
|
|
72
|
+
// ESLint directives use comma-separated rule names; template-lint uses
|
|
73
|
+
// whitespace. Prefix each with `ember/template-` to land in the namespace
|
|
74
|
+
// where the ports live.
|
|
75
|
+
const rules = (rulesPart || "")
|
|
76
|
+
.trim()
|
|
77
|
+
.split(/\s+/)
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.map((r) => `ember/template-${r}`)
|
|
80
|
+
.join(", ");
|
|
81
|
+
const body = rules ? `eslint-${action} ${rules}` : `eslint-${action}`;
|
|
82
|
+
// Emit symmetric markers regardless of what the source did.
|
|
83
|
+
const close = open.length === 5 ? "--}}" : "}}";
|
|
84
|
+
return {
|
|
85
|
+
action,
|
|
86
|
+
newComment: `${open} ${body} ${close}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Strip the in-attribute comment line entirely (leading indent through
|
|
91
|
+
// trailing newline) and re-emit the converted directive on its own line at
|
|
92
|
+
// the element's indent, just above the element.
|
|
93
|
+
function liftBeforeElement(fixer, comment, newComment, element, sourceCode) {
|
|
94
|
+
const text = sourceCode.text;
|
|
95
|
+
const lineStart = comment.range[0] - comment.loc.start.column;
|
|
96
|
+
const lineEnd =
|
|
97
|
+
text[comment.range[1]] === "\n" ? comment.range[1] + 1 : comment.range[1];
|
|
98
|
+
const indent = " ".repeat(element.loc.start.column);
|
|
99
|
+
return [
|
|
100
|
+
fixer.removeRange([lineStart, lineEnd]),
|
|
101
|
+
fixer.insertTextBeforeRange(element.range, `${newComment}\n${indent}`),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
@@ -4,16 +4,24 @@ const MUTATING_COMPONENTS = {
|
|
|
4
4
|
"@value": [
|
|
5
5
|
"Input",
|
|
6
6
|
"Textarea",
|
|
7
|
+
"DTextarea",
|
|
7
8
|
"TextField",
|
|
9
|
+
"DTextField",
|
|
8
10
|
"DatePicker",
|
|
11
|
+
"DDatePicker",
|
|
9
12
|
"ChatChannelChooser",
|
|
10
13
|
],
|
|
11
14
|
"@checked": ["Input", "PreferenceCheckbox"],
|
|
12
|
-
"@selection": [
|
|
15
|
+
"@selection": [
|
|
16
|
+
"RadioButton",
|
|
17
|
+
"DRadioButton",
|
|
18
|
+
"InstallThemeItem",
|
|
19
|
+
"ChatToTopicSelector",
|
|
20
|
+
],
|
|
13
21
|
"@postAction": ["AdminPenaltyPostAction"],
|
|
14
22
|
"@reason": ["AdminPenaltyReason"],
|
|
15
23
|
"@tags": ["TagChooser", "ChatToTopicSelector"],
|
|
16
|
-
"@capsLockOn": ["PasswordField"],
|
|
24
|
+
"@capsLockOn": ["PasswordField", "DPasswordField"],
|
|
17
25
|
"@message": ["FlagActionType"],
|
|
18
26
|
"@isConfirmed": ["FlagActionType"],
|
|
19
27
|
"@topicTitle": ["ChatToTopicSelector"],
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
"Require `{{lazyHash}}` instead of `{{hash}}` for `@outletArgs` on `<PluginOutlet>`.",
|
|
7
|
+
},
|
|
8
|
+
schema: [],
|
|
9
|
+
messages: {
|
|
10
|
+
useLazyHash:
|
|
11
|
+
"Use {{lazyHash}} instead of {{hash}} for @outletArgs in <PluginOutlet>.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
GlimmerElementNode(node) {
|
|
18
|
+
if (node.tag !== "PluginOutlet" || !node.attributes) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const outletArgsAttr = node.attributes.find(
|
|
22
|
+
(attr) => attr.name === "@outletArgs"
|
|
23
|
+
);
|
|
24
|
+
if (!outletArgsAttr || !outletArgsAttr.value) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const value = outletArgsAttr.value;
|
|
28
|
+
if (
|
|
29
|
+
value.type === "GlimmerMustacheStatement" &&
|
|
30
|
+
value.path?.type === "GlimmerPathExpression" &&
|
|
31
|
+
value.path.head?.type === "VarHead" &&
|
|
32
|
+
value.path.head.name === "hash" &&
|
|
33
|
+
!value.path.tail?.length
|
|
34
|
+
) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: value,
|
|
37
|
+
messageId: "useLazyHash",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
const THIS_BINDING_TYPES = new Set([
|
|
2
|
+
"ClassDeclaration",
|
|
3
|
+
"ClassExpression",
|
|
4
|
+
"FunctionDeclaration",
|
|
5
|
+
"FunctionExpression",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function findThisBindingAncestor(target) {
|
|
9
|
+
let current = target.parent;
|
|
10
|
+
while (current) {
|
|
11
|
+
if (THIS_BINDING_TYPES.has(current.type)) {
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
current = current.parent;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
export default {
|
|
2
20
|
meta: {
|
|
3
21
|
type: "suggestion",
|
|
@@ -24,16 +42,15 @@ export default {
|
|
|
24
42
|
(ref) => ref.identifier.parent !== node
|
|
25
43
|
);
|
|
26
44
|
|
|
45
|
+
const declarationThisBinding = findThisBindingAncestor(node);
|
|
46
|
+
|
|
27
47
|
const referencedOnlyInAdjacentTemplateTag = references.every((ref) => {
|
|
28
48
|
if (ref.identifier.type !== "VarHead") {
|
|
29
49
|
return false;
|
|
30
50
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
context.sourceCode.getScope(node).variableScope;
|
|
35
|
-
|
|
36
|
-
return hasSameVariableScope;
|
|
51
|
+
return (
|
|
52
|
+
findThisBindingAncestor(ref.identifier) === declarationThisBinding
|
|
53
|
+
);
|
|
37
54
|
});
|
|
38
55
|
|
|
39
56
|
if (referencedOnlyInAdjacentTemplateTag) {
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a kebab-case string to PascalCase.
|
|
3
|
+
* @param {string} str - e.g. "d-async-content"
|
|
4
|
+
* @returns {string} - e.g. "DAsyncContent"
|
|
5
|
+
*/
|
|
6
|
+
function kebabToPascal(str) {
|
|
7
|
+
return str
|
|
8
|
+
.split("-")
|
|
9
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
10
|
+
.join("");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a kebab-case string to camelCase.
|
|
15
|
+
* @param {string} str - e.g. "d-age-with-tooltip"
|
|
16
|
+
* @returns {string} - e.g. "dAgeWithTooltip"
|
|
17
|
+
*/
|
|
18
|
+
function kebabToCamel(str) {
|
|
19
|
+
return str
|
|
20
|
+
.split("-")
|
|
21
|
+
.map((part, i) =>
|
|
22
|
+
i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
|
|
23
|
+
)
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true if the old import path is for a component (not a helper or modifier).
|
|
29
|
+
* @param {string} oldPath
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
function isComponentPath(oldPath) {
|
|
33
|
+
return !oldPath.includes("/helpers/") && !oldPath.includes("/modifiers/");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Computes the canonical identifier name for a given new import path.
|
|
38
|
+
* Components use PascalCase, helpers/modifiers use camelCase.
|
|
39
|
+
* @param {string} oldPath - The old import path (to determine component vs helper/modifier)
|
|
40
|
+
* @param {string} newPath - The new import path
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function canonicalName(oldPath, newPath) {
|
|
44
|
+
const basename = newPath.split("/").pop();
|
|
45
|
+
return isComponentPath(oldPath)
|
|
46
|
+
? kebabToPascal(basename)
|
|
47
|
+
: kebabToCamel(basename);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const USE_UI_KIT = "Use `{{newSource}}` instead of `{{oldSource}}`";
|
|
51
|
+
|
|
52
|
+
const messages = {
|
|
53
|
+
pathOnly: `${USE_UI_KIT}.`,
|
|
54
|
+
rename: `${USE_UI_KIT}. Rename \`{{localName}}\` to \`{{newName}}\`.`,
|
|
55
|
+
conflict: `${USE_UI_KIT}: \`{{newName}}\` conflicts with an existing identifier. Rename manually.`,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
meta: {
|
|
60
|
+
type: "suggestion",
|
|
61
|
+
docs: {
|
|
62
|
+
description:
|
|
63
|
+
"migrate imports to discourse/ui-kit/ paths and rename identifiers to match the d- prefix convention",
|
|
64
|
+
},
|
|
65
|
+
fixable: "code",
|
|
66
|
+
messages,
|
|
67
|
+
schema: [],
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
create(context) {
|
|
71
|
+
// Collect all pending reports so we can emit a single combined fix in
|
|
72
|
+
// Program:exit. This avoids overlapping composite-fix ranges when
|
|
73
|
+
// multiple imports are fixed in the same file (ESLint merges all fix
|
|
74
|
+
// operations from a single report into one range, and when that range
|
|
75
|
+
// spans from an import to a closing tag deep in the template, it blocks
|
|
76
|
+
// other imports' fixes from being applied in the same pass).
|
|
77
|
+
const pendingReports = [];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ImportDeclaration(node) {
|
|
81
|
+
const oldSource = node.source.value;
|
|
82
|
+
const newSource = MAPPINGS[oldSource];
|
|
83
|
+
|
|
84
|
+
if (!newSource) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const defaultSpecifier = node.specifiers.find(
|
|
89
|
+
(s) => s.type === "ImportDefaultSpecifier"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// No default import (namespace or named-only) — just fix the path
|
|
93
|
+
if (!defaultSpecifier) {
|
|
94
|
+
pendingReports.push({
|
|
95
|
+
node,
|
|
96
|
+
messageId: "pathOnly",
|
|
97
|
+
data: { oldSource, newSource },
|
|
98
|
+
fixable: true,
|
|
99
|
+
kind: "pathOnly",
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const localName = defaultSpecifier.local.name;
|
|
105
|
+
const newName = canonicalName(oldSource, newSource);
|
|
106
|
+
|
|
107
|
+
// Local name already matches canonical new name — just fix the path
|
|
108
|
+
if (localName === newName) {
|
|
109
|
+
pendingReports.push({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "pathOnly",
|
|
112
|
+
data: { oldSource, newSource },
|
|
113
|
+
fixable: true,
|
|
114
|
+
kind: "pathOnly",
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Rename needed — check for naming conflicts
|
|
120
|
+
const moduleScope = context.sourceCode.scopeManager.scopes.find(
|
|
121
|
+
(s) => s.type === "module"
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const hasConflict = moduleScope?.variables.some(
|
|
125
|
+
(v) => v.name === newName && v.defs[0]?.node !== defaultSpecifier
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (hasConflict) {
|
|
129
|
+
pendingReports.push({
|
|
130
|
+
node,
|
|
131
|
+
messageId: "conflict",
|
|
132
|
+
data: { oldSource, newSource, localName, newName },
|
|
133
|
+
fixable: false,
|
|
134
|
+
kind: "conflict",
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const variable = moduleScope?.variables.find(
|
|
140
|
+
(v) => v.name === localName
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
pendingReports.push({
|
|
144
|
+
node,
|
|
145
|
+
messageId: "rename",
|
|
146
|
+
data: { oldSource, newSource, localName, newName },
|
|
147
|
+
fixable: true,
|
|
148
|
+
kind: "rename",
|
|
149
|
+
defaultSpecifier,
|
|
150
|
+
variable,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
ExportNamedDeclaration(node) {
|
|
155
|
+
if (!node.source) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const oldSource = node.source.value;
|
|
160
|
+
const newSource = MAPPINGS[oldSource];
|
|
161
|
+
|
|
162
|
+
if (!newSource) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pendingReports.push({
|
|
167
|
+
node,
|
|
168
|
+
messageId: "pathOnly",
|
|
169
|
+
data: { oldSource, newSource },
|
|
170
|
+
fixable: true,
|
|
171
|
+
kind: "pathOnly",
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// Handle importSync("discourse/components/...") calls, but only when
|
|
176
|
+
// importSync is imported from "@embroider/macros".
|
|
177
|
+
"CallExpression[callee.name='importSync']"(node) {
|
|
178
|
+
const arg = node.arguments[0];
|
|
179
|
+
if (!arg || arg.type !== "Literal" || typeof arg.value !== "string") {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const oldSource = arg.value;
|
|
184
|
+
const newSource = MAPPINGS[oldSource];
|
|
185
|
+
|
|
186
|
+
if (!newSource) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Verify importSync comes from @embroider/macros
|
|
191
|
+
const moduleScope = context.sourceCode.scopeManager.scopes.find(
|
|
192
|
+
(s) => s.type === "module"
|
|
193
|
+
);
|
|
194
|
+
const importSyncVar = moduleScope?.variables.find(
|
|
195
|
+
(v) => v.name === "importSync"
|
|
196
|
+
);
|
|
197
|
+
const importDef = importSyncVar?.defs.find(
|
|
198
|
+
(d) =>
|
|
199
|
+
d.type === "ImportBinding" &&
|
|
200
|
+
d.parent?.source?.value === "@embroider/macros"
|
|
201
|
+
);
|
|
202
|
+
if (!importDef) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pendingReports.push({
|
|
207
|
+
node,
|
|
208
|
+
messageId: "pathOnly",
|
|
209
|
+
data: { oldSource, newSource },
|
|
210
|
+
fixable: true,
|
|
211
|
+
kind: "importSync",
|
|
212
|
+
importSyncArg: arg,
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
"Program:exit"() {
|
|
217
|
+
// Scan JSDoc comments for import("discourse/...") type references.
|
|
218
|
+
// These aren't AST nodes, so we use regex over comment text.
|
|
219
|
+
const JSDOC_IMPORT_RE = /import\(["']([^"']+)["']\)/g;
|
|
220
|
+
const EXTENSION_RE = /\.(gjs|js|ts|gts)$/;
|
|
221
|
+
|
|
222
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
223
|
+
let match;
|
|
224
|
+
while ((match = JSDOC_IMPORT_RE.exec(comment.value)) !== null) {
|
|
225
|
+
const rawPath = match[1];
|
|
226
|
+
const stripped = rawPath.replace(EXTENSION_RE, "");
|
|
227
|
+
const newSource = MAPPINGS[stripped];
|
|
228
|
+
|
|
229
|
+
if (!newSource) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const ext = rawPath.match(EXTENSION_RE)?.[0] || "";
|
|
234
|
+
const fullNewSource = newSource + ext;
|
|
235
|
+
|
|
236
|
+
// Compute the range of the path string inside the comment.
|
|
237
|
+
// comment.range[0] points to /* or //, add 2 to skip the
|
|
238
|
+
// opening delimiter, then match.index is relative to comment.value.
|
|
239
|
+
const pathStart =
|
|
240
|
+
comment.range[0] + 2 + match.index + match[0].indexOf(rawPath);
|
|
241
|
+
const pathEnd = pathStart + rawPath.length;
|
|
242
|
+
|
|
243
|
+
pendingReports.push({
|
|
244
|
+
node: context.sourceCode.ast,
|
|
245
|
+
messageId: "pathOnly",
|
|
246
|
+
data: { oldSource: rawPath, newSource: fullNewSource },
|
|
247
|
+
fixable: true,
|
|
248
|
+
kind: "jsdoc",
|
|
249
|
+
jsdocRange: [pathStart, pathEnd],
|
|
250
|
+
jsdocNewSource: fullNewSource,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (pendingReports.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const sourceText = context.sourceCode.getText();
|
|
260
|
+
const fixableReports = pendingReports.filter((r) => r.fixable);
|
|
261
|
+
|
|
262
|
+
// Emit all reports. Attach a single combined fix to the first
|
|
263
|
+
// fixable report — this avoids overlapping composite ranges.
|
|
264
|
+
let fixAttached = false;
|
|
265
|
+
|
|
266
|
+
for (const report of pendingReports) {
|
|
267
|
+
const reportObj = {
|
|
268
|
+
node: report.node,
|
|
269
|
+
messageId: report.messageId,
|
|
270
|
+
data: report.data,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (report.fixable && !fixAttached) {
|
|
274
|
+
fixAttached = true;
|
|
275
|
+
reportObj.fix = (fixer) =>
|
|
276
|
+
buildCombinedFix(fixer, fixableReports, sourceText);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
context.report(reportObj);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Builds a single array of fix operations covering ALL fixable imports.
|
|
288
|
+
* Having one combined fix avoids overlapping composite ranges.
|
|
289
|
+
*/
|
|
290
|
+
function buildCombinedFix(fixer, reports, sourceText) {
|
|
291
|
+
const fixes = [];
|
|
292
|
+
const fixedRanges = new Set();
|
|
293
|
+
|
|
294
|
+
function addFix(fix) {
|
|
295
|
+
const range = fix.range;
|
|
296
|
+
const key = `${range[0]}:${range[1]}`;
|
|
297
|
+
if (!fixedRanges.has(key)) {
|
|
298
|
+
fixedRanges.add(key);
|
|
299
|
+
fixes.push(fix);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const report of reports) {
|
|
304
|
+
if (report.kind === "jsdoc") {
|
|
305
|
+
addFix(fixer.replaceTextRange(report.jsdocRange, report.jsdocNewSource));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (report.kind === "importSync") {
|
|
310
|
+
addFix(
|
|
311
|
+
fixer.replaceText(report.importSyncArg, `"${report.data.newSource}"`)
|
|
312
|
+
);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (report.kind === "pathOnly") {
|
|
317
|
+
addFix(
|
|
318
|
+
fixer.replaceText(report.node.source, `"${report.data.newSource}"`)
|
|
319
|
+
);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// kind === "rename"
|
|
324
|
+
const { node, defaultSpecifier, variable, data } = report;
|
|
325
|
+
const { newSource, localName, newName } = data;
|
|
326
|
+
|
|
327
|
+
addFix(fixer.replaceText(node.source, `"${newSource}"`));
|
|
328
|
+
addFix(fixer.replaceText(defaultSpecifier, newName));
|
|
329
|
+
|
|
330
|
+
if (variable) {
|
|
331
|
+
for (const ref of variable.references) {
|
|
332
|
+
if (ref.identifier !== defaultSpecifier.local) {
|
|
333
|
+
// When a renamed identifier is used as a shorthand property
|
|
334
|
+
// (e.g. { basePath }), naively renaming it to { dBasePath }
|
|
335
|
+
// changes both the key and value. Instead, expand to explicit
|
|
336
|
+
// key-value form: { basePath: dBasePath }.
|
|
337
|
+
const parent = ref.identifier.parent;
|
|
338
|
+
if (
|
|
339
|
+
parent?.type === "Property" &&
|
|
340
|
+
parent.shorthand &&
|
|
341
|
+
parent.value === ref.identifier
|
|
342
|
+
) {
|
|
343
|
+
addFix(fixer.replaceText(parent, `${localName}: ${newName}`));
|
|
344
|
+
} else {
|
|
345
|
+
addFix(fixer.replaceText(ref.identifier, newName));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// For component elements with closing tags (e.g. <NavItem>...</NavItem>),
|
|
349
|
+
// variable.references only covers the opening tag. We must also
|
|
350
|
+
// fix the closing tag to avoid a Glimmer parse error.
|
|
351
|
+
// Walk up the parent chain to find the GlimmerElementNode — it may
|
|
352
|
+
// be the direct parent (GlimmerElementNodePart → GlimmerElementNode)
|
|
353
|
+
// or deeper when inside {{#if}}/{{#each}} blocks.
|
|
354
|
+
let elementNode = ref.identifier.parent;
|
|
355
|
+
while (elementNode && elementNode.type !== "GlimmerElementNode") {
|
|
356
|
+
elementNode = elementNode.parent;
|
|
357
|
+
}
|
|
358
|
+
if (elementNode && !elementNode.selfClosing) {
|
|
359
|
+
const closingTag = `</${localName}>`;
|
|
360
|
+
const searchStart = elementNode.range[0];
|
|
361
|
+
const searchEnd = elementNode.range[1];
|
|
362
|
+
const idx = sourceText.lastIndexOf(closingTag, searchEnd - 1);
|
|
363
|
+
if (idx >= searchStart) {
|
|
364
|
+
const nameStart = idx + 2; // skip "</"
|
|
365
|
+
const nameEnd = nameStart + localName.length;
|
|
366
|
+
addFix(fixer.replaceTextRange([nameStart, nameEnd], newName));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return fixes;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Mapping from old import paths to new ui-kit paths.
|
|
378
|
+
// Extracted from discourse/discourse PR #38703 (ui-kit-shims.js).
|
|
379
|
+
const MAPPINGS = {
|
|
380
|
+
// Components — already d-prefixed (path change only)
|
|
381
|
+
"discourse/components/d-autocomplete-results":
|
|
382
|
+
"discourse/ui-kit/d-autocomplete-results",
|
|
383
|
+
"discourse/components/d-breadcrumbs-container":
|
|
384
|
+
"discourse/ui-kit/d-breadcrumbs-container",
|
|
385
|
+
"discourse/components/d-breadcrumbs-item":
|
|
386
|
+
"discourse/ui-kit/d-breadcrumbs-item",
|
|
387
|
+
"discourse/components/d-button": "discourse/ui-kit/d-button",
|
|
388
|
+
"discourse/components/d-combo-button": "discourse/ui-kit/d-combo-button",
|
|
389
|
+
"discourse/components/d-icon-grid-picker":
|
|
390
|
+
"discourse/ui-kit/d-icon-grid-picker",
|
|
391
|
+
"discourse/components/d-editor": "discourse/ui-kit/d-editor",
|
|
392
|
+
"discourse/components/d-modal": "discourse/ui-kit/d-modal",
|
|
393
|
+
"discourse/components/d-modal-cancel": "discourse/ui-kit/d-modal-cancel",
|
|
394
|
+
"discourse/components/d-multi-select": "discourse/ui-kit/d-multi-select",
|
|
395
|
+
"discourse/components/d-navigation-item":
|
|
396
|
+
"discourse/ui-kit/d-navigation-item",
|
|
397
|
+
"discourse/components/d-otp": "discourse/ui-kit/d-otp",
|
|
398
|
+
"discourse/components/d-page-action-button":
|
|
399
|
+
"discourse/ui-kit/d-page-action-button",
|
|
400
|
+
"discourse/components/d-page-header": "discourse/ui-kit/d-page-header",
|
|
401
|
+
"discourse/components/d-page-subheader": "discourse/ui-kit/d-page-subheader",
|
|
402
|
+
"discourse/components/d-select": "discourse/ui-kit/d-select",
|
|
403
|
+
"discourse/components/d-stat-tiles": "discourse/ui-kit/d-stat-tiles",
|
|
404
|
+
"discourse/components/d-textarea": "discourse/ui-kit/d-textarea",
|
|
405
|
+
"discourse/components/d-toggle-switch": "discourse/ui-kit/d-toggle-switch",
|
|
406
|
+
|
|
407
|
+
// Components — renamed (old unprefixed → new d-prefixed)
|
|
408
|
+
"discourse/components/async-content": "discourse/ui-kit/d-async-content",
|
|
409
|
+
"discourse/components/avatar-flair": "discourse/ui-kit/d-avatar-flair",
|
|
410
|
+
"discourse/components/badge-button": "discourse/ui-kit/d-badge-button",
|
|
411
|
+
"discourse/components/badge-card": "discourse/ui-kit/d-badge-card",
|
|
412
|
+
"discourse/components/calendar-date-time-input":
|
|
413
|
+
"discourse/ui-kit/d-calendar-date-time-input",
|
|
414
|
+
"discourse/components/cdn-img": "discourse/ui-kit/d-cdn-img",
|
|
415
|
+
"discourse/components/char-counter": "discourse/ui-kit/d-char-counter",
|
|
416
|
+
"discourse/components/color-picker": "discourse/ui-kit/d-color-picker",
|
|
417
|
+
"discourse/components/color-picker-choice":
|
|
418
|
+
"discourse/ui-kit/d-color-picker-choice",
|
|
419
|
+
"discourse/components/conditional-in-element":
|
|
420
|
+
"discourse/ui-kit/d-conditional-in-element",
|
|
421
|
+
"discourse/components/conditional-loading-section":
|
|
422
|
+
"discourse/ui-kit/d-conditional-loading-section",
|
|
423
|
+
"discourse/components/conditional-loading-spinner":
|
|
424
|
+
"discourse/ui-kit/d-conditional-loading-spinner",
|
|
425
|
+
"discourse/components/cook-text": "discourse/ui-kit/d-cook-text",
|
|
426
|
+
"discourse/components/copy-button": "discourse/ui-kit/d-copy-button",
|
|
427
|
+
"discourse/components/count-i18n": "discourse/ui-kit/d-count-i18n",
|
|
428
|
+
"discourse/components/custom-html": "discourse/ui-kit/d-custom-html",
|
|
429
|
+
"discourse/components/date-input": "discourse/ui-kit/d-date-input",
|
|
430
|
+
"discourse/components/date-picker": "discourse/ui-kit/d-date-picker",
|
|
431
|
+
"discourse/components/date-time-input": "discourse/ui-kit/d-date-time-input",
|
|
432
|
+
"discourse/components/date-time-input-range":
|
|
433
|
+
"discourse/ui-kit/d-date-time-input-range",
|
|
434
|
+
"discourse/components/decorated-html": "discourse/ui-kit/d-decorated-html",
|
|
435
|
+
"discourse/components/dropdown-menu": "discourse/ui-kit/d-dropdown-menu",
|
|
436
|
+
"discourse/components/empty-state": "discourse/ui-kit/d-empty-state",
|
|
437
|
+
"discourse/components/expanding-text-area":
|
|
438
|
+
"discourse/ui-kit/d-expanding-text-area",
|
|
439
|
+
"discourse/components/filter-input": "discourse/ui-kit/d-filter-input",
|
|
440
|
+
"discourse/components/flash-message": "discourse/ui-kit/d-flash-message",
|
|
441
|
+
"discourse/components/future-date-input":
|
|
442
|
+
"discourse/ui-kit/d-future-date-input",
|
|
443
|
+
"discourse/components/highlighted-code":
|
|
444
|
+
"discourse/ui-kit/d-highlighted-code",
|
|
445
|
+
"discourse/components/horizontal-overflow-nav":
|
|
446
|
+
"discourse/ui-kit/d-horizontal-overflow-nav",
|
|
447
|
+
"discourse/components/html-with-links": "discourse/ui-kit/d-html-with-links",
|
|
448
|
+
"discourse/components/input-tip": "discourse/ui-kit/d-input-tip",
|
|
449
|
+
"discourse/components/interpolated-translation":
|
|
450
|
+
"discourse/ui-kit/d-interpolated-translation",
|
|
451
|
+
"discourse/components/light-dark-img": "discourse/ui-kit/d-light-dark-img",
|
|
452
|
+
"discourse/components/load-more": "discourse/ui-kit/d-load-more",
|
|
453
|
+
"discourse/components/nav-item": "discourse/ui-kit/d-nav-item",
|
|
454
|
+
"discourse/components/number-field": "discourse/ui-kit/d-number-field",
|
|
455
|
+
"discourse/components/password-field": "discourse/ui-kit/d-password-field",
|
|
456
|
+
"discourse/components/pick-files-button":
|
|
457
|
+
"discourse/ui-kit/d-pick-files-button",
|
|
458
|
+
"discourse/components/popup-input-tip": "discourse/ui-kit/d-popup-input-tip",
|
|
459
|
+
"discourse/components/radio-button": "discourse/ui-kit/d-radio-button",
|
|
460
|
+
"discourse/components/relative-date": "discourse/ui-kit/d-relative-date",
|
|
461
|
+
"discourse/components/relative-time-picker":
|
|
462
|
+
"discourse/ui-kit/d-relative-time-picker",
|
|
463
|
+
"discourse/components/responsive-table":
|
|
464
|
+
"discourse/ui-kit/d-responsive-table",
|
|
465
|
+
"discourse/components/save-controls": "discourse/ui-kit/d-save-controls",
|
|
466
|
+
"discourse/components/second-factor-input":
|
|
467
|
+
"discourse/ui-kit/d-second-factor-input",
|
|
468
|
+
"discourse/components/small-user-list": "discourse/ui-kit/d-small-user-list",
|
|
469
|
+
"discourse/components/table-header-toggle":
|
|
470
|
+
"discourse/ui-kit/d-table-header-toggle",
|
|
471
|
+
"discourse/components/tap-tile": "discourse/ui-kit/d-tap-tile",
|
|
472
|
+
"discourse/components/tap-tile-grid": "discourse/ui-kit/d-tap-tile-grid",
|
|
473
|
+
"discourse/components/text-field": "discourse/ui-kit/d-text-field",
|
|
474
|
+
"discourse/components/textarea": "discourse/ui-kit/d-textarea",
|
|
475
|
+
"discourse/components/time-input": "discourse/ui-kit/d-time-input",
|
|
476
|
+
"discourse/components/time-shortcut-picker":
|
|
477
|
+
"discourse/ui-kit/d-time-shortcut-picker",
|
|
478
|
+
"discourse/components/toggle-password-mask":
|
|
479
|
+
"discourse/ui-kit/d-toggle-password-mask",
|
|
480
|
+
"discourse/components/user-avatar": "discourse/ui-kit/d-user-avatar",
|
|
481
|
+
"discourse/components/user-avatar-flair":
|
|
482
|
+
"discourse/ui-kit/d-user-avatar-flair",
|
|
483
|
+
"discourse/components/user-info": "discourse/ui-kit/d-user-info",
|
|
484
|
+
"discourse/components/user-link": "discourse/ui-kit/d-user-link",
|
|
485
|
+
"discourse/components/user-stat": "discourse/ui-kit/d-user-stat",
|
|
486
|
+
"discourse/components/user-status-message":
|
|
487
|
+
"discourse/ui-kit/d-user-status-message",
|
|
488
|
+
|
|
489
|
+
// Helpers — already d-prefixed
|
|
490
|
+
"discourse/helpers/d-icon": "discourse/ui-kit/helpers/d-icon",
|
|
491
|
+
|
|
492
|
+
// Helpers — renamed
|
|
493
|
+
"discourse/helpers/age-with-tooltip":
|
|
494
|
+
"discourse/ui-kit/helpers/d-age-with-tooltip",
|
|
495
|
+
"discourse/helpers/avatar": "discourse/ui-kit/helpers/d-avatar",
|
|
496
|
+
"discourse/helpers/base-path": "discourse/ui-kit/helpers/d-base-path",
|
|
497
|
+
"discourse/helpers/bound-avatar": "discourse/ui-kit/helpers/d-bound-avatar",
|
|
498
|
+
"discourse/helpers/bound-avatar-template":
|
|
499
|
+
"discourse/ui-kit/helpers/d-bound-avatar-template",
|
|
500
|
+
"discourse/helpers/bound-category-link":
|
|
501
|
+
"discourse/ui-kit/helpers/d-bound-category-link",
|
|
502
|
+
"discourse/helpers/category-badge":
|
|
503
|
+
"discourse/ui-kit/helpers/d-category-badge",
|
|
504
|
+
"discourse/helpers/category-link": "discourse/ui-kit/helpers/d-category-link",
|
|
505
|
+
"discourse/helpers/concat-class": "discourse/ui-kit/helpers/d-concat-class",
|
|
506
|
+
"discourse/helpers/dasherize": "discourse/ui-kit/helpers/d-dasherize",
|
|
507
|
+
"discourse/helpers/dir-span": "discourse/ui-kit/helpers/d-dir-span",
|
|
508
|
+
"discourse/helpers/discourse-tag": "discourse/ui-kit/helpers/d-discourse-tag",
|
|
509
|
+
"discourse/helpers/discourse-tags":
|
|
510
|
+
"discourse/ui-kit/helpers/d-discourse-tags",
|
|
511
|
+
"discourse/helpers/element": "discourse/ui-kit/helpers/d-element",
|
|
512
|
+
"discourse/helpers/emoji": "discourse/ui-kit/helpers/d-emoji",
|
|
513
|
+
"discourse/helpers/format-date": "discourse/ui-kit/helpers/d-format-date",
|
|
514
|
+
"discourse/helpers/format-duration":
|
|
515
|
+
"discourse/ui-kit/helpers/d-format-duration",
|
|
516
|
+
"discourse/helpers/icon-or-image": "discourse/ui-kit/helpers/d-icon-or-image",
|
|
517
|
+
"discourse/helpers/loading-spinner":
|
|
518
|
+
"discourse/ui-kit/helpers/d-loading-spinner",
|
|
519
|
+
"discourse/helpers/number": "discourse/ui-kit/helpers/d-number",
|
|
520
|
+
"discourse/helpers/replace-emoji": "discourse/ui-kit/helpers/d-replace-emoji",
|
|
521
|
+
"discourse/helpers/topic-link": "discourse/ui-kit/helpers/d-topic-link",
|
|
522
|
+
"discourse/helpers/unique-id": "discourse/ui-kit/helpers/d-unique-id",
|
|
523
|
+
"discourse/helpers/user-avatar": "discourse/ui-kit/helpers/d-user-avatar",
|
|
524
|
+
|
|
525
|
+
// Modifiers — already d-prefixed
|
|
526
|
+
"discourse/modifiers/d-autocomplete":
|
|
527
|
+
"discourse/ui-kit/modifiers/d-autocomplete",
|
|
528
|
+
|
|
529
|
+
// Modifiers — renamed
|
|
530
|
+
"discourse/modifiers/auto-focus": "discourse/ui-kit/modifiers/d-auto-focus",
|
|
531
|
+
"discourse/modifiers/close-on-click-outside":
|
|
532
|
+
"discourse/ui-kit/modifiers/d-close-on-click-outside",
|
|
533
|
+
"discourse/modifiers/draggable": "discourse/ui-kit/modifiers/d-draggable",
|
|
534
|
+
"discourse/modifiers/observe-intersection":
|
|
535
|
+
"discourse/ui-kit/modifiers/d-observe-intersection",
|
|
536
|
+
"discourse/modifiers/on-resize": "discourse/ui-kit/modifiers/d-on-resize",
|
|
537
|
+
"discourse/modifiers/scroll-into-view":
|
|
538
|
+
"discourse/ui-kit/modifiers/d-scroll-into-view",
|
|
539
|
+
"discourse/modifiers/swipe": "discourse/ui-kit/modifiers/d-swipe",
|
|
540
|
+
"discourse/modifiers/tab-to-sibling":
|
|
541
|
+
"discourse/ui-kit/modifiers/d-tab-to-sibling",
|
|
542
|
+
"discourse/modifiers/trap-tab": "discourse/ui-kit/modifiers/d-trap-tab",
|
|
543
|
+
};
|
package/eslint.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import BabelParser from "@babel/eslint-parser";
|
|
2
2
|
import js from "@eslint/js";
|
|
3
|
-
import EmberESLintParser from "ember-eslint-parser";
|
|
4
3
|
import DecoratorPosition from "eslint-plugin-decorator-position";
|
|
5
4
|
import EmberPlugin from "eslint-plugin-ember";
|
|
6
5
|
import EmberRecommended from "eslint-plugin-ember/configs/recommended";
|
|
6
|
+
import EmberTemplateLintMigration from "eslint-plugin-ember/configs/template-lint-migration";
|
|
7
7
|
import ImportPlugin from "eslint-plugin-import";
|
|
8
8
|
import QUnitPlugin from "eslint-plugin-qunit";
|
|
9
9
|
import QUnitRecommended from "eslint-plugin-qunit/configs/recommended";
|
|
@@ -23,20 +23,24 @@ import lineBeforeDefaultExport from "./eslint-rules/line-before-default-export.m
|
|
|
23
23
|
import linesBetweenClassMembers from "./eslint-rules/lines-between-class-members.mjs";
|
|
24
24
|
import migrateTrackedBuiltInsToEmberCollections from "./eslint-rules/migrate-tracked-built-ins-to-ember-collections.mjs";
|
|
25
25
|
import movedPackagesImportPaths from "./eslint-rules/moved-packages-import-paths.mjs";
|
|
26
|
+
import noAtClass from "./eslint-rules/no-at-class.mjs";
|
|
26
27
|
import noComputedMacros from "./eslint-rules/no-computed-macros.mjs";
|
|
27
28
|
import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
|
|
28
29
|
import noDiscourseComputed from "./eslint-rules/no-discourse-computed.mjs";
|
|
29
30
|
import noOnclick from "./eslint-rules/no-onclick.mjs";
|
|
30
31
|
import noRouteTemplate from "./eslint-rules/no-route-template.mjs";
|
|
31
32
|
import noSimpleQuerySelector from "./eslint-rules/no-simple-query-selector.mjs";
|
|
33
|
+
import noTemplateLintDirectives from "./eslint-rules/no-template-lint-directives.mjs";
|
|
32
34
|
import noUnnecessaryTracked from "./eslint-rules/no-unnecessary-tracked.mjs";
|
|
33
35
|
import noUnusedServices from "./eslint-rules/no-unused-services.mjs";
|
|
34
36
|
import pluginApiNoVersion from "./eslint-rules/plugin-api-no-version.mjs";
|
|
37
|
+
import pluginOutletLazyHash from "./eslint-rules/plugin-outlet-lazy-hash.mjs";
|
|
35
38
|
import serviceInjectImport from "./eslint-rules/service-inject-import.mjs";
|
|
36
39
|
import templateTagNoSelfThis from "./eslint-rules/template-tag-no-self-this.mjs";
|
|
37
40
|
import testFilenameSuffix from "./eslint-rules/test-filename-suffix.mjs";
|
|
38
41
|
import themeImports from "./eslint-rules/theme-imports.mjs";
|
|
39
42
|
import truthHelpersImports from "./eslint-rules/truth-helpers-imports.mjs";
|
|
43
|
+
import uiKitImports from "./eslint-rules/ui-kit-imports.mjs";
|
|
40
44
|
|
|
41
45
|
let decoratorsPluginPath = import.meta
|
|
42
46
|
.resolve("@babel/plugin-proposal-decorators")
|
|
@@ -49,14 +53,25 @@ export default [
|
|
|
49
53
|
js.configs.recommended,
|
|
50
54
|
QUnitRecommended,
|
|
51
55
|
...EmberRecommended,
|
|
56
|
+
...EmberTemplateLintMigration,
|
|
52
57
|
{
|
|
53
58
|
ignores: ["assets/vendor/**/*", "public/**/*"],
|
|
54
59
|
},
|
|
60
|
+
{
|
|
61
|
+
// Scope the Babel parser to non-template JS so it doesn't clobber the
|
|
62
|
+
// ember-eslint-parser that the upstream `base` config sets up for
|
|
63
|
+
// .gjs/.gts files. The parserOptions live in the main block below so
|
|
64
|
+
// they also flow through to ember-eslint-parser, which delegates the JS
|
|
65
|
+
// portion of gjs/gts to @babel/eslint-parser internally.
|
|
66
|
+
files: ["**/*.{js,mjs,cjs}"],
|
|
67
|
+
languageOptions: {
|
|
68
|
+
parser: BabelParser,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
55
71
|
{
|
|
56
72
|
languageOptions: {
|
|
57
73
|
ecmaVersion: 2022,
|
|
58
74
|
sourceType: "module",
|
|
59
|
-
parser: BabelParser,
|
|
60
75
|
parserOptions: {
|
|
61
76
|
useBabel: true,
|
|
62
77
|
requireConfigFile: false,
|
|
@@ -65,7 +80,6 @@ export default [
|
|
|
65
80
|
plugins: [[decoratorsPluginPath, { legacy: true }]],
|
|
66
81
|
},
|
|
67
82
|
},
|
|
68
|
-
|
|
69
83
|
globals: {
|
|
70
84
|
...globals.browser,
|
|
71
85
|
...globals.node,
|
|
@@ -153,6 +167,10 @@ export default [
|
|
|
153
167
|
"no-unnecessary-tracked": noUnnecessaryTracked,
|
|
154
168
|
"migrate-tracked-built-ins-to-ember-collections":
|
|
155
169
|
migrateTrackedBuiltInsToEmberCollections,
|
|
170
|
+
"ui-kit-imports": uiKitImports,
|
|
171
|
+
"no-at-class": noAtClass,
|
|
172
|
+
"plugin-outlet-lazy-hash": pluginOutletLazyHash,
|
|
173
|
+
"no-template-lint-directives": noTemplateLintDirectives,
|
|
156
174
|
},
|
|
157
175
|
},
|
|
158
176
|
},
|
|
@@ -221,6 +239,39 @@ export default [
|
|
|
221
239
|
"ember/no-unnecessary-service-injection-argument": "error",
|
|
222
240
|
"ember/no-replace-test-comments": "error",
|
|
223
241
|
"ember/route-path-style": "error",
|
|
242
|
+
|
|
243
|
+
// Intentionally disabled template rules
|
|
244
|
+
"ember/template-no-autofocus-attribute": "off",
|
|
245
|
+
"ember/template-no-positive-tabindex": "off",
|
|
246
|
+
"ember/template-require-mandatory-role-attributes": "off",
|
|
247
|
+
"ember/template-require-media-caption": "off",
|
|
248
|
+
"ember/template-builtin-component-arguments": "off",
|
|
249
|
+
"ember/template-no-builtin-form-components": "off",
|
|
250
|
+
"ember/template-no-unknown-arguments-for-builtin-components": "off",
|
|
251
|
+
|
|
252
|
+
// Pending default template rules
|
|
253
|
+
"ember/template-link-href-attributes": "off",
|
|
254
|
+
"ember/template-no-at-ember-render-modifiers": "off",
|
|
255
|
+
"ember/template-no-curly-component-invocation": "off",
|
|
256
|
+
"ember/template-no-duplicate-landmark-elements": "off",
|
|
257
|
+
"ember/template-no-inline-styles": "off",
|
|
258
|
+
"ember/template-no-link-to-tagname": "off",
|
|
259
|
+
"ember/template-no-passed-in-event-handlers": "off",
|
|
260
|
+
"ember/template-no-route-action": "off",
|
|
261
|
+
"ember/template-require-input-label": "off",
|
|
262
|
+
"ember/template-require-presentational-children": "off",
|
|
263
|
+
"ember/template-require-valid-alt-text": "off",
|
|
264
|
+
|
|
265
|
+
// Non-default rules
|
|
266
|
+
"ember/template-no-chained-this": "error",
|
|
267
|
+
"ember/template-require-strict-mode": "error",
|
|
268
|
+
|
|
269
|
+
// From `ember-template-lint`'s old `stylistic` preset
|
|
270
|
+
"ember/template-linebreak-style": "error",
|
|
271
|
+
"ember/template-no-multiple-empty-lines": "error",
|
|
272
|
+
"ember/template-no-trailing-spaces": "error",
|
|
273
|
+
"ember/template-no-unnecessary-concat": "error",
|
|
274
|
+
|
|
224
275
|
"qunit/no-loose-assertions": "error",
|
|
225
276
|
"qunit/no-identical-names": "off", // the rule doesn't consider that tests might be in different `acceptance` modules
|
|
226
277
|
"sort-class-members/sort-class-members": [
|
|
@@ -320,12 +371,10 @@ export default [
|
|
|
320
371
|
"discourse/keep-array-sorted": ["error"],
|
|
321
372
|
"discourse/no-unnecessary-tracked": ["warn"],
|
|
322
373
|
"discourse/migrate-tracked-built-ins-to-ember-collections": ["error"],
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
languageOptions: {
|
|
328
|
-
parser: EmberESLintParser,
|
|
374
|
+
"discourse/ui-kit-imports": ["error"],
|
|
375
|
+
"discourse/no-at-class": ["error"],
|
|
376
|
+
"discourse/plugin-outlet-lazy-hash": ["error"],
|
|
377
|
+
"discourse/no-template-lint-directives": ["error"],
|
|
329
378
|
},
|
|
330
379
|
},
|
|
331
380
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discourse/lint-configs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Shareable lint configs for Discourse core, plugins, and themes",
|
|
5
5
|
"author": "Discourse",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,22 +22,16 @@
|
|
|
22
22
|
"./prettier": {
|
|
23
23
|
"require": "./.prettierrc.cjs"
|
|
24
24
|
},
|
|
25
|
-
"./stylelint": "./stylelint.mjs"
|
|
26
|
-
"./template-lint": {
|
|
27
|
-
"require": "./template-lint.config.cjs"
|
|
28
|
-
},
|
|
29
|
-
"./template-lint-rules": "./template-lint-rules/index.mjs"
|
|
25
|
+
"./stylelint": "./stylelint.mjs"
|
|
30
26
|
},
|
|
31
27
|
"dependencies": {
|
|
32
28
|
"@babel/core": "^7.29.0",
|
|
33
29
|
"@babel/eslint-parser": "^7.28.6",
|
|
34
30
|
"@babel/plugin-proposal-decorators": "^7.29.0",
|
|
35
31
|
"@eslint/js": "^9.39.2",
|
|
36
|
-
"ember-eslint-parser": "^0.5.13",
|
|
37
|
-
"ember-template-lint": "^7.9.3",
|
|
38
32
|
"eslint": "^9.39.2",
|
|
39
33
|
"eslint-plugin-decorator-position": "^6.0.0",
|
|
40
|
-
"eslint-plugin-ember": "^
|
|
34
|
+
"eslint-plugin-ember": "^13.2.1",
|
|
41
35
|
"eslint-plugin-import": "^2.32.0",
|
|
42
36
|
"eslint-plugin-qunit": "^8.2.6",
|
|
43
37
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
@@ -52,7 +46,6 @@
|
|
|
52
46
|
"typescript": "^5.9.3"
|
|
53
47
|
},
|
|
54
48
|
"peerDependencies": {
|
|
55
|
-
"ember-template-lint": "7.9.3",
|
|
56
49
|
"eslint": "9.39.2",
|
|
57
50
|
"prettier": "3.8.1",
|
|
58
51
|
"stylelint": "17.5.0"
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import NoAtClass from "./no-at-class.mjs";
|
|
2
|
-
import NoImplicitThis from "./no-implicit-this.mjs";
|
|
3
|
-
import PluginOutletLazyHash from "./plugin-outlet-lazy-hash.mjs";
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
// Name of plugin
|
|
7
|
-
name: "discourse",
|
|
8
|
-
|
|
9
|
-
// Define rules for this plugin. Each path should map to a plugin rule
|
|
10
|
-
rules: {
|
|
11
|
-
"discourse/no-at-class": NoAtClass,
|
|
12
|
-
"discourse/no-implicit-this": NoImplicitThis,
|
|
13
|
-
"discourse/plugin-outlet-lazy-hash": PluginOutletLazyHash,
|
|
14
|
-
},
|
|
15
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Rule } from "ember-template-lint";
|
|
2
|
-
|
|
3
|
-
const AFFECTED_COMPONENTS = [
|
|
4
|
-
"DButton",
|
|
5
|
-
"DModal",
|
|
6
|
-
"TableHeaderToggle",
|
|
7
|
-
"Textarea",
|
|
8
|
-
"TextArea",
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
export default class NoAtClass extends Rule {
|
|
12
|
-
visitor() {
|
|
13
|
-
return {
|
|
14
|
-
ElementNode(node) {
|
|
15
|
-
if (AFFECTED_COMPONENTS.includes(node.tag) && node.attributes) {
|
|
16
|
-
node.attributes.forEach((attribute) => {
|
|
17
|
-
if (attribute.name === "@class") {
|
|
18
|
-
if (this.mode === "fix") {
|
|
19
|
-
attribute.name = "class";
|
|
20
|
-
} else {
|
|
21
|
-
this.log({
|
|
22
|
-
message: `Use 'class' instead of '@class' for ${node.tag}.`,
|
|
23
|
-
node,
|
|
24
|
-
isFixable: true,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// Adapted from https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-implicit-this.md
|
|
2
|
-
// With the addition of autofix
|
|
3
|
-
|
|
4
|
-
import { Rule } from "ember-template-lint";
|
|
5
|
-
|
|
6
|
-
function createErrorMessage(ruleName, lines, config) {
|
|
7
|
-
return [
|
|
8
|
-
`The ${ruleName} rule accepts one of the following values.`,
|
|
9
|
-
lines,
|
|
10
|
-
`You specified \`${JSON.stringify(config)}\``,
|
|
11
|
-
].join("\n");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function message(original) {
|
|
15
|
-
return (
|
|
16
|
-
`Ambiguous path '${original}' is not allowed. ` +
|
|
17
|
-
`Use '@${original}' if it is a named argument ` +
|
|
18
|
-
`or 'this.${original}' if it is a property on 'this'. ` +
|
|
19
|
-
"If it is a helper or component that has no arguments, " +
|
|
20
|
-
"you must either convert it to an angle bracket invocation " +
|
|
21
|
-
"or manually add it to the 'no-implicit-this' rule configuration, e.g. " +
|
|
22
|
-
`'no-implicit-this': { allow: ['${original}'] }.`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isString(value) {
|
|
27
|
-
return typeof value === "string";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isRegExp(value) {
|
|
31
|
-
return value instanceof RegExp;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function allowedFormat(value) {
|
|
35
|
-
return isString(value) || isRegExp(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Allow Ember's builtin argless syntaxes
|
|
39
|
-
export const ARGLESS_BUILTIN_HELPERS = [
|
|
40
|
-
"array",
|
|
41
|
-
"concat",
|
|
42
|
-
"debugger",
|
|
43
|
-
"has-block",
|
|
44
|
-
"hasBlock",
|
|
45
|
-
"has-block-params",
|
|
46
|
-
"hasBlockParams",
|
|
47
|
-
"hash",
|
|
48
|
-
"input",
|
|
49
|
-
"log",
|
|
50
|
-
"outlet",
|
|
51
|
-
"query-params",
|
|
52
|
-
"textarea",
|
|
53
|
-
"yield",
|
|
54
|
-
"unique-id",
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// arg'less Components / Helpers in default ember-cli blueprint
|
|
58
|
-
const ARGLESS_DEFAULT_BLUEPRINT = [
|
|
59
|
-
"welcome-page",
|
|
60
|
-
/* from app/index.html and tests/index.html */
|
|
61
|
-
"rootURL",
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export default class NoImplicitThis extends Rule {
|
|
65
|
-
parseConfig(config) {
|
|
66
|
-
if (config === false || config === undefined || this.isStrictMode) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
switch (typeof config) {
|
|
71
|
-
case "undefined": {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
case "boolean": {
|
|
76
|
-
if (config) {
|
|
77
|
-
return {
|
|
78
|
-
allow: [...ARGLESS_BUILTIN_HELPERS, ...ARGLESS_DEFAULT_BLUEPRINT],
|
|
79
|
-
};
|
|
80
|
-
} else {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
case "object": {
|
|
86
|
-
if (Array.isArray(config.allow) && config.allow.every(allowedFormat)) {
|
|
87
|
-
return {
|
|
88
|
-
allow: [
|
|
89
|
-
...ARGLESS_BUILTIN_HELPERS,
|
|
90
|
-
...ARGLESS_DEFAULT_BLUEPRINT,
|
|
91
|
-
...config.allow,
|
|
92
|
-
],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let errorMessage = createErrorMessage(
|
|
100
|
-
this.ruleName,
|
|
101
|
-
[
|
|
102
|
-
" * boolean - `true` to enable / `false` to disable",
|
|
103
|
-
" * object -- An object with the following keys:",
|
|
104
|
-
" * `allow` -- An array of component / helper names for that may be called without arguments",
|
|
105
|
-
],
|
|
106
|
-
config
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
throw new Error(errorMessage);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// The way this visitor works is a bit sketchy. We need to lint the PathExpressions
|
|
113
|
-
// in the callee position differently those in an argument position.
|
|
114
|
-
//
|
|
115
|
-
// Unfortunately, the current visitor API doesn't give us a good way to differentiate
|
|
116
|
-
// these two cases. Instead, we rely on the fact that the _first_ PathExpression that
|
|
117
|
-
// we enter after entering a MustacheStatement/BlockStatement/... will be the callee
|
|
118
|
-
// and we track this using a flag called `nextPathIsCallee`.
|
|
119
|
-
visitor() {
|
|
120
|
-
let nextPathIsCallee = false;
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
PathExpression(path) {
|
|
124
|
-
if (nextPathIsCallee) {
|
|
125
|
-
// All paths are valid callees so there's nothing to check.
|
|
126
|
-
} else {
|
|
127
|
-
let valid =
|
|
128
|
-
path.data ||
|
|
129
|
-
path.this ||
|
|
130
|
-
this.isLocal(path) ||
|
|
131
|
-
this.config.allow.some((item) => {
|
|
132
|
-
return isRegExp(item)
|
|
133
|
-
? item.test(path.original)
|
|
134
|
-
: item === path.original;
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
if (!valid) {
|
|
138
|
-
if (this.mode === "fix") {
|
|
139
|
-
path.original = `this.${path.original}`;
|
|
140
|
-
} else {
|
|
141
|
-
this.log({
|
|
142
|
-
message: message(path.original),
|
|
143
|
-
node: path,
|
|
144
|
-
isFixable: true,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
nextPathIsCallee = false;
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
SubExpression() {
|
|
154
|
-
nextPathIsCallee = true;
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
ElementModifierStatement() {
|
|
158
|
-
nextPathIsCallee = true;
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
MustacheStatement(node) {
|
|
162
|
-
let isCall = node.params.length > 0 || node.hash.pairs.length > 0;
|
|
163
|
-
|
|
164
|
-
nextPathIsCallee = isCall;
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
BlockStatement: {
|
|
168
|
-
enter() {
|
|
169
|
-
nextPathIsCallee = true;
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Rule } from "ember-template-lint";
|
|
2
|
-
|
|
3
|
-
export default class PluginOutletLazyHash extends Rule {
|
|
4
|
-
visitor() {
|
|
5
|
-
return {
|
|
6
|
-
ElementNode(node) {
|
|
7
|
-
if (node.tag === "PluginOutlet") {
|
|
8
|
-
const outletArgsAttr = node.attributes.find(
|
|
9
|
-
(attr) => attr.name === "@outletArgs"
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
outletArgsAttr &&
|
|
14
|
-
outletArgsAttr.value.type === "MustacheStatement" &&
|
|
15
|
-
outletArgsAttr.value.path.original === "hash"
|
|
16
|
-
) {
|
|
17
|
-
this.log({
|
|
18
|
-
message:
|
|
19
|
-
"Use {{lazyHash}} instead of {{hash}} for @outletArgs in <PluginOutlet>.",
|
|
20
|
-
node: outletArgsAttr.value,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
}
|
package/template-lint.config.cjs
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
extends: ["recommended", "stylistic"],
|
|
3
|
-
plugins: ["@discourse/lint-configs/template-lint-rules"],
|
|
4
|
-
ignore: ["**/*.js"],
|
|
5
|
-
rules: {
|
|
6
|
-
// Intentionally disabled default rules
|
|
7
|
-
"no-autofocus-attribute": false,
|
|
8
|
-
"no-positive-tabindex": false,
|
|
9
|
-
"require-mandatory-role-attributes": false,
|
|
10
|
-
"require-media-caption": false,
|
|
11
|
-
|
|
12
|
-
// Pending default rules
|
|
13
|
-
"link-href-attributes": false,
|
|
14
|
-
"no-at-ember-render-modifiers": false,
|
|
15
|
-
"no-curly-component-invocation": false,
|
|
16
|
-
"no-duplicate-landmark-elements": false,
|
|
17
|
-
"no-implicit-this": false,
|
|
18
|
-
"no-inline-styles": false,
|
|
19
|
-
"no-link-to-tagname": false,
|
|
20
|
-
"no-passed-in-event-handlers": false,
|
|
21
|
-
"no-route-action": false,
|
|
22
|
-
"require-input-label": false,
|
|
23
|
-
"require-presentational-children": false,
|
|
24
|
-
"require-valid-alt-text": false,
|
|
25
|
-
|
|
26
|
-
// Non-default rules
|
|
27
|
-
"no-unnecessary-curly-parens": true,
|
|
28
|
-
"no-unnecessary-curly-strings": true,
|
|
29
|
-
"simple-modifiers": true,
|
|
30
|
-
"no-chained-this": true,
|
|
31
|
-
"require-strict-mode": true,
|
|
32
|
-
|
|
33
|
-
// Pending non-default rules
|
|
34
|
-
"attribute-order": false,
|
|
35
|
-
"inline-link-to": false,
|
|
36
|
-
"no-builtin-form-components": false,
|
|
37
|
-
"no-this-in-template-only-components": false, // emits false-positives in gjs
|
|
38
|
-
|
|
39
|
-
// GJS compatibility
|
|
40
|
-
"modifier-name-case": false,
|
|
41
|
-
|
|
42
|
-
// Prettier compatibility
|
|
43
|
-
"block-indentation": false,
|
|
44
|
-
"eol-last": false,
|
|
45
|
-
quotes: false,
|
|
46
|
-
"self-closing-void-elements": false,
|
|
47
|
-
|
|
48
|
-
// Discourse custom
|
|
49
|
-
"discourse/no-at-class": true,
|
|
50
|
-
"discourse/no-implicit-this": {
|
|
51
|
-
allow: [
|
|
52
|
-
/-/, // kebab-case, probably a component or helper
|
|
53
|
-
],
|
|
54
|
-
},
|
|
55
|
-
"discourse/plugin-outlet-lazy-hash": true,
|
|
56
|
-
},
|
|
57
|
-
overrides: [
|
|
58
|
-
{
|
|
59
|
-
files: ["**/*.gjs", "**/*.gts"],
|
|
60
|
-
rules: {
|
|
61
|
-
"discourse/no-implicit-this": false,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
};
|