@discourse/lint-configs 2.44.0 → 2.45.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,436 @@
1
+ /**
2
+ * @fileoverview ESLint rule to replace computed property macro decorators
3
+ * from `@ember/object/computed` and `discourse/lib/computed` with native
4
+ * getters using `@computed` or `@dependentKeyCompat` + `@tracked`.
5
+ */
6
+
7
+ import { analyzeMacroUsage } from "./no-computed-macros/computed-macros-analysis.mjs";
8
+ import { createClassFix } from "./no-computed-macros/computed-macros-fixer.mjs";
9
+ import { MACRO_SOURCES } from "./no-computed-macros/macro-transforms.mjs";
10
+ import {
11
+ collectImports,
12
+ getImportedLocalNames,
13
+ } from "./utils/analyze-imports.mjs";
14
+ import { buildImportStatement, fixImport } from "./utils/fix-import.mjs";
15
+
16
+ const USE_NATIVE_GETTER_INSTEAD = "Use a native getter instead of `@{{name}}`";
17
+
18
+ export default {
19
+ meta: {
20
+ type: "suggestion",
21
+ docs: {
22
+ description: "Replace computed property macros with native getters",
23
+ },
24
+ fixable: "code",
25
+ schema: [],
26
+ messages: {
27
+ replaceMacro: `${USE_NATIVE_GETTER_INSTEAD}.`,
28
+ addTracked:
29
+ "Add `@tracked` to `{{name}}` (dependency of a converted macro).",
30
+ cannotAutoFixComplex: `${USE_NATIVE_GETTER_INSTEAD}: {{reason}}.`,
31
+ cannotAutoFixDynamic: `${USE_NATIVE_GETTER_INSTEAD}: it has non-literal arguments (convert manually).`,
32
+ cannotAutoFixSelfReference: `${USE_NATIVE_GETTER_INSTEAD}: \`{{propName}}\` references itself (convert manually).`,
33
+ },
34
+ },
35
+
36
+ create(context) {
37
+ const sourceCode = context.getSourceCode();
38
+ let analysis = null;
39
+ let importsMap = null;
40
+ let importFixGenerated = false;
41
+
42
+ function ensureAnalysis() {
43
+ if (analysis) {
44
+ return analysis;
45
+ }
46
+ importsMap = collectImports(sourceCode);
47
+ analysis = analyzeMacroUsage(sourceCode, importsMap);
48
+ return analysis;
49
+ }
50
+
51
+ // Precompute per-class fixable usages and existing nodes to decorate.
52
+ // Lazily initialised on first access (like analysis).
53
+ let classFixes = null;
54
+ function ensureClassFixes() {
55
+ if (classFixes) {
56
+ return classFixes;
57
+ }
58
+ const { usages } = ensureAnalysis();
59
+ classFixes = new Map();
60
+
61
+ for (const usage of usages) {
62
+ if (!usage.canAutoFix) {
63
+ continue;
64
+ }
65
+ const classBody = usage.propertyNode.parent;
66
+ if (!classFixes.has(classBody)) {
67
+ classFixes.set(classBody, {
68
+ fixableUsages: [],
69
+ existingNodesToDecorate: new Set(),
70
+ });
71
+ }
72
+ const entry = classFixes.get(classBody);
73
+ entry.fixableUsages.push(usage);
74
+ if (usage.existingNodesToDecorate) {
75
+ for (const node of usage.existingNodesToDecorate) {
76
+ entry.existingNodesToDecorate.add(node);
77
+ }
78
+ }
79
+ }
80
+
81
+ return classFixes;
82
+ }
83
+
84
+ // Track which class bodies have already had a combined fix attached
85
+ const classFixAttached = new Set();
86
+
87
+ return {
88
+ // Report on macro import declarations — only the import-level fix here
89
+ ImportDeclaration(node) {
90
+ if (!MACRO_SOURCES.has(node.source.value)) {
91
+ return;
92
+ }
93
+
94
+ const { usages, importedMacros, macroImportNodes } = ensureAnalysis();
95
+
96
+ // Only report specifiers that have at least one usage in a native class
97
+ const usedLocalNames = new Set(usages.map((u) => u.localName));
98
+ const macroSpecifiers = node.specifiers.filter(
99
+ (spec) =>
100
+ spec.type === "ImportSpecifier" &&
101
+ importedMacros.has(spec.local.name) &&
102
+ usedLocalNames.has(spec.local.name)
103
+ );
104
+
105
+ if (macroSpecifiers.length === 0) {
106
+ return;
107
+ }
108
+
109
+ const fixableUsages = usages.filter((u) => u.canAutoFix);
110
+ const hasAnyFix = fixableUsages.length > 0;
111
+
112
+ // Build the import fix ONCE across all macro import declarations.
113
+ // When a file imports from both @ember/object/computed and
114
+ // discourse/lib/computed, we must handle all import nodes in a
115
+ // single fix to avoid duplicate new import lines.
116
+ let importFix;
117
+ if (hasAnyFix && !importFixGenerated) {
118
+ importFixGenerated = true;
119
+ importFix = (fixer) =>
120
+ buildImportFixes(fixer, {
121
+ sourceCode,
122
+ importsMap,
123
+ usages: fixableUsages,
124
+ macroImportNodes,
125
+ });
126
+ }
127
+
128
+ let fixAttached = false;
129
+ for (const spec of macroSpecifiers) {
130
+ const macroName = importedMacros.get(spec.local.name);
131
+ context.report({
132
+ node: spec,
133
+ messageId: "replaceMacro",
134
+ data: { name: macroName },
135
+ fix: !fixAttached ? importFix : undefined,
136
+ });
137
+ fixAttached = true;
138
+ }
139
+ },
140
+
141
+ // Report on each macro usage in class bodies.
142
+ // The combined class-body fix (remove macros + insert getters at the
143
+ // correct position) is attached to the FIRST fixable macro per class.
144
+ PropertyDefinition(node) {
145
+ if (!node.decorators) {
146
+ return;
147
+ }
148
+
149
+ const { usages } = ensureAnalysis();
150
+ const usage = usages.find((u) => u.propertyNode === node);
151
+
152
+ if (!usage) {
153
+ return;
154
+ }
155
+
156
+ let fix;
157
+ if (usage.canAutoFix) {
158
+ const classBody = node.parent;
159
+ const fixesMap = ensureClassFixes();
160
+ const classEntry = fixesMap.get(classBody);
161
+ if (classEntry && !classFixAttached.has(classBody)) {
162
+ classFixAttached.add(classBody);
163
+ fix = createClassFix(
164
+ classEntry.fixableUsages,
165
+ classEntry.existingNodesToDecorate,
166
+ sourceCode
167
+ );
168
+ }
169
+ }
170
+
171
+ context.report({
172
+ node: usage.decoratorNode || node,
173
+ messageId: usage.messageId || "replaceMacro",
174
+ data: usage.reportData || { name: usage.macroName },
175
+ fix,
176
+ });
177
+ },
178
+
179
+ // Report @tracked additions for existing class members.
180
+ // The actual fix is included in the combined class-body fix
181
+ // (attached to the first fixable macro in each class) to avoid
182
+ // overlapping fix ranges.
183
+ "Program:exit"() {
184
+ const { usages } = ensureAnalysis();
185
+ const decorated = new Set();
186
+
187
+ for (const usage of usages) {
188
+ if (!usage.canAutoFix || !usage.existingNodesToDecorate) {
189
+ continue;
190
+ }
191
+ for (const memberNode of usage.existingNodesToDecorate) {
192
+ if (decorated.has(memberNode)) {
193
+ continue;
194
+ }
195
+ decorated.add(memberNode);
196
+
197
+ const name =
198
+ memberNode.key.type === "Identifier"
199
+ ? memberNode.key.name
200
+ : String(memberNode.key.value);
201
+ context.report({
202
+ node: memberNode,
203
+ messageId: "addTracked",
204
+ data: { name },
205
+ });
206
+ }
207
+ }
208
+ },
209
+ };
210
+ },
211
+ };
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Import fix builder
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /**
218
+ * Build import-related fixes for ALL fixable macro usages.
219
+ *
220
+ * This function handles ALL macro import nodes at once to avoid producing
221
+ * duplicate new import lines when a file imports macros from both
222
+ * `@ember/object/computed` and `discourse/lib/computed`.
223
+ *
224
+ * Note: adding `@tracked` to existing class members is handled by the
225
+ * combined class-body fix (in `createClassFix`), not here.
226
+ *
227
+ * @returns {import('eslint').Rule.Fix[]}
228
+ */
229
+ function buildImportFixes(
230
+ fixer,
231
+ { sourceCode, importsMap, usages, macroImportNodes }
232
+ ) {
233
+ const fixes = [];
234
+ const allImportedNames = getImportedLocalNames(sourceCode);
235
+
236
+ // Exclude names we're about to remove from the collision set
237
+ const fixableLocalNames = new Set(usages.map((u) => u.localName));
238
+ for (const name of fixableLocalNames) {
239
+ allImportedNames.delete(name);
240
+ }
241
+
242
+ // Collect all required new imports from fixable usages
243
+ const newImports = collectRequiredImports(usages);
244
+
245
+ // Determine conditional imports based on dep classification
246
+ const needsComputed = usages.some((u) => !u.allLocal);
247
+ const needsDependentKeyCompat = usages.some((u) => u.allLocal);
248
+ const needsTracked = usages.some(
249
+ (u) =>
250
+ (u.allLocal &&
251
+ (u.trackedDeps?.length > 0 || u.existingNodesToDecorate?.length > 0)) ||
252
+ u.transform.overrideTrackedFields
253
+ );
254
+
255
+ // Add in the order we want them to appear in the output
256
+ if (needsTracked) {
257
+ addToImportSet(newImports, "@glimmer/tracking", "tracked");
258
+ }
259
+ if (needsComputed) {
260
+ addToImportSet(newImports, "@ember/object", "computed");
261
+ }
262
+ // Setter-specific imports (e.g. set from @ember/object for alias).
263
+ // Added after computed so that named imports appear in the right order.
264
+ for (const usage of usages) {
265
+ if (!usage.allLocal && usage.transform.setterRequiredImports) {
266
+ for (const req of usage.transform.setterRequiredImports) {
267
+ addToImportSet(newImports, req.source, req.name, req.isDefault);
268
+ }
269
+ }
270
+ }
271
+ if (needsDependentKeyCompat) {
272
+ addToImportSet(newImports, "@ember/object/compat", "dependentKeyCompat");
273
+ }
274
+
275
+ // Build new import lines for required imports (once for all macro sources)
276
+ const newImportLines = [];
277
+ for (const [source, names] of newImports) {
278
+ const existing = importsMap.get(source);
279
+
280
+ if (existing) {
281
+ // Modify existing import — generate a fixImport call
282
+ const existingNamedSet = new Set(
283
+ existing.specifiers
284
+ .filter((s) => s.type === "ImportSpecifier")
285
+ .map((s) => s.imported.name)
286
+ );
287
+ const defaultSpec = existing.specifiers.find(
288
+ (s) => s.type === "ImportDefaultSpecifier"
289
+ );
290
+
291
+ const namedToAdd = [];
292
+ let defaultToAdd;
293
+
294
+ for (const { name, isDefault } of names) {
295
+ if (isDefault) {
296
+ if (!defaultSpec) {
297
+ defaultToAdd = name;
298
+ }
299
+ } else if (!existingNamedSet.has(name)) {
300
+ const localName = allImportedNames.has(name) ? `${name}Import` : name;
301
+ namedToAdd.push(
302
+ localName === name ? name : `${name} as ${localName}`
303
+ );
304
+ }
305
+ }
306
+
307
+ if (namedToAdd.length > 0 || defaultToAdd) {
308
+ fixes.push(
309
+ fixImport(fixer, existing.node, {
310
+ defaultImport: defaultToAdd,
311
+ namedImportsToAdd: namedToAdd,
312
+ })
313
+ );
314
+ }
315
+ } else {
316
+ // Build a new import line string (will be appended below)
317
+ newImportLines.push(
318
+ resolveNewImportLine(source, names, allImportedNames)
319
+ );
320
+ }
321
+ }
322
+
323
+ // Process each macro import node — remove/replace specifiers
324
+ let newImportsPlaced = false;
325
+ for (const [, importNode] of macroImportNodes) {
326
+ const removableSpecifiers = importNode.specifiers.filter(
327
+ (spec) =>
328
+ spec.type === "ImportSpecifier" &&
329
+ fixableLocalNames.has(spec.local.name) &&
330
+ usages
331
+ .filter((u) => u.localName === spec.local.name)
332
+ .every((u) => u.canAutoFix)
333
+ );
334
+ const remainingSpecifiers = importNode.specifiers.filter(
335
+ (s) => !removableSpecifiers.includes(s)
336
+ );
337
+
338
+ if (removableSpecifiers.length === 0) {
339
+ continue;
340
+ }
341
+
342
+ if (remainingSpecifiers.length === 0) {
343
+ // All specifiers removed — replace with new imports or remove entirely
344
+ if (!newImportsPlaced && newImportLines.length > 0) {
345
+ fixes.push(fixer.replaceText(importNode, newImportLines.join("\n")));
346
+ newImportsPlaced = true;
347
+ } else {
348
+ // Remove the entire import line (including trailing newline)
349
+ const text = sourceCode.getText();
350
+ let end = importNode.range[1];
351
+ if (end < text.length && text[end] === "\n") {
352
+ end++;
353
+ }
354
+ fixes.push(fixer.removeRange([importNode.range[0], end]));
355
+ }
356
+ } else {
357
+ // Some specifiers remain — remove fixable ones
358
+ const namesToRemove = removableSpecifiers.map((s) => s.imported.name);
359
+ fixes.push(
360
+ fixImport(fixer, importNode, {
361
+ namedImportsToRemove: namesToRemove,
362
+ })
363
+ );
364
+
365
+ // Append new import lines after this import (only once)
366
+ if (!newImportsPlaced && newImportLines.length > 0) {
367
+ for (const line of newImportLines) {
368
+ fixes.push(fixer.insertTextAfter(importNode, `\n${line}`));
369
+ }
370
+ newImportsPlaced = true;
371
+ }
372
+ }
373
+ }
374
+
375
+ return fixes;
376
+ }
377
+
378
+ /**
379
+ * Resolve import names (handling collisions) and delegate to
380
+ * the shared `buildImportStatement` utility.
381
+ *
382
+ * @param {string} source
383
+ * @param {Array<{name: string, isDefault?: boolean}>} names
384
+ * @param {Set<string>} allImportedNames
385
+ * @returns {string}
386
+ */
387
+ function resolveNewImportLine(source, names, allImportedNames) {
388
+ const namedImports = [];
389
+ let defaultImport;
390
+
391
+ for (const { name, isDefault } of names) {
392
+ if (isDefault) {
393
+ defaultImport = name;
394
+ } else {
395
+ const localName = allImportedNames.has(name) ? `${name}Import` : name;
396
+ namedImports.push(localName === name ? name : `${name} as ${localName}`);
397
+ }
398
+ }
399
+
400
+ return buildImportStatement(source, { defaultImport, namedImports });
401
+ }
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Import helpers
405
+ // ---------------------------------------------------------------------------
406
+
407
+ /**
408
+ * Collect all required imports from fixable usages into a Map.
409
+ */
410
+ function collectRequiredImports(usages) {
411
+ const result = new Map();
412
+
413
+ for (const usage of usages) {
414
+ if (!usage.canAutoFix || !usage.transform.requiredImports) {
415
+ continue;
416
+ }
417
+ for (const req of usage.transform.requiredImports) {
418
+ addToImportSet(result, req.source, req.name, req.isDefault);
419
+ }
420
+ }
421
+
422
+ return result;
423
+ }
424
+
425
+ /**
426
+ * Add a named or default import to the import set, avoiding duplicates.
427
+ */
428
+ function addToImportSet(importSet, source, name, isDefault = false) {
429
+ if (!importSet.has(source)) {
430
+ importSet.set(source, []);
431
+ }
432
+ const list = importSet.get(source);
433
+ if (!list.some((i) => i.name === name && i.isDefault === isDefault)) {
434
+ list.push({ name, isDefault });
435
+ }
436
+ }
@@ -4,7 +4,7 @@ import {
4
4
  collectImports,
5
5
  getImportedLocalNames,
6
6
  } from "./utils/analyze-imports.mjs";
