@discourse/lint-configs 2.20.0 → 2.22.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.
@@ -0,0 +1,65 @@
1
+ export default {
2
+ meta: {
3
+ type: "suggestion",
4
+ docs: {
5
+ description: "Component names should start with a capital letter.",
6
+ },
7
+ fixable: "code",
8
+ schema: [], // no options
9
+ },
10
+
11
+ create(context) {
12
+ return {
13
+ GlimmerElementNode(node) {
14
+ if (node.name === "template") {
15
+ return;
16
+ }
17
+
18
+ if (!node.name.match(/^[a-z]/)) {
19
+ return;
20
+ }
21
+
22
+ const moduleScope = context.sourceCode.scopeManager.scopes.find(
23
+ (s) => s.type === "module"
24
+ );
25
+ const variable = moduleScope.variables.find(
26
+ (v) => v.name === node.name
27
+ );
28
+
29
+ if (!variable) {
30
+ return;
31
+ }
32
+
33
+ const newVariableName = variable.name.replace(/^[a-z]/, (char) =>
34
+ char.toUpperCase()
35
+ );
36
+
37
+ const importBinding = variable.defs.find(
38
+ (d) =>
39
+ d.type === "ImportBinding" &&
40
+ d.node.type === "ImportDefaultSpecifier"
41
+ );
42
+
43
+ if (!importBinding) {
44
+ return;
45
+ }
46
+
47
+ context.report({
48
+ node: node.openTag,
49
+ message: `Component names should start with a capital letter.`,
50
+ fix(fixer) {
51
+ const fixes = [];
52
+
53
+ fixes.push(fixer.replaceText(importBinding.node, newVariableName));
54
+
55
+ variable.references.forEach((ref) => {
56
+ fixes.push(fixer.replaceText(ref.identifier, newVariableName));
57
+ });
58
+
59
+ return fixes;
60
+ },
61
+ });
62
+ },
63
+ };
64
+ },
65
+ };
@@ -0,0 +1,116 @@
1
+ import path from "path";
2
+
3
+ function lintCurlyComponent(node, context) {
4
+ const isSimplePath =
5
+ node.path.type === "GlimmerPathExpression" &&
6
+ node.path.head.type === "VarHead" &&
7
+ !node.tail?.length;
8
+
9
+ if (!isSimplePath) {
10
+ return;
11
+ }
12
+
13
+ let check = node.parent;
14
+ while (check) {
15
+ if (check.type === "GlimmerAttrNode") {
16
+ // <Foo @bar={{baz}} />
17
+ return;
18
+ }
19
+ check = check.parent;
20
+ }
21
+
22
+ const variableName = node.path.head.name;
23
+
24
+ const moduleScope = context.sourceCode.scopeManager.scopes.find(
25
+ (s) => s.type === "module"
26
+ );
27
+ const variable = moduleScope.variables.find((v) => v.name === variableName);
28
+
29
+ if (!variable) {
30
+ return;
31
+ }
32
+
33
+ const importBinding = variable.defs.find(
34
+ (d) =>
35
+ d.type === "ImportBinding" && d.node.type === "ImportDefaultSpecifier"
36
+ );
37
+
38
+ if (!importBinding) {
39
+ return;
40
+ }
41
+
42
+ const importedModuleName = importBinding.node.parent.source.value;
43
+
44
+ // This is not perfect, but it should catch 99% of components
45
+ let resolvedModuleName = importedModuleName;
46
+ if (importedModuleName.startsWith(".")) {
47
+ const cwd = context.cwd;
48
+ const sourceDirectoryFromCwd = path.dirname(
49
+ path.relative(cwd, context.getFilename())
50
+ );
51
+
52
+ resolvedModuleName = path.join(sourceDirectoryFromCwd, importedModuleName);
53
+ }
54
+
55
+ if (!resolvedModuleName.includes("/components/")) {
56
+ return;
57
+ }
58
+
59
+ context.report({
60
+ node,
61
+ message: `Use angle bracket syntax for components.`,
62
+ fix(fixer) {
63
+ const fixes = [];
64
+
65
+ let argumentString = "";
66
+ node.hash?.pairs.forEach(({ key, value }) => {
67
+ let valueSource = context.sourceCode.getText(value);
68
+ valueSource = valueSource.replace(/^\(/, "").replace(/\)$/, "");
69
+ if (value.type !== "GlimmerStringLiteral") {
70
+ valueSource = `{{${valueSource}}}`;
71
+ }
72
+ argumentString += `@${key}=${valueSource} `;
73
+ });
74
+
75
+ if (node.type === "GlimmerBlockStatement") {
76
+ fixes.push(
77
+ fixer.replaceText(
78
+ node,
79
+ `<${variable.name} ${argumentString}>${context.sourceCode.getText(
80
+ node.program
81
+ )}</${variable.name}>`
82
+ )
83
+ );
84
+ } else if (node.type === "GlimmerMustacheStatement") {
85
+ fixes.push(
86
+ fixer.replaceText(node, `<${variable.name} ${argumentString}/>`)
87
+ );
88
+ }
89
+
90
+ return fixes;
91
+ },
92
+ });
93
+ }
94
+
95
+ export default {
96
+ meta: {
97
+ type: "suggestion",
98
+ docs: {
99
+ description: "Use angle-bracket syntax for components.",
100
+ },
101
+ fixable: "code",
102
+ schema: [], // no options
103
+ },
104
+
105
+ create(context) {
106
+ return {
107
+ GlimmerBlockStatement(node) {
108
+ return lintCurlyComponent(node, context);
109
+ },
110
+
111
+ GlimmerMustacheStatement(node) {
112
+ return lintCurlyComponent(node, context);
113
+ },
114
+ };
115
+ },
116
+ };
package/eslint.mjs CHANGED
@@ -12,6 +12,7 @@ import QUnitRecommended from "eslint-plugin-qunit/configs/recommended";
12
12
  import SimpleImportSort from "eslint-plugin-simple-import-sort";
