@discourse/lint-configs 2.44.1 → 2.46.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,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
+ };
@@ -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";
@@ -36,6 +37,7 @@ import templateTagNoSelfThis from "./eslint-rules/template-tag-no-self-this.mjs"
36
37
  import testFilenameSuffix from "./eslint-rules/test-filename-suffix.mjs";
37
38
  import themeImports from "./eslint-rules/theme-imports.mjs";
38
39
  import truthHelpersImports from "./eslint-rules/truth-helpers-imports.mjs";
40
+ import uiKitImports from "./eslint-rules/ui-kit-imports.mjs";
39
41
 
40
42
  let decoratorsPluginPath = import.meta
41
43
  .resolve("@babel/plugin-proposal-decorators")
@@ -146,11 +148,13 @@ export default [
146
148
  "no-route-template": noRouteTemplate,
147
149
  "template-tag-no-self-this": templateTagNoSelfThis,
148
150
  "moved-packages-import-paths": movedPackagesImportPaths,
151
+ "no-computed-macros": noComputedMacros,
149
152
  "no-discourse-computed": noDiscourseComputed,
150
153
  "test-filename-suffix": testFilenameSuffix,
151
154
  "no-unnecessary-tracked": noUnnecessaryTracked,
152
155
  "migrate-tracked-built-ins-to-ember-collections":
153
156
  migrateTrackedBuiltInsToEmberCollections,
157
+ "ui-kit-imports": uiKitImports,
154
158
  },
155
159
  },
156
160
  },
@@ -313,10 +317,12 @@ export default [
313
317
  "discourse/no-route-template": ["error"],
314
318
  "discourse/moved-packages-import-paths": ["error"],
315
319
  "discourse/test-filename-suffix": ["error"],
316
- "discourse/keep-array-sorted": ["error"],
320
+ "discourse/no-computed-macros": ["error"],
317
321
  "discourse/no-discourse-computed": ["error"],
322
+ "discourse/keep-array-sorted": ["error"],
318
323
  "discourse/no-unnecessary-tracked": ["warn"],
319
324
  "discourse/migrate-tracked-built-ins-to-ember-collections": ["error"],
325
+ "discourse/ui-kit-imports": ["error"],
320
326
  },
321
327
  },
322
328
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discourse/lint-configs",
3
- "version": "2.44.1",
3
+ "version": "2.46.0",
4
4
  "description": "Shareable lint configs for Discourse core, plugins, and themes",
5
5
  "author": "Discourse",
6
6
  "license": "MIT",