@discourse/lint-configs 2.0.0 → 2.1.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/eslint-rules/deprecated-lookups.mjs +58 -0
- package/eslint-rules/i18n-import-location.mjs +64 -0
- package/eslint-rules/i18n-t.mjs +91 -0
- package/eslint-rules/no-simple-queryselector.mjs +46 -0
- package/eslint-rules/service-inject-import.mjs +36 -0
- package/eslint-rules/utils/fix-import.mjs +69 -0
- package/eslint.mjs +21 -0
- package/package.json +4 -3
- package/template-lint-rules/index.mjs +11 -0
- package/template-lint-rules/no-at-class.mjs +33 -0
- package/template-lint.config.cjs +4 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const replacements = {
|
|
2
|
+
"store:main": "service:store",
|
|
3
|
+
"search-service:main": "service:search",
|
|
4
|
+
"key-value-store:main": "service:key-value-store",
|
|
5
|
+
"pm-topic-tracking-state:main": "service:pm-topic-tracking-state",
|
|
6
|
+
"message-bus:main": "service:message-bus",
|
|
7
|
+
"site-settings:main": "service:site-settings",
|
|
8
|
+
"capabilities:main": "service:capabilities",
|
|
9
|
+
"current-user:main": "service:current-user",
|
|
10
|
+
"site:main": "service:site",
|
|
11
|
+
"topic-tracking-state:main": "service:topic-tracking-state",
|
|
12
|
+
"controller:composer": "service:composer",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
meta: {
|
|
17
|
+
type: "suggestion",
|
|
18
|
+
docs: {
|
|
19
|
+
description:
|
|
20
|
+
"replace deprecated resolver 'lookup' calls and modifyClass arguments with modern equivalents",
|
|
21
|
+
category: "Best Practices",
|
|
22
|
+
recommended: true,
|
|
23
|
+
},
|
|
24
|
+
fixable: "code",
|
|
25
|
+
schema: [], // no options
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
CallExpression(node) {
|
|
30
|
+
const calleeName =
|
|
31
|
+
node.callee.type === "MemberExpression"
|
|
32
|
+
? node.callee.property.name
|
|
33
|
+
: null;
|
|
34
|
+
const isLookupCall = calleeName === "lookup";
|
|
35
|
+
const isModifyClassCall =
|
|
36
|
+
calleeName === "modifyClass" || calleeName === "modifyClassStatic";
|
|
37
|
+
|
|
38
|
+
if ((isLookupCall || isModifyClassCall) && node.arguments.length > 0) {
|
|
39
|
+
const firstArg = node.arguments[0];
|
|
40
|
+
if (firstArg.type === "Literal") {
|
|
41
|
+
const argValue = firstArg.value;
|
|
42
|
+
const replacement = replacements[argValue];
|
|
43
|
+
|
|
44
|
+
if (replacement) {
|
|
45
|
+
context.report({
|
|
46
|
+
node: firstArg,
|
|
47
|
+
message: `Use '${replacement}' instead of '${argValue}'`,
|
|
48
|
+
fix(fixer) {
|
|
49
|
+
return fixer.replaceText(firstArg, `"${replacement}"`);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { fixImport } from "./utils/fix-import.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
"disallow imports from 'i18n' and replace with 'discourse-i18n'",
|
|
9
|
+
category: "Best Practices",
|
|
10
|
+
recommended: false,
|
|
11
|
+
},
|
|
12
|
+
fixable: "code",
|
|
13
|
+
schema: [], // no options
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
ImportDeclaration(node) {
|
|
18
|
+
if (node.source.value.toLowerCase() === "i18n") {
|
|
19
|
+
context.report({
|
|
20
|
+
node,
|
|
21
|
+
message:
|
|
22
|
+
"Import from 'i18n' is not allowed. Use 'discourse-i18n' instead.",
|
|
23
|
+
fix(fixer) {
|
|
24
|
+
return fixer.replaceText(node.source, "'discourse-i18n'");
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
node.source.value.toLowerCase() === "discourse-common/helpers/i18n"
|
|
31
|
+
) {
|
|
32
|
+
context.report({
|
|
33
|
+
node,
|
|
34
|
+
message:
|
|
35
|
+
"Import from 'discourse-common/helpers/i18n' is not allowed. Use 'discourse-i18n' instead.",
|
|
36
|
+
fix(fixer) {
|
|
37
|
+
const existingImport = context
|
|
38
|
+
.getSourceCode()
|
|
39
|
+
.ast.body.find(
|
|
40
|
+
(n) =>
|
|
41
|
+
n.type === "ImportDeclaration" &&
|
|
42
|
+
n.source.value === "discourse-i18n"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (existingImport) {
|
|
46
|
+
return [
|
|
47
|
+
fixer.remove(node),
|
|
48
|
+
fixImport(fixer, existingImport, {
|
|
49
|
+
namedImportsToAdd: ["i18n"],
|
|
50
|
+
}),
|
|
51
|
+
];
|
|
52
|
+
} else {
|
|
53
|
+
return fixer.replaceText(
|
|
54
|
+
node,
|
|
55
|
+
`import { i18n } from 'discourse-i18n';`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { fixImport } from "./utils/fix-import.mjs";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Use i18n(...) instead of 'I18n.t(...)'.",
|
|
8
|
+
category: "Best Practices",
|
|
9
|
+
recommended: false,
|
|
10
|
+
},
|
|
11
|
+
fixable: "code",
|
|
12
|
+
schema: [], // no options
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
16
|
+
let alreadyFixedImport = false;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
MemberExpression(node) {
|
|
20
|
+
const isI18nT =
|
|
21
|
+
node.object.name === "I18n" && node.property.name === "t";
|
|
22
|
+
if (!isI18nT) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let scope = sourceCode.getScope(node);
|
|
27
|
+
let variable;
|
|
28
|
+
while (scope && !variable) {
|
|
29
|
+
variable = scope.variables.find((v) => v.name === "I18n");
|
|
30
|
+
scope = scope.upper;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!variable) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const i18nDefaultImport = variable.defs.find(
|
|
38
|
+
(d) =>
|
|
39
|
+
d.type === "ImportBinding" &&
|
|
40
|
+
d.node.type === "ImportDefaultSpecifier" &&
|
|
41
|
+
d.node.parent.source.value === "discourse-i18n"
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!i18nDefaultImport) {
|
|
45
|
+
// I18n imported from elsewhere... weird!
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
message: "Use 'i18n(...)' instead of 'I18n.t(...)'.",
|
|
52
|
+
fix(fixer) {
|
|
53
|
+
const fixes = [];
|
|
54
|
+
|
|
55
|
+
// Replace I18n.t with i18n
|
|
56
|
+
fixes.push(fixer.replaceText(node, `i18n`));
|
|
57
|
+
|
|
58
|
+
if (!alreadyFixedImport) {
|
|
59
|
+
const importDeclaration = i18nDefaultImport.node.parent;
|
|
60
|
+
const i18nSpecifier = importDeclaration.specifiers.find(
|
|
61
|
+
(specifier) =>
|
|
62
|
+
specifier.type === "ImportSpecifier" &&
|
|
63
|
+
specifier.imported.name === "i18n"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Check if I18n is used elsewhere
|
|
67
|
+
const shouldRemoveDefaultImport = !variable.references.some(
|
|
68
|
+
(ref) =>
|
|
69
|
+
ref.identifier.parent.type !== "MemberExpression" ||
|
|
70
|
+
ref.identifier.parent.property.name !== "t"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!i18nSpecifier || shouldRemoveDefaultImport) {
|
|
74
|
+
fixes.push(
|
|
75
|
+
fixImport(fixer, importDeclaration, {
|
|
76
|
+
defaultImport: !shouldRemoveDefaultImport,
|
|
77
|
+
namedImportsToAdd: ["i18n"],
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
alreadyFixedImport = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return fixes;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// no-queryselector-body-html.mjs
|
|
2
|
+
export default {
|
|
3
|
+
meta: {
|
|
4
|
+
type: "problem",
|
|
5
|
+
docs: {
|
|
6
|
+
description:
|
|
7
|
+
'disallow document.querySelector("body") and document.querySelector("html")',
|
|
8
|
+
category: "Best Practices",
|
|
9
|
+
recommended: false,
|
|
10
|
+
},
|
|
11
|
+
fixable: "code",
|
|
12
|
+
schema: [], // no options
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
CallExpression(node) {
|
|
17
|
+
const { callee, arguments: args } = node;
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
callee.type === "MemberExpression" &&
|
|
21
|
+
callee.object.name === "document" &&
|
|
22
|
+
callee.property.name === "querySelector" &&
|
|
23
|
+
args.length === 1 &&
|
|
24
|
+
args[0].type === "Literal"
|
|
25
|
+
) {
|
|
26
|
+
if (args[0].value === "body") {
|
|
27
|
+
context.report({
|
|
28
|
+
node,
|
|
29
|
+
message:
|
|
30
|
+
'Avoid using document.querySelector("body"). Use document.body instead.',
|
|
31
|
+
fix: (fixer) => fixer.replaceText(node, "document.body"),
|
|
32
|
+
});
|
|
33
|
+
} else if (args[0].value === "html") {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
message:
|
|
37
|
+
'Avoid using document.querySelector("html"). Use document.documentElement instead.',
|
|
38
|
+
fix: (fixer) =>
|
|
39
|
+
fixer.replaceText(node, "document.documentElement"),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Convert 'inject as service' to 'service'",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: false,
|
|
8
|
+
},
|
|
9
|
+
fixable: "code",
|
|
10
|
+
schema: [], // no options
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
return {
|
|
14
|
+
ImportDeclaration(node) {
|
|
15
|
+
if (node.source.value === "@ember/service") {
|
|
16
|
+
node.specifiers.forEach((specifier) => {
|
|
17
|
+
if (
|
|
18
|
+
specifier.type === "ImportSpecifier" &&
|
|
19
|
+
specifier.imported.name === "inject" &&
|
|
20
|
+
specifier.local.name === "service"
|
|
21
|
+
) {
|
|
22
|
+
context.report({
|
|
23
|
+
node: specifier,
|
|
24
|
+
message:
|
|
25
|
+
"Use direct 'service' import instead of 'inject as service'.",
|
|
26
|
+
fix(fixer) {
|
|
27
|
+
return fixer.replaceText(specifier, "service");
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix an import declaration
|
|
3
|
+
*
|
|
4
|
+
* @param {ASTNode} importDeclarationNode - The AST node representing the import declaration.
|
|
5
|
+
* @param {Object} options - Options for modifying the import statement.
|
|
6
|
+
* @param {undefined|false|string} options.defaultImport - Undefined to leave default import unchanged. False to remove it. String to set it to the given name, if it doesn't already exist.
|
|
7
|
+
* @param {string[]} options.namedImportsToAdd - Named imports to add to the import statement.
|
|
8
|
+
* @param {string[]} options.namedImportsToRemove - Named imports to remove from the import statement.
|
|
9
|
+
*/
|
|
10
|
+
export function fixImport(
|
|
11
|
+
fixer,
|
|
12
|
+
importDeclarationNode,
|
|
13
|
+
{ defaultImport, namedImportsToAdd = [], namedImportsToRemove = [] }
|
|
14
|
+
) {
|
|
15
|
+
const existingSpecifiers = importDeclarationNode.specifiers;
|
|
16
|
+
const existingDefaultImport = existingSpecifiers.find(
|
|
17
|
+
(specifier) => specifier.type === "ImportDefaultSpecifier"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Map existing named imports to their local names
|
|
21
|
+
const existingNamedImports = existingSpecifiers
|
|
22
|
+
.filter((specifier) => specifier.type === "ImportSpecifier")
|
|
23
|
+
.reduce((acc, specifier) => {
|
|
24
|
+
acc[specifier.imported.name] = specifier.local.name;
|
|
25
|
+
return acc;
|
|
26
|
+
}, {});
|
|
27
|
+
|
|
28
|
+
// Determine final default import
|
|
29
|
+
let finalDefaultImport;
|
|
30
|
+
if (defaultImport === undefined) {
|
|
31
|
+
finalDefaultImport = existingDefaultImport
|
|
32
|
+
? existingDefaultImport.local.name
|
|
33
|
+
: null;
|
|
34
|
+
} else if (defaultImport) {
|
|
35
|
+
finalDefaultImport = existingDefaultImport
|
|
36
|
+
? existingDefaultImport.local.name
|
|
37
|
+
: defaultImport;
|
|
38
|
+
} else {
|
|
39
|
+
finalDefaultImport = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Determine final named imports, preserving aliases
|
|
43
|
+
const finalNamedImports = Array.from(
|
|
44
|
+
new Set([
|
|
45
|
+
...Object.entries(existingNamedImports)
|
|
46
|
+
.filter(([imported]) => !namedImportsToRemove.includes(imported))
|
|
47
|
+
.map(([imported, local]) =>
|
|
48
|
+
imported === local ? imported : `${imported} as ${local}`
|
|
49
|
+
),
|
|
50
|
+
...namedImportsToAdd,
|
|
51
|
+
])
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Construct the new import statement
|
|
55
|
+
let newImportStatement = "import ";
|
|
56
|
+
if (finalDefaultImport) {
|
|
57
|
+
newImportStatement += `${finalDefaultImport}`;
|
|
58
|
+
if (finalNamedImports.length > 0) {
|
|
59
|
+
newImportStatement += ", ";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (finalNamedImports.length > 0) {
|
|
63
|
+
newImportStatement += `{ ${finalNamedImports.join(", ")} }`;
|
|
64
|
+
}
|
|
65
|
+
newImportStatement += ` from '${importDeclarationNode.source.value}';`;
|
|
66
|
+
|
|
67
|
+
// Replace the entire import declaration
|
|
68
|
+
return fixer.replaceText(importDeclarationNode, newImportStatement);
|
|
69
|
+
}
|
package/eslint.mjs
CHANGED
|
@@ -9,6 +9,11 @@ import QUnitRecommended from "eslint-plugin-qunit/configs/recommended";
|
|
|
9
9
|
import SimpleImportSort from "eslint-plugin-simple-import-sort";
|
|
10
10
|
import SortClassMembers from "eslint-plugin-sort-class-members";
|
|
11
11
|
import globals from "globals";
|
|
12
|
+
import deprecatedLookups from "./eslint-rules/deprecated-lookups.mjs";
|
|
13
|
+
import i18nImport from "./eslint-rules/i18n-import-location.mjs";
|
|
14
|
+
import i18nT from "./eslint-rules/i18n-t.mjs";
|
|
15
|
+
import noSimpleQueryselector from "./eslint-rules/no-simple-queryselector.mjs";
|
|
16
|
+
import serviceInjectImport from "./eslint-rules/service-inject-import.mjs";
|
|
12
17
|
|
|
13
18
|
// Copied from "ember-template-imports/lib/utils"
|
|
14
19
|
const TEMPLATE_TAG_PLACEHOLDER = "__GLIMMER_TEMPLATE";
|
|
@@ -85,6 +90,15 @@ export default [
|
|
|
85
90
|
"decorator-position": DecoratorPosition,
|
|
86
91
|
"simple-import-sort": SimpleImportSort,
|
|
87
92
|
qunit: QUnitPlugin,
|
|
93
|
+
discourse: {
|
|
94
|
+
rules: {
|
|
95
|
+
"i18n-import-location": i18nImport,
|
|
96
|
+
"i18n-t": i18nT,
|
|
97
|
+
"service-inject-import": serviceInjectImport,
|
|
98
|
+
"no-simple-queryselector": noSimpleQueryselector,
|
|
99
|
+
"deprecated-lookups": deprecatedLookups,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
88
102
|
},
|
|
89
103
|
rules: {
|
|
90
104
|
"block-scoped-var": "error",
|
|
@@ -249,6 +263,13 @@ export default [
|
|
|
249
263
|
],
|
|
250
264
|
},
|
|
251
265
|
],
|
|
266
|
+
// TODO: enable by default once this commit is available widely
|
|
267
|
+
// https://github.com/discourse/discourse/commit/d606ac3d8e
|
|
268
|
+
// "discourse/i18n-import-location": ["error"],
|
|
269
|
+
// "discourse/i18n-t": ["error"],
|
|
270
|
+
"discourse/service-inject-import": ["error"],
|
|
271
|
+
"discourse/no-simple-queryselector": ["error"],
|
|
272
|
+
"discourse/deprecated-lookups": ["error"],
|
|
252
273
|
},
|
|
253
274
|
},
|
|
254
275
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discourse/lint-configs",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Shareable lint configs for Discourse core, plugins, and themes",
|
|
5
5
|
"author": "Discourse",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
},
|
|
22
22
|
"./template-lint": {
|
|
23
23
|
"require": "./template-lint.config.cjs"
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
|
+
"./template-lint-rules": "./template-lint-rules/index.mjs"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"lint": "eslint --no-error-on-unmatched-pattern **/*.cjs */**.mjs **/*.js && pnpm prettier --check .",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
"@babel/plugin-proposal-decorators": "^7.25.7",
|
|
34
35
|
"ember-template-lint": "^6.0.0",
|
|
35
36
|
"eslint": "^9.14.0",
|
|
36
|
-
"eslint-plugin-decorator-position": "^
|
|
37
|
+
"eslint-plugin-decorator-position": "^6.0.0",
|
|
37
38
|
"eslint-plugin-ember": "^12.3.1",
|
|
38
39
|
"eslint-plugin-qunit": "^8.1.2",
|
|
39
40
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
package/template-lint.config.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
extends: ["recommended", "stylistic"],
|
|
3
|
+
plugins: ["@discourse/lint-configs/template-lint-rules"],
|
|
3
4
|
rules: {
|
|
4
5
|
// Intentionally disabled default rules
|
|
5
6
|
"no-autofocus-attribute": false,
|
|
@@ -42,5 +43,8 @@ module.exports = {
|
|
|
42
43
|
"eol-last": false,
|
|
43
44
|
quotes: false,
|
|
44
45
|
"self-closing-void-elements": false,
|
|
46
|
+
|
|
47
|
+
// Discourse custom
|
|
48
|
+
"discourse/no-at-class": true,
|
|
45
49
|
},
|
|
46
50
|
};
|