13
13
  import SortClassMembers from "eslint-plugin-sort-class-members";
14
14
  import globals from "globals";
15
+ import capitalComponents from "./eslint-rules/capital-components.mjs";
15
16
  import deprecatedLookups from "./eslint-rules/deprecated-lookups.mjs";
16
17
  import deprecatedPluginApis from "./eslint-rules/deprecated-plugin-apis.mjs";
17
18
  import discourseCommonImports from "./eslint-rules/discourse-common-imports.mjs";
@@ -20,6 +21,7 @@ import i18nT from "./eslint-rules/i18n-t.mjs";
20
21
  import lineAfterImports from "./eslint-rules/line-after-imports.mjs";
21
22
  import lineBeforeDefaultExport from "./eslint-rules/line-before-default-export.mjs";
22
23
  import linesBetweenClassMembers from "./eslint-rules/lines-between-class-members.mjs";
24
+ import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
23
25
  import noSimpleQuerySelector from "./eslint-rules/no-simple-query-selector.mjs";
24
26
  import serviceInjectImport from "./eslint-rules/service-inject-import.mjs";
25
27
  import truthHelpersImports from "./eslint-rules/truth-helpers-imports.mjs";
@@ -122,6 +124,8 @@ export default [
122
124
  "deprecated-plugin-apis": deprecatedPluginApis,
123
125
  "line-after-imports": lineAfterImports,
124
126
  "line-before-default-export": lineBeforeDefaultExport,
127
+ "no-curly-components": noCurlyComponents,
128
+ "capital-components": capitalComponents,
125
129
  },
126
130
  },
127
131
  },
@@ -186,8 +190,6 @@ export default [
186
190
  "ember/avoid-leaking-state-in-ember-objects": "off",
187
191
  "ember/no-get": "off",
188
192
  "ember/no-observers": "off",
189
- "ember/no-mixins": "off",
190
- "ember/no-new-mixins": "off",
191
193
  "ember/no-implicit-injections": "off", // this rule is broken
192
194
  "ember/no-array-prototype-extensions": "off",
193
195
  "ember/no-at-ember-render-modifiers": "off",
@@ -296,6 +298,8 @@ export default [
296
298
  "discourse/lines-between-class-members": ["error"],
297
299
  "discourse/line-after-imports": ["error"],
298
300
  "discourse/line-before-default-export": ["error"],
301
+ "discourse/no-curly-components": ["error"],
302
+ "discourse/capital-components": ["error"],
299
303
  },
300
304
  },
301
305
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourse/lint-configs",
3
- "version": "2.20.0",
3
+ "version": "2.22.0",
4
4
  "description": "Shareable lint configs for Discourse core, plugins, and themes",
5
5
  "author": "Discourse",
6
6
  "license": "MIT",