7
- import { fixImport } from "./utils/fix-import.mjs";
7
+ import { buildImportStatement, fixImport } from "./utils/fix-import.mjs";
8
8
 
9
9
  const USE_COMPUTED_INSTEAD = "Use `@computed` instead of `@{{name}}`";
10
10
 
@@ -75,12 +75,10 @@ function fixDiscourseImport(
75
75
  })
76
76
  );
77
77
  } else {
78
- fixes.push(
79
- fixer.insertTextAfter(
80
- importNode,
81
- `\nimport { ${computedImportString} } from "@ember/object";`
82
- )
83
- );
78
+ const newImport = buildImportStatement("@ember/object", {
79
+ namedImports: [computedImportString],
80
+ });
81
+ fixes.push(fixer.insertTextAfter(importNode, `\n${newImport}`));
84
82
  }
85
83
  }
86
84
  } else {
@@ -103,7 +101,9 @@ function fixDiscourseImport(
103
101
  fixes.push(
104
102
  fixer.replaceText(
105
103
  importNode,
106
- `import { ${computedImportString} } from "@ember/object";`
104
+ buildImportStatement("@ember/object", {
105
+ namedImports: [computedImportString],
106
+ })
107
107
  )
108
108
  );
109
109
  }
@@ -128,13 +128,15 @@ function fixDiscourseImport(
128
128
  }
129
129
  });
