@discourse/lint-configs 2.46.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 CHANGED
@@ -33,9 +33,3 @@ export default {
33
33
  extends: ["@discourse/lint-configs/stylelint"],
34
34
  };
35
35
  ```
36
-
37
- ### .template-lintrc.cjs
38
-
39
- ```js
40
- module.exports = require("@discourse/lint-configs/template-lint");
41
- ```
@@ -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
+ }
@@ -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
- const hasSameVariableScope =
33
- context.sourceCode.getScope(ref.identifier).variableScope ===
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) {
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,15 +23,18 @@ 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";
@@ -50,14 +53,25 @@ export default [
50
53
  js.configs.recommended,
51
54
  QUnitRecommended,
52
55
  ...EmberRecommended,
56
+ ...EmberTemplateLintMigration,
53
57
  {
54
58
  ignores: ["assets/vendor/**/*", "public/**/*"],
55
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
+ },
56
71
  {
57
72
  languageOptions: {
58
73
  ecmaVersion: 2022,
59
74
  sourceType: "module",
60
- parser: BabelParser,
61
75
  parserOptions: {
62
76
  useBabel: true,
63
77
  requireConfigFile: false,
@@ -66,7 +80,6 @@ export default [
66
80
  plugins: [[decoratorsPluginPath, { legacy: true }]],
67
81
  },
68
82
  },
69
-
70
83
  globals: {
71
84
  ...globals.browser,
72
85
  ...globals.node,
@@ -155,6 +168,9 @@ export default [
155
168
  "migrate-tracked-built-ins-to-ember-collections":
156
169
  migrateTrackedBuiltInsToEmberCollections,
157
170
  "ui-kit-imports": uiKitImports,
171
+ "no-at-class": noAtClass,
172
+ "plugin-outlet-lazy-hash": pluginOutletLazyHash,
173
+ "no-template-lint-directives": noTemplateLintDirectives,
158
174
  },
159
175
  },
160
176
  },
@@ -223,6 +239,39 @@ export default [
223
239
  "ember/no-unnecessary-service-injection-argument": "error",
224
240
  "ember/no-replace-test-comments": "error",
225
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
+
226
275
  "qunit/no-loose-assertions": "error",
227
276
  "qunit/no-identical-names": "off", // the rule doesn't consider that tests might be in different `acceptance` modules
228
277
  "sort-class-members/sort-class-members": [
@@ -323,12 +372,9 @@ export default [
323
372
  "discourse/no-unnecessary-tracked": ["warn"],
324
373
  "discourse/migrate-tracked-built-ins-to-ember-collections": ["error"],
325
374
  "discourse/ui-kit-imports": ["error"],
326
- },
327
- },
328
- {
329
- files: ["**/*.gjs", "**/*.gts"],
330
- languageOptions: {
331
- parser: EmberESLintParser,
375
+ "discourse/no-at-class": ["error"],
376
+ "discourse/plugin-outlet-lazy-hash": ["error"],
377
+ "discourse/no-template-lint-directives": ["error"],
332
378
  },
333
379
  },
334
380
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourse/lint-configs",
3
- "version": "2.46.0",
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": "^12.7.5",
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
- }
@@ -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
- };