@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.
- package/eslint-rules/i18n-import-location.mjs +9 -9
- package/eslint-rules/no-computed-macros/computed-macros-analysis.mjs +903 -0
- package/eslint-rules/no-computed-macros/computed-macros-fixer.mjs +612 -0
- package/eslint-rules/no-computed-macros/macro-transforms.mjs +645 -0
- package/eslint-rules/no-computed-macros.mjs +436 -0
- package/eslint-rules/no-discourse-computed.mjs +21 -21
- package/eslint-rules/no-unnecessary-tracked.mjs +10 -2
- package/eslint-rules/truth-helpers-imports.mjs +9 -5
- package/eslint-rules/ui-kit-imports.mjs +543 -0
- package/eslint-rules/utils/fix-import.mjs +32 -12
- package/eslint.mjs +7 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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/
|
|
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
|
{
|