130
130
 
131
- let newImportStatement = "import discourseComputed";
132
- if (namedImportStrings.length > 0) {
133
- newImportStatement += `, { ${namedImportStrings.join(", ")} }`;
134
- }
135
- newImportStatement += ` from "${importNode.source.value}";`;
136
-
137
- fixes.push(fixer.replaceText(importNode, newImportStatement));
131
+ fixes.push(
132
+ fixer.replaceText(
133
+ importNode,
134
+ buildImportStatement(importNode.source.value, {
135
+ defaultImport: "discourseComputed",
136
+ namedImports: namedImportStrings,
137
+ })
138
+ )
139
+ );
138
140
  }
139
141
 
140
142
  if (!hasComputedImport) {
@@ -145,12 +147,10 @@ function fixDiscourseImport(
145
147
  })
146
148
  );
147
149
  } else {
148
- fixes.push(
149
- fixer.insertTextAfter(
150
- importNode,
151
- `\nimport { ${computedImportString} } from "@ember/object";`
152
- )
153
- );
150
+ const newImport = buildImportStatement("@ember/object", {
151
+ namedImports: [computedImportString],
152
+ });
153
+ fixes.push(fixer.insertTextAfter(importNode, `\n${newImport}`));
154
154
  }
155
155
  }
156
156
  }
@@ -1,3 +1,5 @@
1
+ import { buildImportStatement } from "./utils/fix-import.mjs";
2
+
1
3
  export default {
2
4
  meta: {
3
5
  type: "suggestion",
@@ -24,11 +26,13 @@ export default {
24
26
  sourceName = "notEq";
25
27
  }
26
28
 
27
- if (localName === sourceName) {
28
- code = `import { ${localName} } from 'truth-helpers';`;
29
- } else {
30
- code = `import { ${sourceName} as ${localName} } from 'truth-helpers';`;
31
- }
29
+ const namedImport =
30
+ localName === sourceName
31
+ ? localName
32
+ : `${sourceName} as ${localName}`;
33
+ code = buildImportStatement("truth-helpers", {
34
+ namedImports: [namedImport],
35
+ });
32
36
 
33
37
  return fixer.replaceText(node, code);
34
38
  },
@@ -1,3 +1,31 @@
1
+ /**
2
+ * Build an import statement string from its parts.
3
+ *
4
+ * @param {string} source - The import source (e.g. "@ember/object").
5
+ * @param {Object} [options]
6
+ * @param {string|null} [options.defaultImport] - Default import name, or null.
7
+ * @param {string[]} [options.namedImports] - Named import specifiers (may include aliases like "foo as bar").
8
+ * @param {string} [options.quote] - Quote style: `"` (default) or `'`.
9
+ * @returns {string} A complete import statement string.
10
+ */
11
+ export function buildImportStatement(
12
+ source,
13
+ { defaultImport = null, namedImports = [] } = {}
14
+ ) {
15
+ let stmt = "import ";
16
+ if (defaultImport) {
17
+ stmt += defaultImport;
18
+ if (namedImports.length > 0) {
19
+ stmt += ", ";
20
+ }
21
+ }
22
+ if (namedImports.length > 0) {
23
+ stmt += `{ ${namedImports.join(", ")} }`;
24
+ }
25
+ stmt += ` from "${source}";`;
26
+ return stmt;
27
+ }
28
+
1
29
  /**
2
30
  * Fix an import declaration
3
31
  *
@@ -51,18 +79,10 @@ export function fixImport(
51
79
  ])
52
80
  );
53
81
 
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}";`;
82
+ const newImportStatement = buildImportStatement(
83
+ importDeclarationNode.source.value,
84
+ { defaultImport: finalDefaultImport, namedImports: finalNamedImports }
85
+ );
66
86
 
67
87
  // Replace the entire import declaration
68
88
  return fixer.replaceText(importDeclarationNode, newImportStatement);
package/eslint.mjs CHANGED
@@ -23,6 +23,7 @@ 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 noComputedMacros from "./eslint-rules/no-computed-macros.mjs";
26
27
  import noCurlyComponents from "./eslint-rules/no-curly-components.mjs";
27
28
  import noDiscourseComputed from "./eslint-rules/no-discourse-computed.mjs";
28
29
  import noOnclick from "./eslint-rules/no-onclick.mjs";
@@ -146,6 +147,7 @@ export default [
146
147
  "no-route-template": noRouteTemplate,
147
148
  "template-tag-no-self-this": templateTagNoSelfThis,
148
149
  "moved-packages-import-paths": movedPackagesImportPaths,
150
+ "no-computed-macros": noComputedMacros,
149
151
  "no-discourse-computed": noDiscourseComputed,
150
152
  "test-filename-suffix": testFilenameSuffix,
151
153
  "no-unnecessary-tracked": noUnnecessaryTracked,
@@ -281,14 +283,7 @@ export default [
281
283
  "",
282
284
  // Internal
283
285
  "^discourse/",
284
- "^discourse-common/",
285
286
  "^discourse-.+",
286
- "^admin/",
287
- "^wizard/",
288
- "^I18n$",
289
- "^select-kit/",
290
- "^float-kit/",
291
- "^truth-helpers/",
292
287
  // Plugins
293
288
  "^discourse/plugins/",
294
289
  // Relative
@@ -320,8 +315,9 @@ export default [
320
315
  "discourse/no-route-template": ["error"],
321
316
  "discourse/moved-packages-import-paths": ["error"],
322
317
  "discourse/test-filename-suffix": ["error"],
323
- "discourse/keep-array-sorted": ["error"],
318
+ "discourse/no-computed-macros": ["error"],
324
319
  "discourse/no-discourse-computed": ["error"],
320
+ "discourse/keep-array-sorted": ["error"],
325
321
  "discourse/no-unnecessary-tracked": ["warn"],
326
322
  "discourse/migrate-tracked-built-ins-to-ember-collections": ["error"],
327
323
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourse/lint-configs",
3
- "version": "2.44.0",
3
+ "version": "2.45.0",
4
4
  "description": "Shareable lint configs for Discourse core, plugins, and themes",
5
5
  "author": "Discourse",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "globals": "^17.4.0",
46
46
  "prettier": "^3.8.1",
47
47
  "prettier-plugin-ember-template-tag": "^2.1.3",
48
- "stylelint": "^17.4.0",
48
+ "stylelint": "^17.5.0",
49
49
  "stylelint-config-standard": "^40.0.0",
50
50
  "stylelint-config-standard-scss": "^17.0.0",
51
51
  "stylelint-scss": "^7.0.0",
@@ -55,7 +55,7 @@
55
55
  "ember-template-lint": "7.9.3",
56
56
  "eslint": "9.39.2",
57
57
  "prettier": "3.8.1",
58
- "stylelint": "17.4.0"
58
+ "stylelint": "17.5.0"
59
59
  },
60
60
  "scripts": {
61
61
  "lint": "eslint --no-error-on-unmatched-pattern \"**/*.{cjs,mjs,js}\" && pnpm prettier --check \"**/*.{cjs,mjs,js}\"",
package/stylelint.mjs CHANGED
@@ -33,6 +33,12 @@ export default {
33
33
  true,
34
34
  { ignoreKeywords: ["break-word"] },
35
35
  ],
36
+ "value-keyword-case": [
37
+ "lower",
38
+ {
39
+ ignoreProperties: ["/^\\$/", "font", "font-family"],
40
+ },
41
+ ],
36
42
 
37
43
  "discourse/no-breakpoint-mixin": true,
38
44
  },