@arcgis/eslint-config 4.33.0-next.16 → 4.33.0-next.160

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.
Files changed (81) hide show
  1. package/dist/config/applications.d.ts +5 -0
  2. package/dist/config/applications.js +40 -0
  3. package/dist/config/extra.js +2 -3
  4. package/dist/config/index.d.ts +2 -19
  5. package/dist/config/index.js +52 -123
  6. package/dist/config/lumina.d.ts +1 -5
  7. package/dist/config/lumina.js +17 -20
  8. package/dist/config/storybook.d.ts +1 -1
  9. package/dist/makePlugin-DEVY1iGf.js +38 -0
  10. package/dist/plugins/lumina/index.d.ts +1 -2
  11. package/dist/plugins/lumina/index.js +2242 -5
  12. package/dist/plugins/lumina/plugin.d.ts +8 -0
  13. package/dist/plugins/lumina/rules/add-missing-jsx-import.d.ts +2 -1
  14. package/dist/plugins/lumina/rules/auto-add-type.d.ts +3 -2
  15. package/dist/plugins/lumina/rules/ban-events.d.ts +6 -0
  16. package/dist/plugins/lumina/rules/component-placement-rules.d.ts +2 -1
  17. package/dist/plugins/lumina/rules/consistent-event-naming.d.ts +2 -2
  18. package/dist/plugins/lumina/rules/consistent-nullability.d.ts +2 -0
  19. package/dist/plugins/lumina/rules/decorators-context.d.ts +2 -1
  20. package/dist/plugins/lumina/rules/explicit-setter-type.d.ts +19 -0
  21. package/dist/plugins/lumina/rules/member-ordering/build.d.ts +3 -3
  22. package/dist/plugins/lumina/rules/member-ordering/comments.d.ts +2 -2
  23. package/dist/plugins/lumina/rules/member-ordering/config.d.ts +1 -1
  24. package/dist/plugins/lumina/rules/member-ordering/normalize.d.ts +2 -2
  25. package/dist/plugins/lumina/rules/member-ordering.d.ts +2 -1
  26. package/dist/plugins/lumina/rules/no-create-element-component.d.ts +2 -0
  27. package/dist/plugins/lumina/rules/no-ignore-jsdoc-tag.d.ts +2 -1
  28. package/dist/plugins/lumina/rules/no-incorrect-dynamic-tag-name.d.ts +3 -2
  29. package/dist/plugins/lumina/rules/no-inline-arrow-in-ref.d.ts +2 -1
  30. package/dist/plugins/lumina/rules/no-invalid-directives-prop.d.ts +2 -1
  31. package/dist/plugins/lumina/rules/no-jsx-spread.d.ts +2 -1
  32. package/dist/plugins/lumina/rules/no-listen-in-connected-callback.d.ts +2 -1
  33. package/dist/plugins/lumina/rules/no-non-component-exports.d.ts +2 -1
  34. package/dist/plugins/lumina/rules/no-property-name-start-with-on.d.ts +2 -1
  35. package/dist/plugins/lumina/rules/no-render-false.d.ts +3 -2
  36. package/dist/plugins/lumina/rules/no-unnecessary-assertion-on-event.d.ts +3 -0
  37. package/dist/plugins/lumina/rules/no-unnecessary-attribute-name.d.ts +2 -1
  38. package/dist/plugins/lumina/rules/no-unnecessary-bind-this.d.ts +2 -1
  39. package/dist/plugins/lumina/rules/no-unnecessary-key.d.ts +2 -1
  40. package/dist/plugins/lumina/rules/tag-name-rules.d.ts +2 -2
  41. package/dist/plugins/lumina/utils/checker.d.ts +3 -1
  42. package/dist/plugins/lumina/utils/estree.d.ts +2 -1
  43. package/dist/plugins/lumina/utils/tags.d.ts +14 -0
  44. package/dist/plugins/utils/makePlugin.d.ts +12 -13
  45. package/dist/plugins/webgis/index.d.ts +1 -2
  46. package/dist/plugins/webgis/index.js +180 -5
  47. package/dist/plugins/webgis/plugin.d.ts +8 -0
  48. package/dist/plugins/webgis/rules/no-dts-files.d.ts +2 -0
  49. package/dist/plugins/webgis/rules/no-import-outside-src.d.ts +2 -1
  50. package/dist/plugins/webgis/rules/no-touching-jsdoc.d.ts +2 -1
  51. package/dist/plugins/webgis/rules/require-js-in-imports.d.ts +2 -0
  52. package/package.json +9 -10
  53. package/dist/chunk-2ND3FRSX.js +0 -160
  54. package/dist/chunk-C76ANB4Z.js +0 -26
  55. package/dist/chunk-PVNFCQDL.js +0 -1975
  56. package/dist/plugins/lumina/rules/add-missing-jsx-import.test.d.ts +0 -1
  57. package/dist/plugins/lumina/rules/auto-add-type.test.d.ts +0 -1
  58. package/dist/plugins/lumina/rules/component-placement-rules.test.d.ts +0 -1
  59. package/dist/plugins/lumina/rules/consistent-event-naming.test.d.ts +0 -1
  60. package/dist/plugins/lumina/rules/decorators-context.test.d.ts +0 -1
  61. package/dist/plugins/lumina/rules/member-ordering.test.d.ts +0 -1
  62. package/dist/plugins/lumina/rules/no-ignore-jsdoc-tag.test.d.ts +0 -1
  63. package/dist/plugins/lumina/rules/no-incorrect-dynamic-tag-name.test.d.ts +0 -1
  64. package/dist/plugins/lumina/rules/no-inline-arrow-in-ref.test.d.ts +0 -1
  65. package/dist/plugins/lumina/rules/no-invalid-directives-prop.test.d.ts +0 -1
  66. package/dist/plugins/lumina/rules/no-jsx-spread.test.d.ts +0 -1
  67. package/dist/plugins/lumina/rules/no-listen-in-connected-callback.test.d.ts +0 -1
  68. package/dist/plugins/lumina/rules/no-non-component-exports.test.d.ts +0 -1
  69. package/dist/plugins/lumina/rules/no-property-name-start-with-on.test.d.ts +0 -1
  70. package/dist/plugins/lumina/rules/no-render-false.test.d.ts +0 -1
  71. package/dist/plugins/lumina/rules/no-unnecessary-attribute-name.test.d.ts +0 -1
  72. package/dist/plugins/lumina/rules/no-unnecessary-bind-this.test.d.ts +0 -1
  73. package/dist/plugins/lumina/rules/no-unnecessary-key.test.d.ts +0 -1
  74. package/dist/plugins/lumina/rules/tag-name-rules.spec.d.ts +0 -1
  75. package/dist/plugins/lumina/utils/creator.d.ts +0 -3
  76. package/dist/plugins/utils/tests.d.ts +0 -26
  77. package/dist/plugins/webgis/rules/no-import-outside-src.test.d.ts +0 -1
  78. package/dist/plugins/webgis/rules/no-touching-jsdoc.test.d.ts +0 -1
  79. package/dist/plugins/webgis/rules/require-js-in-core-import.d.ts +0 -1
  80. package/dist/plugins/webgis/rules/require-js-in-core-import.test.d.ts +0 -1
  81. package/dist/plugins/webgis/utils/creator.d.ts +0 -3
@@ -1,7 +1,2244 @@
1
- import {
2
- lumina_default
3
- } from "../../chunk-PVNFCQDL.js";
4
- import "../../chunk-C76ANB4Z.js";
1
+ import { m as makeEslintPlugin } from "../../makePlugin-DEVY1iGf.js";
2
+ import { AST_NODE_TYPES, ESLintUtils, AST_TOKEN_TYPES } from "@typescript-eslint/utils";
3
+ import ts from "typescript";
4
+ import { camelToKebab } from "@arcgis/components-utils";
5
+ const plugin = makeEslintPlugin(
6
+ "lumina",
7
+ (rule) => `https://devtopia.esri.com/WebGIS/arcgis-web-components/tree/main/packages/support-packages/eslint-config/src/plugins/lumina/rules/${rule}.ts`
8
+ );
9
+ const unwrapExpression = (expression) => expression.type === AST_NODE_TYPES.TSAsExpression || expression.type === AST_NODE_TYPES.TSNonNullExpression || expression.type === AST_NODE_TYPES.TSSatisfiesExpression ? unwrapExpression(expression.expression) : expression;
10
+ const luminaEntrypointName = "@arcgis/lumina";
11
+ const luminaTestEntrypointName = "@arcgis/lumina-compiler/testing";
12
+ const luminaJsxExportName = "h";
13
+ function checkForLuminaJsx() {
14
+ const ImportDeclaration = (node) => {
15
+ if (node.source.value !== luminaEntrypointName) {
16
+ return;
17
+ }
18
+ for (const specifier of node.specifiers) {
19
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name === luminaJsxExportName || specifier.imported.type === AST_NODE_TYPES.Literal && specifier.imported.value === luminaJsxExportName)) {
20
+ withProperty.isLuminaJsx = true;
21
+ return;
22
+ }
23
+ }
24
+ };
25
+ const withProperty = ImportDeclaration;
26
+ return withProperty;
27
+ }
28
+ function hasDecorator(node, decoratorName) {
29
+ return node.decorators.some(
30
+ (decorator) => decorator.expression.type === AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === AST_NODE_TYPES.Identifier && decorator.expression.callee.name === decoratorName
31
+ ) ?? false;
32
+ }
33
+ function extractDeclareElementsInterface(node) {
34
+ return node.kind === "global" ? node.body.body.find(
35
+ (node2) => node2.type === AST_NODE_TYPES.TSInterfaceDeclaration && node2.id.name === "DeclareElements"
36
+ ) : void 0;
37
+ }
38
+ function isCreateEvent(node) {
39
+ return node.value?.type === AST_NODE_TYPES.CallExpression && node.value.callee.type === AST_NODE_TYPES.Identifier && node.value.callee.name === "createEvent" && !node.static;
40
+ }
41
+ const getProperty = (properties, name) => properties?.find(
42
+ (option) => option.type === AST_NODE_TYPES.Property && option.key.type === AST_NODE_TYPES.Identifier && option.key.name === name
43
+ )?.value;
44
+ function isGetterWithoutSetter(node) {
45
+ const isGetter = node.type === AST_NODE_TYPES.MethodDefinition && node.kind === "get";
46
+ if (!isGetter) {
47
+ return false;
48
+ }
49
+ const index = node.parent.body.indexOf(node);
50
+ const previousNode = node.parent.body.at(index - 1);
51
+ const nextNode = node.parent.body.at(index + 1);
52
+ const name = getName(node);
53
+ if (name === void 0) {
54
+ return false;
55
+ }
56
+ const previousIsSetter = previousNode?.type === AST_NODE_TYPES.MethodDefinition && previousNode.kind === "set" && getName(previousNode) === name;
57
+ if (previousIsSetter) {
58
+ return false;
59
+ }
60
+ const nextIsSetter = nextNode?.type === AST_NODE_TYPES.MethodDefinition && nextNode.kind === "set" && getName(nextNode) === name;
61
+ return !nextIsSetter;
62
+ }
63
+ function getName(node) {
64
+ if (node.key.type === AST_NODE_TYPES.Identifier) {
65
+ return node.key.name;
66
+ } else if (node.key.type === AST_NODE_TYPES.Literal && typeof node.key.value === "string") {
67
+ return node.key.value;
68
+ } else {
69
+ return void 0;
70
+ }
71
+ }
72
+ function parsePropertyDecorator(decorator) {
73
+ const isPropertyDecorator = decorator.expression.type === AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === AST_NODE_TYPES.Identifier && decorator.expression.callee.name === "property";
74
+ if (!isPropertyDecorator) {
75
+ return;
76
+ }
77
+ const callExpression = decorator?.expression.type === AST_NODE_TYPES.CallExpression ? decorator.expression : void 0;
78
+ if (callExpression === void 0) {
79
+ return;
80
+ }
81
+ const options = callExpression.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression ? callExpression.arguments[0] : void 0;
82
+ const properties = options?.properties;
83
+ return {
84
+ callExpression,
85
+ options,
86
+ properties
87
+ };
88
+ }
89
+ function isBindThisCallee(callee) {
90
+ return callee.type === AST_NODE_TYPES.MemberExpression && // expression.identifier(this)
91
+ callee.property.type === AST_NODE_TYPES.Identifier && // expression.bind(this)
92
+ callee.property.name === "bind" && // expression.expression.bind(this)
93
+ callee.object.type === AST_NODE_TYPES.MemberExpression && // expression.identifier.bind(this)
94
+ callee.object.property.type === AST_NODE_TYPES.Identifier && // this.identifier.bind(this)
95
+ callee.object.object.type === AST_NODE_TYPES.ThisExpression;
96
+ }
97
+ const importDeclaration = `import { ${luminaJsxExportName} } from "${luminaEntrypointName}";`;
98
+ const description$k = `To use Lumina's JSX, you need to ${importDeclaration}`;
99
+ plugin.createRule({
100
+ name: "add-missing-jsx-import",
101
+ meta: {
102
+ docs: {
103
+ description: description$k,
104
+ defaultLevel: "error"
105
+ },
106
+ messages: {
107
+ addMissingJsxImport: description$k
108
+ },
109
+ type: "problem",
110
+ schema: [],
111
+ fixable: "code"
112
+ },
113
+ defaultOptions: [],
114
+ create(context) {
115
+ let errorAlreadyReported = false;
116
+ let isUsingLumina = false;
117
+ let lastImportDeclaration;
118
+ let lastLuminaImportClause;
119
+ let hasLuminaJsxImport = false;
120
+ return {
121
+ ImportDeclaration(node) {
122
+ lastImportDeclaration = node;
123
+ if (node.source.value === luminaTestEntrypointName) {
124
+ isUsingLumina = true;
125
+ return;
126
+ }
127
+ if (node.source.value !== luminaEntrypointName) {
128
+ return;
129
+ }
130
+ isUsingLumina = true;
131
+ const isTypeOnly = node.importKind === "type";
132
+ for (const specifier of node.specifiers) {
133
+ if (!isTypeOnly) {
134
+ lastLuminaImportClause = specifier;
135
+ }
136
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name === luminaJsxExportName || specifier.imported.type === AST_NODE_TYPES.Literal && specifier.imported.value === luminaJsxExportName)) {
137
+ hasLuminaJsxImport = true;
138
+ return;
139
+ }
140
+ }
141
+ },
142
+ JSXIdentifier(node) {
143
+ if (isUsingLumina && !hasLuminaJsxImport && !errorAlreadyReported) {
144
+ errorAlreadyReported = true;
145
+ context.report({
146
+ messageId: "addMissingJsxImport",
147
+ node,
148
+ fix(fixer) {
149
+ if (lastLuminaImportClause !== void 0) {
150
+ return fixer.insertTextAfter(lastLuminaImportClause, `, ${luminaJsxExportName}`);
151
+ }
152
+ if (lastImportDeclaration !== void 0) {
153
+ return fixer.insertTextAfter(lastImportDeclaration, importDeclaration);
154
+ } else {
155
+ return fixer.insertTextBefore(context.sourceCode.ast, importDeclaration);
156
+ }
157
+ }
158
+ });
159
+ }
160
+ }
161
+ };
162
+ }
163
+ });
164
+ const description$j = "Auto add { type: Boolean } or { type: Number } where necessary";
165
+ plugin.createRule({
166
+ name: "auto-add-type",
167
+ meta: {
168
+ docs: {
169
+ description: description$j,
170
+ defaultLevel: "warn"
171
+ },
172
+ messages: {
173
+ addType: `This property is of {{ type }} type, yet the type is not trivially inferrable from the AST without type-checking. Such properties require a { type: {{ type }} } annotation.
174
+
175
+ More information: https://devtopia.esri.com/WebGIS/arcgis-web-components/issues/1991`,
176
+ typeAnnotationMismatchesActualType: "The @property({type: {{type}} }) doesn't match the actual property type ({{actualType}}).",
177
+ noEmptyPropertyObject: "Replace @property({}) with @property()",
178
+ noUnnecessaryType: "Property type is trivially inferrable without type-checking - remove needless { type: {{ type }} } annotation.",
179
+ unhandledType: `The default Lit attribute<-->property converter does not define any behavior for @property({ type: {{type}} }). Consider removing this type annotation, or adding a custom converter @property({ converter: ... }) to handle this type`
180
+ },
181
+ type: "problem",
182
+ schema: [],
183
+ fixable: "code"
184
+ },
185
+ defaultOptions: [],
186
+ create(context) {
187
+ const services = ESLintUtils.getParserServices(context);
188
+ return {
189
+ Decorator(decorator) {
190
+ const part = parsePropertyDecorator(decorator);
191
+ if (part === void 0) {
192
+ return;
193
+ }
194
+ const { options, properties, callExpression } = part;
195
+ const property = decorator.parent;
196
+ if (property?.type !== AST_NODE_TYPES.MethodDefinition && property?.type !== AST_NODE_TYPES.PropertyDefinition) {
197
+ return;
198
+ }
199
+ const trivialType = inferTrivialType(property);
200
+ const typeProperty = getProperty(properties, "type");
201
+ const converterProperty = getProperty(properties, "converter");
202
+ const isTrivialType = trivialType === "Number" || trivialType === "Boolean";
203
+ if (isTrivialType && typeProperty !== void 0 && typeProperty.type === AST_NODE_TYPES.Identifier && (typeProperty.name === "Number" || typeProperty.name === "Boolean")) {
204
+ context.report({
205
+ node: typeProperty,
206
+ messageId: "noUnnecessaryType",
207
+ data: {
208
+ type: typeProperty.name
209
+ },
210
+ fix(fixer) {
211
+ const nextToken = context.sourceCode.getTokenAfter(typeProperty.parent);
212
+ const isTrailingComma = nextToken && nextToken.value === ",";
213
+ return isTrailingComma ? fixer.removeRange([typeProperty.parent.range[0], nextToken.range[1]]) : fixer.remove(typeProperty.parent);
214
+ }
215
+ });
216
+ return;
217
+ }
218
+ if (
219
+ // Do not emit any warnings if has "converter" as we don't know what the converter does
220
+ converterProperty === void 0 && typeProperty?.type === AST_NODE_TYPES.Identifier && !builtInConverterTypes.has(typeProperty.name)
221
+ ) {
222
+ context.report({
223
+ node: typeProperty,
224
+ messageId: "unhandledType",
225
+ data: {
226
+ type: typeProperty.name
227
+ }
228
+ });
229
+ return;
230
+ }
231
+ if (isTrivialType) {
232
+ return;
233
+ }
234
+ if (options !== void 0 && properties?.length === 0) {
235
+ context.report({
236
+ node: options,
237
+ messageId: "noEmptyPropertyObject",
238
+ fix(fixer) {
239
+ return fixer.replaceText(
240
+ options,
241
+ context.sourceCode.getCommentsInside(options).map((comment) => context.sourceCode.getText(comment)).join("\n")
242
+ );
243
+ }
244
+ });
245
+ return;
246
+ }
247
+ const attributeOption = getProperty(properties, "attribute");
248
+ const isAttributeFalse = attributeOption?.type === AST_NODE_TYPES.Literal && attributeOption.value === false;
249
+ if (isAttributeFalse) {
250
+ return;
251
+ }
252
+ const type = services.getTypeAtLocation(property);
253
+ const typeFlags = (type.flags & ts.TypeFlags.Union ? type.types : [type]).reduce(
254
+ (flags, type2) => flags | type2.flags,
255
+ 0
256
+ );
257
+ const isTypeCastable = (typeFlags & (ts.TypeFlags.String | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0;
258
+ const isNumberType = isTypeCastable && typeFlags & ts.TypeFlags.NumberLike;
259
+ const isBooleanType = isTypeCastable && typeFlags & ts.TypeFlags.BooleanLike;
260
+ if (isNumberType && isBooleanType) {
261
+ return;
262
+ }
263
+ if (!isNumberType && !isBooleanType) {
264
+ return;
265
+ }
266
+ if (typeProperty !== void 0) {
267
+ if (
268
+ // Do not emit any warnings if has "converter" as we don't know what the converter does
269
+ converterProperty === void 0 && typeProperty.type === AST_NODE_TYPES.Identifier && (typeProperty.name === "Number" && isBooleanType || typeProperty.name === "Boolean" && isNumberType)
270
+ ) {
271
+ context.report({
272
+ node: typeProperty,
273
+ messageId: "typeAnnotationMismatchesActualType",
274
+ data: {
275
+ type: typeProperty.name,
276
+ actualType: isNumberType ? "Number" : "Boolean"
277
+ }
278
+ });
279
+ }
280
+ return;
281
+ }
282
+ const comments = context.sourceCode.getCommentsBefore(property);
283
+ const isDocsOnlyReadOnly = comments.some((comment) => comment.value.includes("@readonly"));
284
+ const readOnlyProperty = getProperty(properties, "readOnly");
285
+ const hasReadOnlyFlag = readOnlyProperty?.type === AST_NODE_TYPES.Literal && readOnlyProperty.value === true;
286
+ const isReadOnly = isDocsOnlyReadOnly || hasReadOnlyFlag || isGetterWithoutSetter(property);
287
+ const reflectsProperty = getProperty(properties, "reflects");
288
+ if (isReadOnly && (!isBooleanType || reflectsProperty?.type !== AST_NODE_TYPES.Literal || reflectsProperty.value !== true)) {
289
+ return;
290
+ }
291
+ context.report({
292
+ messageId: "addType",
293
+ data: {
294
+ type: isNumberType ? "Number" : "Boolean"
295
+ },
296
+ node: property,
297
+ fix(fixer) {
298
+ const sourceCode = context.sourceCode;
299
+ if (options === void 0) {
300
+ const token = sourceCode.getTokenAfter(callExpression.callee, {
301
+ filter: (token2) => token2.value === "("
302
+ });
303
+ return fixer.insertTextAfterRange(token.range, `{ type: ${isNumberType ? "Number" : "Boolean"} }`);
304
+ } else {
305
+ return fixer.insertTextBefore(options.properties[0], `type: ${isNumberType ? "Number" : "Boolean"}, `);
306
+ }
307
+ }
308
+ });
309
+ }
310
+ };
311
+ }
312
+ });
313
+ function inferTrivialType(member) {
314
+ const type = member.type === AST_NODE_TYPES.PropertyDefinition ? member.typeAnnotation?.typeAnnotation : member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "set" ? member.value.params[0]?.typeAnnotation?.typeAnnotation : member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "get" ? member.value.returnType?.typeAnnotation : void 0;
315
+ if (type === void 0) {
316
+ if (member.value?.type === AST_NODE_TYPES.Literal && typeof member.value.value === "number" || member.value?.type === AST_NODE_TYPES.UnaryExpression && member.value.argument.type === AST_NODE_TYPES.Literal && typeof member.value.argument.value === "number" && member.value.operator === "-") {
317
+ return "Number";
318
+ }
319
+ if (member.value?.type === AST_NODE_TYPES.Literal && typeof member.value.value === "boolean") {
320
+ return "Boolean";
321
+ }
322
+ } else if (type.type === AST_NODE_TYPES.TSNumberKeyword) {
323
+ return "Number";
324
+ } else if (type.type === AST_NODE_TYPES.TSBooleanKeyword) {
325
+ return "Boolean";
326
+ }
327
+ return "Other";
328
+ }
329
+ const builtInConverterTypes = /* @__PURE__ */ new Set(["Number", "Boolean", "Array", "Object"]);
330
+ plugin.createRule({
331
+ name: "ban-events",
332
+ defaultOptions: [],
333
+ meta: {
334
+ docs: {
335
+ description: "This rule helps ban or warn against listened event types",
336
+ defaultLevel: "off"
337
+ },
338
+ messages: {
339
+ default: "{{message}}"
340
+ },
341
+ schema: {
342
+ type: "array",
343
+ items: {
344
+ anyOf: [
345
+ { type: "string" },
346
+ {
347
+ type: "object",
348
+ properties: {
349
+ event: { type: "string" },
350
+ message: {
351
+ type: "string",
352
+ minLength: 1
353
+ }
354
+ },
355
+ additionalProperties: false,
356
+ required: ["event"]
357
+ }
358
+ ]
359
+ },
360
+ uniqueItems: true
361
+ },
362
+ type: "problem"
363
+ },
364
+ create(context) {
365
+ const bannedEventToMessageLookup = /* @__PURE__ */ new Map();
366
+ context.options.forEach((option) => {
367
+ const event = typeof option === "string" ? option : option.event;
368
+ const message = typeof option === "string" ? null : option.message ?? null;
369
+ bannedEventToMessageLookup.set(event, message);
370
+ });
371
+ function buildMessage(eventName) {
372
+ return bannedEventToMessageLookup.get(eventName) ?? `${eventName} is not allowed`;
373
+ }
374
+ function checkEvent(node, eventName) {
375
+ if (bannedEventToMessageLookup.has(eventName)) {
376
+ context.report({
377
+ node,
378
+ messageId: "default",
379
+ data: {
380
+ message: buildMessage(eventName)
381
+ }
382
+ });
383
+ }
384
+ }
385
+ const luminaJsxCheck = checkForLuminaJsx();
386
+ return {
387
+ "ImportDeclaration": luminaJsxCheck,
388
+ "CallExpression:matches([callee.property.name=addEventListener], [callee.property.name=removeEventListener]), CallExpression[callee.object.type=ThisExpression][callee.property.name=listen]"(node) {
389
+ if (!luminaJsxCheck.isLuminaJsx) {
390
+ return;
391
+ }
392
+ const eventName = node.arguments[0].value;
393
+ checkEvent(node, eventName);
394
+ },
395
+ "CallExpression[callee.object.type=ThisExpression][callee.property.name=listenOn]"(node) {
396
+ if (!luminaJsxCheck.isLuminaJsx) {
397
+ return;
398
+ }
399
+ const eventName = node.arguments[1].value;
400
+ checkEvent(node, eventName);
401
+ }
402
+ };
403
+ }
404
+ });
405
+ const description$i = `Lumina component must be declared in a TSX file with a matching folder name located inside of src/components folder.`;
406
+ plugin.createRule({
407
+ name: "component-placement-rules",
408
+ meta: {
409
+ docs: {
410
+ description: description$i,
411
+ defaultLevel: "error"
412
+ },
413
+ messages: {
414
+ fileFolderNameMismatch: "Lumina component must be declared in a file whose name (without extension) matches the parent folder name.\n\nCreating components in nested folders is supported as long as the file name matches the immediate parent folder name.",
415
+ extensionNotTsx: "Lumina component must be declared in a .tsx file.",
416
+ noComponentOutsideSrcComponents: "All lumina components must be declared within the src/components folder. Inside that folder, you can create sub-folders for structuring component files."
417
+ },
418
+ type: "problem",
419
+ schema: [],
420
+ fixable: "code"
421
+ },
422
+ defaultOptions: [],
423
+ create(context) {
424
+ return {
425
+ TSModuleDeclaration(node) {
426
+ const luminaDeclarationInterface = extractDeclareElementsInterface(node);
427
+ if (luminaDeclarationInterface === void 0) {
428
+ return;
429
+ }
430
+ const filePath = context.filename.replaceAll("\\", "/");
431
+ const containsSrcComponents = filePath.includes("/src/components/");
432
+ if (!containsSrcComponents) {
433
+ context.report({ messageId: "noComponentOutsideSrcComponents", node: luminaDeclarationInterface });
434
+ }
435
+ const isFileExtensionTsx = filePath.endsWith(".tsx");
436
+ if (!isFileExtensionTsx) {
437
+ context.report({ messageId: "extensionNotTsx", node: luminaDeclarationInterface });
438
+ return;
439
+ }
440
+ const [fileName, folderName] = filePath.split("/").reverse();
441
+ const extensionlessFileName = fileName.slice(0, -".tsx".length);
442
+ if (folderName !== extensionlessFileName) {
443
+ context.report({ messageId: "fileFolderNameMismatch", node: luminaDeclarationInterface });
444
+ }
445
+ }
446
+ };
447
+ }
448
+ });
449
+ const description$h = `Enforce consistent event naming.`;
450
+ const defaultOptions$1 = [
451
+ {
452
+ eventNamespaces: ["arcgis"],
453
+ includeComponentNameInEventName: false
454
+ }
455
+ ];
456
+ plugin.createRule({
457
+ name: "consistent-event-naming",
458
+ meta: {
459
+ docs: {
460
+ description: description$h,
461
+ defaultLevel: "warn"
462
+ },
463
+ messages: {
464
+ eventNamespaceError: `Custom event name must start with one of the following prefixes: {{ prefixes }}.
465
+
466
+ Details: https://qawebgis.esri.com/components/lumina/events#best-practices-around-emitting-events`,
467
+ componentNameInEventError: `For consistency, event name should not start with component name.
468
+
469
+ Discussion: https://devtopia.esri.com/WebGIS/arcgis-web-components/discussions/307`,
470
+ noComponentNameInEventError: `For consistency, event name should include component name.`,
471
+ missingPrivateJsDocTag: `Internal and private events must be marked with @internal or @private JSDoc tag.`
472
+ },
473
+ type: "problem",
474
+ schema: [
475
+ {
476
+ type: "object",
477
+ properties: {
478
+ eventNamespaces: {
479
+ type: "array",
480
+ items: {
481
+ type: "string"
482
+ }
483
+ },
484
+ includeComponentNameInEventName: {
485
+ type: "boolean"
486
+ }
487
+ },
488
+ additionalProperties: false
489
+ }
490
+ ]
491
+ },
492
+ defaultOptions: defaultOptions$1,
493
+ create(context, options) {
494
+ const eventNamespaces = options[0].eventNamespaces;
495
+ const includeComponentNameInEventName = options[0].includeComponentNameInEventName;
496
+ return {
497
+ PropertyDefinition(node) {
498
+ const isLuminaEvent = isCreateEvent(node);
499
+ if (!isLuminaEvent) {
500
+ return;
501
+ }
502
+ const comments = context.sourceCode.getCommentsBefore(node);
503
+ const isDeprecated = comments.some((comment) => comment.value.includes("@deprecated"));
504
+ if (isDeprecated) {
505
+ return;
506
+ }
507
+ const componentName = context.sourceCode.getAncestors(node).find((ancestor) => ancestor.type === AST_NODE_TYPES.ClassDeclaration)?.id?.name;
508
+ if (componentName === void 0) {
509
+ return;
510
+ }
511
+ const propertyName = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : node.key.type === AST_NODE_TYPES.Literal ? node.key.value : void 0;
512
+ if (typeof propertyName !== "string") {
513
+ return;
514
+ }
515
+ let eventName = propertyName;
516
+ if (eventNamespaces.length > 0) {
517
+ const currentNamespace = eventNamespaces.find((namespace) => eventName.startsWith(namespace));
518
+ if (currentNamespace === void 0) {
519
+ context.report({
520
+ messageId: "eventNamespaceError",
521
+ data: {
522
+ prefixes: eventNamespaces.join(", ")
523
+ },
524
+ node: node.key
525
+ });
526
+ return;
527
+ }
528
+ eventName = eventName.slice(currentNamespace.length);
529
+ }
530
+ const startsWithInternal = eventName.toLowerCase().startsWith("internal");
531
+ if (startsWithInternal || eventName.toLowerCase().startsWith("private")) {
532
+ const hasInternalJsDocTag = comments.some((comment) => comment.value.includes("@internal"));
533
+ const hasPrivateJsDocTag = comments.some((comment) => comment.value.includes("@private"));
534
+ if (!hasInternalJsDocTag && !hasPrivateJsDocTag) {
535
+ context.report({
536
+ messageId: "missingPrivateJsDocTag",
537
+ node: node.key
538
+ });
539
+ }
540
+ eventName = startsWithInternal ? eventName.slice("internal".length) : eventName.slice("private".length);
541
+ }
542
+ const capitalIndex = capitalAfterLower.exec(componentName)?.index;
543
+ const unNamespacedComponentName = capitalIndex === void 0 ? void 0 : componentName.slice(capitalIndex) || void 0;
544
+ const includesComponentName = eventName.startsWith(componentName) || unNamespacedComponentName !== void 0 && eventName.startsWith(unNamespacedComponentName);
545
+ if (includeComponentNameInEventName && !includesComponentName) {
546
+ context.report({
547
+ messageId: "noComponentNameInEventError",
548
+ node: node.key
549
+ });
550
+ } else if (!includeComponentNameInEventName && includesComponentName) {
551
+ context.report({
552
+ messageId: "componentNameInEventError",
553
+ node: node.key
554
+ });
555
+ }
556
+ }
557
+ };
558
+ }
559
+ });
560
+ const capitalAfterLower = /(?<=[a-z\d])[A-Z]/u;
561
+ const description$g = `Enforce consistent usage of ? for marking property as nullable, rather than |null, |undefined or |Nil.`;
562
+ plugin.createRule({
563
+ name: "consistent-nullability",
564
+ meta: {
565
+ docs: {
566
+ description: description$g,
567
+ // TODO: enable this by default
568
+ defaultLevel: "off"
569
+ },
570
+ messages: {
571
+ consistentNullabilityError: `For consistency, use {{propertyName}}?:{{newType}} to denote property as optional, rather than {{propertyName}}:{{oldType}}.`,
572
+ removeNeedlessDefault: `This default value is likely unnecessary. Remove it to reduce bundle size.`
573
+ },
574
+ schema: [],
575
+ fixable: "code",
576
+ type: "suggestion"
577
+ },
578
+ defaultOptions: [],
579
+ create(context) {
580
+ return {
581
+ PropertyDefinition(node) {
582
+ const isProperty = hasDecorator(node, "property");
583
+ if (!isProperty) {
584
+ return;
585
+ }
586
+ const comments = context.sourceCode.getCommentsBefore(node);
587
+ const isDeprecated = comments.some((comment) => comment.value.includes("@deprecated"));
588
+ if (isDeprecated) {
589
+ return;
590
+ }
591
+ const defaultValue = node.value;
592
+ if (defaultValue?.type === AST_NODE_TYPES.Literal && defaultValue.value === null || defaultValue?.type === AST_NODE_TYPES.Identifier && defaultValue.name === "undefined") {
593
+ context.report({
594
+ node: defaultValue,
595
+ messageId: "removeNeedlessDefault",
596
+ fix(fixer) {
597
+ const tokenBeforeDefaultValue = context.sourceCode.getTokenBefore(defaultValue);
598
+ const isEqualsSign = tokenBeforeDefaultValue && tokenBeforeDefaultValue.value === "=";
599
+ return isEqualsSign ? fixer.removeRange([tokenBeforeDefaultValue.range[0], defaultValue.range[1]]) : fixer.remove(defaultValue);
600
+ }
601
+ });
602
+ }
603
+ const typeAnnotation = node.typeAnnotation?.typeAnnotation;
604
+ if (typeAnnotation === void 0) {
605
+ return;
606
+ }
607
+ const isUnionType = typeAnnotation.type === AST_NODE_TYPES.TSUnionType;
608
+ if (!isUnionType) {
609
+ return;
610
+ }
611
+ const withoutNillTypes = typeAnnotation.types.filter(
612
+ (type) => type.type !== AST_NODE_TYPES.TSUndefinedKeyword && type.type !== AST_NODE_TYPES.TSNullKeyword && (type.type !== AST_NODE_TYPES.TSTypeReference || type.typeName.type !== AST_NODE_TYPES.Identifier || type.typeName.name !== "Nil" && type.typeName.name !== "nil" && type.typeName.name !== "nullish")
613
+ );
614
+ const isSameCount = typeAnnotation.types.length === withoutNillTypes.length;
615
+ if (isSameCount || withoutNillTypes.length === 0) {
616
+ return;
617
+ }
618
+ const hasQuestionMark = node.optional;
619
+ const newType = withoutNillTypes.map((type) => context.sourceCode.getText(type)).join(" | ");
620
+ context.report({
621
+ node: typeAnnotation,
622
+ messageId: "consistentNullabilityError",
623
+ data: {
624
+ propertyName: context.sourceCode.getText(node.key),
625
+ newType,
626
+ oldType: context.sourceCode.getText(typeAnnotation)
627
+ },
628
+ fix(fixer) {
629
+ const tokenAfterKey = context.sourceCode.getTokenAfter(node.key);
630
+ const hasExclamationMark = tokenAfterKey !== null && tokenAfterKey.value === "!";
631
+ return [
632
+ ...hasExclamationMark ? [fixer.remove(tokenAfterKey)] : [],
633
+ ...hasQuestionMark ? [] : [fixer.insertTextAfter(node.key, "?")],
634
+ fixer.replaceText(typeAnnotation, newType)
635
+ ];
636
+ }
637
+ });
638
+ }
639
+ };
640
+ }
641
+ });
642
+ const description$f = `Enforce that @property(), @method() and createEvent() members are used in the correct context.`;
643
+ plugin.createRule({
644
+ name: "decorators-context",
645
+ meta: {
646
+ docs: {
647
+ description: description$f,
648
+ defaultLevel: "error"
649
+ },
650
+ messages: {
651
+ publicApiMustBePublic: `@property(), @method() and createEvent() members must not have private or protected modifier.
652
+
653
+ If you wish to hide this member from public documentation, use @private or @protected JSDoc tags instead. Documentation: https://qawebgis.esri.com/components/lumina/documenting-components#excluding-api-from-public-documentation`,
654
+ noPropertyDecoratorOnMethods: `Methods must not have @property() nor @state() decorator. Did you mean @property() instead?`,
655
+ noCombinedPropertyEvent: `Property may either be an event (initialized with createEvent()) or a property (has @property() decorator), but not both`,
656
+ noCombinedPropertyState: `Property may either be a state (initialized with @state()) or a property (has @property() decorator), but not both`,
657
+ noComputedName: `Computed property names are not allowed as they are not statically analyzable`
658
+ },
659
+ type: "problem",
660
+ schema: [],
661
+ fixable: "code"
662
+ },
663
+ defaultOptions: [],
664
+ create(context) {
665
+ return {
666
+ PropertyDefinition(node) {
667
+ const hasPropertyDecorator = hasDecorator(node, "property");
668
+ const isPrivateOrProtected = node.accessibility === "private" || node.accessibility === "protected";
669
+ if (hasPropertyDecorator && isPrivateOrProtected) {
670
+ context.report({ messageId: "publicApiMustBePublic", node });
671
+ }
672
+ const isLuminaEvent = isCreateEvent(node);
673
+ if (isLuminaEvent && isPrivateOrProtected) {
674
+ context.report({ messageId: "publicApiMustBePublic", node });
675
+ }
676
+ if (isLuminaEvent && hasPropertyDecorator) {
677
+ context.report({ messageId: "noCombinedPropertyEvent", node });
678
+ }
679
+ const hasStateDecorator = hasDecorator(node, "state");
680
+ if (hasStateDecorator && hasPropertyDecorator) {
681
+ context.report({ messageId: "noCombinedPropertyState", node });
682
+ }
683
+ if (hasPropertyDecorator && node.computed) {
684
+ context.report({ messageId: "noComputedName", node });
685
+ }
686
+ },
687
+ MethodDefinition(node) {
688
+ const hasMethodDecorator = hasDecorator(node, "method");
689
+ const hasPropertyDecorator = hasDecorator(node, "property");
690
+ const hasStateDecorator = hasDecorator(node, "state");
691
+ const isPrivateOrProtected = node.accessibility === "private" || node.accessibility === "protected";
692
+ if (hasMethodDecorator && isPrivateOrProtected) {
693
+ context.report({ messageId: "publicApiMustBePublic", node });
694
+ }
695
+ if (hasMethodDecorator && (hasPropertyDecorator || hasStateDecorator)) {
696
+ context.report({ messageId: "noPropertyDecoratorOnMethods", node });
697
+ }
698
+ if (hasStateDecorator && hasPropertyDecorator) {
699
+ context.report({ messageId: "noCombinedPropertyState", node });
700
+ }
701
+ const isRealMethod = node.kind === "method";
702
+ if (isRealMethod && (hasPropertyDecorator || hasStateDecorator)) {
703
+ context.report({ messageId: "noPropertyDecoratorOnMethods", node });
704
+ }
705
+ if ((hasPropertyDecorator || hasMethodDecorator) && node.computed) {
706
+ context.report({ messageId: "noComputedName", node });
707
+ }
708
+ }
709
+ };
710
+ }
711
+ });
712
+ const litTemplateResult = "TemplateResult";
713
+ const explicitJsxTypeName = "JsxNode";
714
+ const implicitJsxTypeName = "Element";
715
+ const implicitJsxParentName = "LuminaJsx";
716
+ function isLuminaJsxType(type) {
717
+ for (const declaration of type.aliasSymbol?.declarations ?? []) {
718
+ if (!ts.isTypeAliasDeclaration(declaration)) {
719
+ continue;
720
+ }
721
+ const typeName = declaration.name.escapedText;
722
+ if (typeName === litTemplateResult || typeName === explicitJsxTypeName) {
723
+ return true;
724
+ }
725
+ if (typeName !== implicitJsxTypeName) {
726
+ continue;
727
+ }
728
+ const grandParent = declaration.parent?.parent;
729
+ if (!ts.isModuleDeclaration(grandParent)) {
730
+ continue;
731
+ }
732
+ const symbol = grandParent.symbol;
733
+ if (symbol?.escapedName === implicitJsxParentName) {
734
+ return true;
735
+ }
736
+ }
737
+ return false;
738
+ }
739
+ const hasTypeFlag = (type, flag) => type.flags & flag ? true : (type.flags & ts.TypeFlags.Union) !== 0 && type.types.some((t) => hasTypeFlag(t, flag));
740
+ const literalTypeFlag = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike;
741
+ const description$e = `Need to add explicit type annotation: {{ setterType }}
742
+
743
+ Explanation:
744
+ Lumina automatically creates an attribute for a property if property type includes a literal type (string|number|boolean).
745
+ For bound properties, the property type by default comes from the bound getter type.
746
+ The property you are trying to bind includes a literal in the setter type, but not the getter type.
747
+ An explicit type annotation is required to ensure the property has an attribute created.`;
748
+ plugin.createRule({
749
+ name: "explicit-setter-type",
750
+ meta: {
751
+ docs: {
752
+ description: description$e,
753
+ defaultLevel: "error"
754
+ },
755
+ messages: {
756
+ explicitSetterType: description$e,
757
+ addExplicitSetterType: `Add {{ setterType }} type annotation`
758
+ },
759
+ type: "problem",
760
+ schema: [],
761
+ fixable: void 0,
762
+ hasSuggestions: true
763
+ },
764
+ defaultOptions: [],
765
+ create(context) {
766
+ const services = ESLintUtils.getParserServices(context);
767
+ let isUsingLumina = false;
768
+ return {
769
+ ImportDeclaration(node) {
770
+ if (node.source.value === luminaEntrypointName) {
771
+ isUsingLumina = true;
772
+ }
773
+ },
774
+ ClassDeclaration(node) {
775
+ if (!isUsingLumina) {
776
+ return;
777
+ }
778
+ const accessorProperties = /* @__PURE__ */ new Set();
779
+ node.body.body.forEach((member) => {
780
+ if (member.type !== AST_NODE_TYPES.PropertyDefinition) {
781
+ return;
782
+ }
783
+ if (member.key.type !== AST_NODE_TYPES.Identifier) {
784
+ return;
785
+ }
786
+ const keyName = member.key.name;
787
+ const isGenericControllerLike = member.value?.type === AST_NODE_TYPES.CallExpression && member.value.callee.type === AST_NODE_TYPES.Identifier && member.value.callee.name.startsWith("use") && member.value.arguments.at(0)?.type === AST_NODE_TYPES.ThisExpression;
788
+ if (isGenericControllerLike) {
789
+ accessorProperties.add(keyName);
790
+ return;
791
+ }
792
+ const isBoundLike = member.value?.type === AST_NODE_TYPES.MemberExpression && member.value.object.type === AST_NODE_TYPES.MemberExpression && member.value.object.object.type === AST_NODE_TYPES.ThisExpression && member.value.object.property.type === AST_NODE_TYPES.Identifier && accessorProperties.has(member.value.object.property.name);
793
+ if (!isBoundLike) {
794
+ return;
795
+ }
796
+ accessorProperties.add(keyName);
797
+ const hasPropertyDecorator = member.decorators?.some(
798
+ (decorator) => decorator.expression.type === AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === AST_NODE_TYPES.Identifier && decorator.expression.callee.name === "property"
799
+ );
800
+ if (!hasPropertyDecorator) {
801
+ return;
802
+ }
803
+ if (member.typeAnnotation !== void 0) {
804
+ return;
805
+ }
806
+ const symbol = services.getSymbolAtLocation(member.value);
807
+ const setter = symbol?.declarations?.find(ts.isSetAccessor);
808
+ const getter = symbol?.declarations?.find(ts.isGetAccessor);
809
+ if (setter === void 0 || getter === void 0) {
810
+ return;
811
+ }
812
+ const setterTypeNode = setter.parameters[0].type;
813
+ const getterTypeNode = getter.type;
814
+ if (setterTypeNode === void 0 || getterTypeNode === void 0) {
815
+ return;
816
+ }
817
+ const checker = services.program.getTypeChecker();
818
+ const setterType = checker.getTypeAtLocation(setterTypeNode);
819
+ const getterType = checker.getTypeAtLocation(getterTypeNode);
820
+ const setterIncludesLiteral = hasTypeFlag(setterType, literalTypeFlag);
821
+ const getterIncludesLiteral = hasTypeFlag(getterType, literalTypeFlag);
822
+ if (getterIncludesLiteral || !setterIncludesLiteral) {
823
+ return;
824
+ }
825
+ const setterTypeString = setterTypeNode.getText();
826
+ context.report({
827
+ node: member.key,
828
+ messageId: "explicitSetterType",
829
+ data: {
830
+ setterType: setterTypeString
831
+ },
832
+ // Not an auto fix because you may need to add imports too
833
+ suggest: [
834
+ {
835
+ messageId: "addExplicitSetterType",
836
+ data: {
837
+ setterType: setterTypeString
838
+ },
839
+ fix(fixer) {
840
+ return fixer.insertTextAfter(member.key, `: ${setterTypeString}`);
841
+ }
842
+ }
843
+ ]
844
+ });
845
+ });
846
+ }
847
+ };
848
+ }
849
+ });
850
+ const ordering = [
851
+ /*
852
+ * Putting static before instance members is a common OOP convention.
853
+ * Static members are always evaluated before instance members.
854
+ */
855
+ "Static Members",
856
+ /**
857
+ * Put private properties before public, because private properties are
858
+ * often used as a default value for public properties.
859
+ */
860
+ "Private Properties",
861
+ /**
862
+ * Put private properties before public, because private properties are often
863
+ * used as a default value for public properties.
864
+ */
865
+ "State Properties",
866
+ /*
867
+ * Properties are the key component API. Also, since default values of
868
+ * properties evaluate before the constructor, they should logically be before
869
+ * the constructor.
870
+ */
871
+ "Public Properties",
872
+ /*
873
+ * Methods should be a supplementary component API, not primary, so are listed
874
+ * after properties. At the same time, it is nice to keep public component API
875
+ * all in one place.
876
+ */
877
+ "Public Methods",
878
+ /*
879
+ * Put events near properties and methods to keep all public component API in
880
+ * one place.
881
+ */
882
+ "Events",
883
+ /*
884
+ * Lifecycle methods like constructor and connectedCallback are executed
885
+ * after all properties have default values, but before any render functions.
886
+ */
887
+ "Lifecycle",
888
+ /*
889
+ * Private methods in between lifecycles and rendering as they are often
890
+ * called by either of them.
891
+ */
892
+ "Private Methods",
893
+ /*
894
+ * Fred:
895
+ * The reason rendering methods are at the bottom of my modules because I can
896
+ * quickly jump to the bottom find the rendering method and work on it. Less
897
+ * scrolling.
898
+ * Once a module has been implemented, most of the bugs/enhancements occur in
899
+ * the rendering methods.
900
+ */
901
+ "Rendering"
902
+ ];
903
+ const getWireframe = () => new Map(ordering.map((region) => [region, []]));
904
+ const entries = Object.entries;
905
+ const regionMatchers = entries({
906
+ "Static Members": (node) => node.type === AST_NODE_TYPES.StaticBlock || "static" in node && node.static,
907
+ "Lifecycle": (_node, name) => lifecyclesSet.has(name),
908
+ "Public Methods": (node) => "decorators" in node && hasDecorator(node, "method"),
909
+ "Public Properties": (node) => "decorators" in node && hasDecorator(node, "property"),
910
+ "State Properties": (node) => "decorators" in node && hasDecorator(node, "state"),
911
+ "Events": isEvent,
912
+ "Rendering": (_node, name) => name.startsWith("render") || name.startsWith("_render"),
913
+ "Private Properties": (node) => node.type === AST_NODE_TYPES.PropertyDefinition || // We don't yet support public accessor properties
914
+ node.type === AST_NODE_TYPES.AccessorProperty || node.type === AST_NODE_TYPES.MethodDefinition && (node.kind === "get" || node.kind === "set"),
915
+ "Private Methods": () => true
916
+ });
917
+ const definitelyComponentRegions = /* @__PURE__ */ new Set([
918
+ "Rendering",
919
+ "State Properties",
920
+ "Public Methods",
921
+ "Events"
922
+ ]);
923
+ const regionsArray = regionMatchers.map(([key]) => key);
924
+ function isEvent(node, name) {
925
+ if (node.type !== AST_NODE_TYPES.PropertyDefinition) {
926
+ return false;
927
+ }
928
+ if (name === "arcgisPropertyChange") {
929
+ return true;
930
+ }
931
+ const initializer = node.value;
932
+ if (initializer?.type !== AST_NODE_TYPES.CallExpression) {
933
+ return false;
934
+ }
935
+ const callExpression = initializer.callee;
936
+ const functionName = callExpression.type === AST_NODE_TYPES.Identifier ? callExpression.name : callExpression.type === AST_NODE_TYPES.CallExpression && callExpression.callee.type === AST_NODE_TYPES.Identifier ? callExpression.callee.name : void 0;
937
+ return functionName === "createEvent" || functionName === "reEmitEvent" || functionName === "usePropertyChange";
938
+ }
939
+ const lifecycles = [
940
+ "constructor",
941
+ "connectedCallback",
942
+ "load",
943
+ "shouldUpdate",
944
+ // Not an actual lifecycle method, but can be temporary inserted by the codemod
945
+ "_shouldUpdate",
946
+ "willUpdate",
947
+ "update",
948
+ "firstUpdated",
949
+ "updated",
950
+ "loaded",
951
+ "disconnectedCallback"
952
+ ];
953
+ const lifecyclesSet = new Set(lifecycles);
954
+ const supportedNodeTypes = {
955
+ MethodDefinition: true,
956
+ PropertyDefinition: true,
957
+ AccessorProperty: true,
958
+ StaticBlock: true,
959
+ // These should not be present in non-abstract Lumina component classes
960
+ TSAbstractAccessorProperty: false,
961
+ TSAbstractMethodDefinition: false,
962
+ TSAbstractPropertyDefinition: false,
963
+ TSIndexSignature: false
964
+ };
965
+ function categorizeComments(membersData, sourceCode) {
966
+ const categorized = {
967
+ header: [],
968
+ regionFooter: /* @__PURE__ */ new Map(),
969
+ memberLineEnd: /* @__PURE__ */ new Map(),
970
+ footer: [],
971
+ membersByName: /* @__PURE__ */ new Map()
972
+ };
973
+ let regionName;
974
+ membersData.forEach((memberData, index) => {
975
+ const nameNodes = categorized.membersByName.get(memberData.name);
976
+ if (nameNodes !== void 0) {
977
+ nameNodes.push(memberData);
978
+ } else {
979
+ categorized.membersByName.set(memberData.name, [memberData]);
980
+ }
981
+ memberData.comments = handleLineEndComment(memberData.comments, membersData.at(index - 1), categorized);
982
+ let newRegionName;
983
+ const lastRegionStart = memberData.comments.findLastIndex((comment) => {
984
+ const match = parseRegionComment(comment);
985
+ const isRegionStart = match !== void 0 && match.end === void 0;
986
+ newRegionName = match?.name;
987
+ return isRegionStart;
988
+ });
989
+ if (lastRegionStart !== -1) {
990
+ const commentsBeforeNewRegionStart = memberData.comments.slice(0, lastRegionStart);
991
+ const previousMember = membersData.at(index - 1);
992
+ handleCommentsAroundEndRegion(
993
+ commentsBeforeNewRegionStart,
994
+ false,
995
+ previousMember ?? memberData,
996
+ categorized,
997
+ regionName
998
+ );
999
+ memberData.comments = memberData.comments.slice(lastRegionStart + 1);
1000
+ regionName = newRegionName;
1001
+ }
1002
+ const isLastNode = index === membersData.length - 1;
1003
+ if (isLastNode) {
1004
+ const trailingComments = sourceCode.getCommentsAfter(memberData.member);
1005
+ const remainingTrailingComments = handleLineEndComment(trailingComments, memberData, categorized);
1006
+ const previousMember = membersData.at(index - 1);
1007
+ handleCommentsAroundEndRegion(
1008
+ remainingTrailingComments,
1009
+ true,
1010
+ previousMember ?? memberData,
1011
+ categorized,
1012
+ regionName
1013
+ );
1014
+ }
1015
+ });
1016
+ return categorized;
1017
+ }
1018
+ function handleLineEndComment(comments, previousNode, categorized) {
1019
+ if (comments.at(0)?.loc.start.line === previousNode?.member.loc.end.line) {
1020
+ categorized.memberLineEnd.set(previousNode, comments.slice(0, 1));
1021
+ return comments.slice(1);
1022
+ }
1023
+ return comments;
1024
+ }
1025
+ function handleCommentsAroundEndRegion(commentsBeforeNewRegionStart, isFooter, lastMember, categorized, regionName) {
1026
+ if (regionName === void 0) {
1027
+ const destination = isFooter ? categorized.footer : categorized.header;
1028
+ destination.push(...commentsBeforeNewRegionStart);
1029
+ } else {
1030
+ const firstEndRegion = commentsBeforeNewRegionStart.findIndex(
1031
+ (comment) => parseRegionComment(comment)?.end !== void 0
1032
+ );
1033
+ let previousRegionComments = commentsBeforeNewRegionStart;
1034
+ if (firstEndRegion !== -1) {
1035
+ categorized.footer.push(...commentsBeforeNewRegionStart.slice(firstEndRegion + 1));
1036
+ previousRegionComments = commentsBeforeNewRegionStart.slice(0, firstEndRegion);
1037
+ }
1038
+ const existingFooter = categorized.regionFooter.get(regionName);
1039
+ if (existingFooter === void 0) {
1040
+ categorized.regionFooter.set(regionName, { comments: Array.from(previousRegionComments), lastMember });
1041
+ } else {
1042
+ existingFooter.comments.push(...previousRegionComments);
1043
+ }
1044
+ }
1045
+ }
1046
+ const normalizeComments = (comments = [], sourceCode, indent = " ") => comments.filter((comment) => parseRegionComment(comment) === void 0).map((comment) => `${indent}${sourceCode.getText(comment)}`).join("\n").replaceAll(reJsApiSectionComment, "");
1047
+ const join = (left, right, joiner = "\n\n") => left === "" || right === "" ? right || left : `${left}${joiner}${right}`;
1048
+ const reRegionComment = /^ ?#(?<end>end)?region ?(?<name>[^\n]*)$/u;
1049
+ const parseRegionComment = (comment) => comment.type === AST_TOKEN_TYPES.Line ? reRegionComment.exec(comment.value)?.groups : void 0;
1050
+ const reJsApiSectionComment = /^ \/\/ ?-{4,}\n(?: \/\/\s*\n)? \/\/\s+[^\s][^\n]+\n(?: \/\/\s*\n)? \/\/ ?-{4,}/gmu;
1051
+ function getNormalizedRegions(membersByName) {
1052
+ const regions = getWireframe();
1053
+ membersByName.values().forEach((members) => {
1054
+ const lowestRegionIndex = members.reduce(
1055
+ (min, { region }) => Math.min(min, regionsArray.indexOf(region)),
1056
+ Number.POSITIVE_INFINITY
1057
+ );
1058
+ if (members.length === 2 && members[0].member.type === AST_NODE_TYPES.MethodDefinition && members[0].member.kind === "set") {
1059
+ members.reverse();
1060
+ }
1061
+ const regionName = regionsArray[lowestRegionIndex];
1062
+ regions.get(regionName).push(...members);
1063
+ });
1064
+ const normalizedRegions = Array.from(regions).filter(([_region, members]) => members.length > 0).map(
1065
+ ([region, members]) => [region, sortableRegions.has(region) ? members.sort(sortFunction.bind(void 0, region)) : members]
1066
+ );
1067
+ return normalizedRegions;
1068
+ }
1069
+ const sortableRegions = /* @__PURE__ */ new Set(["Public Methods", "Events", "Lifecycle"]);
1070
+ function sortFunction(region, leftMember, rightMember) {
1071
+ let leftName = leftMember.name;
1072
+ let rightName = rightMember.name;
1073
+ if (region === "Lifecycle") {
1074
+ const compare = lifecycles.indexOf(leftName) - lifecycles.indexOf(rightName);
1075
+ return compare < 0 ? -1 : 1;
1076
+ }
1077
+ if (leftName.startsWith("_")) {
1078
+ leftName = leftName.slice(1);
1079
+ }
1080
+ if (rightName.startsWith("_")) {
1081
+ rightName = rightName.slice(1);
1082
+ }
1083
+ if (leftName < rightName) {
1084
+ return -1;
1085
+ } else if (leftName > rightName) {
1086
+ return 1;
1087
+ } else {
1088
+ return 0;
1089
+ }
1090
+ }
1091
+ function reParentOrphanFooterComments(normalizedRegions, regionFooter) {
1092
+ const finalRegions = new Set(normalizedRegions.map(([region]) => region));
1093
+ regionFooter.forEach(({ comments, lastMember }, originalRegionName) => {
1094
+ const regionStillExists = finalRegions.has(originalRegionName);
1095
+ if (regionStillExists) {
1096
+ return;
1097
+ }
1098
+ const newFooterCommentsParent = lastMember.region;
1099
+ const existingParentFooter = regionFooter.get(newFooterCommentsParent);
1100
+ if (existingParentFooter === void 0) {
1101
+ regionFooter.set(newFooterCommentsParent, { comments, lastMember });
1102
+ } else {
1103
+ existingParentFooter.comments.push(...comments);
1104
+ }
1105
+ });
1106
+ }
1107
+ function buildNewBody(normalizedRegions, strayComments, sourceCode) {
1108
+ const newRegions = normalizedRegions.map(([region, members]) => {
1109
+ const memberLines = members.map((memberData, index) => {
1110
+ const { member, comments, name } = memberData;
1111
+ const commentsString = normalizeComments(comments, sourceCode);
1112
+ const code = sourceCode.getText(member);
1113
+ const lineEndComment = normalizeComments(strayComments.memberLineEnd.get(memberData), sourceCode, " ");
1114
+ const memberString = ` ${code}${lineEndComment}`;
1115
+ const fullMemberString = join(commentsString, memberString, "\n");
1116
+ const nextMember = members.at(index + 1);
1117
+ const isLast = nextMember === void 0;
1118
+ const isNextSameName = name.length > 0 && name === nextMember?.name;
1119
+ const separator = isLast ? "" : isNextSameName ? "\n" : "\n\n";
1120
+ return `${fullMemberString}${separator}`;
1121
+ });
1122
+ const footerComments = normalizeComments(strayComments.regionFooter.get(region)?.comments, sourceCode);
1123
+ const footer2 = join(footerComments, " //#endregion", "\n\n");
1124
+ return [` //#region ${region}`, memberLines.join(""), footer2].join("\n\n");
1125
+ });
1126
+ const header = normalizeComments(strayComments.header, sourceCode);
1127
+ const footer = normalizeComments(strayComments.footer, sourceCode);
1128
+ const newBodyContent = join(join(header, newRegions.join("\n\n")), footer);
1129
+ const newBody = `{
1130
+ ${newBodyContent}
1131
+ }`;
1132
+ return newBody;
1133
+ }
1134
+ const baseDescription$3 = `Consistently sort component members`;
1135
+ plugin.createRule({
1136
+ name: "member-ordering",
1137
+ meta: {
1138
+ docs: {
1139
+ description: baseDescription$3,
1140
+ defaultLevel: "warn"
1141
+ },
1142
+ messages: {
1143
+ memberOrdering: "Component member ordering is not consistent. Run ESLint autofix to sort members.",
1144
+ unsupportedElementType: "Unsupported class element type: {{ type }}"
1145
+ },
1146
+ type: "layout",
1147
+ schema: [],
1148
+ fixable: "whitespace"
1149
+ },
1150
+ defaultOptions: [],
1151
+ create: (context) => ({
1152
+ ClassDeclaration(component) {
1153
+ const isNamedExport = component.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration;
1154
+ if (!isNamedExport || component.abstract) {
1155
+ return;
1156
+ }
1157
+ const hasExtends = component.superClass !== null;
1158
+ if (!hasExtends) {
1159
+ return;
1160
+ }
1161
+ const sourceCode = context.sourceCode;
1162
+ const seenNames = /* @__PURE__ */ new Set();
1163
+ let currentRegion = "";
1164
+ let hasWrongOrdering = false;
1165
+ let isDefinitelyComponent = false;
1166
+ let hadError = false;
1167
+ const membersData = component.body.body.map((member) => {
1168
+ const isSupportedType = supportedNodeTypes[member.type];
1169
+ if (!isSupportedType) {
1170
+ hadError = true;
1171
+ context.report({ node: member, messageId: "unsupportedElementType", data: { type: member.type } });
1172
+ }
1173
+ const name = "key" in member ? getName(member) ?? "" : "";
1174
+ const newRegion = regionMatchers.find(([, rules]) => rules(member, name))[0];
1175
+ const comments = sourceCode.getCommentsBefore(member);
1176
+ if (!hasWrongOrdering) {
1177
+ let wrongOrdering = false;
1178
+ const previousRegion = currentRegion;
1179
+ for (const comment of comments) {
1180
+ const region = parseRegionComment(comment);
1181
+ if (region === void 0 || region.end !== void 0) {
1182
+ continue;
1183
+ }
1184
+ currentRegion = region.name;
1185
+ wrongOrdering ||= currentRegion !== newRegion;
1186
+ }
1187
+ if (
1188
+ /*
1189
+ * The @property()/@method() decorator might be missing from this
1190
+ * node in case of getter/setter or method overload, while still
1191
+ * being in correct ordering.
1192
+ * The logic in rule body is lightweight compared to more
1193
+ * comprehensive in the autofix to keep common case fast, so we
1194
+ * ignore possible errors in getter/setters (but those errors would
1195
+ * still be fixed as long as there is any other error in the file).
1196
+ */
1197
+ !seenNames.has(name) && (member.type !== AST_NODE_TYPES.MethodDefinition || member.kind !== "get" && member.kind !== "set")
1198
+ ) {
1199
+ wrongOrdering ||= currentRegion !== newRegion;
1200
+ wrongOrdering ||= previousRegion !== currentRegion && ordering.indexOf(previousRegion) > ordering.indexOf(currentRegion);
1201
+ hasWrongOrdering = wrongOrdering;
1202
+ }
1203
+ seenNames.add(name);
1204
+ }
1205
+ isDefinitelyComponent ||= definitelyComponentRegions.has(newRegion);
1206
+ return { member, name, comments, region: newRegion };
1207
+ });
1208
+ if (!isDefinitelyComponent) {
1209
+ return;
1210
+ }
1211
+ if (hadError) {
1212
+ return;
1213
+ }
1214
+ if (!hasWrongOrdering) {
1215
+ return;
1216
+ }
1217
+ context.report({
1218
+ /**
1219
+ * Since this rule is autofixable and stylistic, to be less obtrusive,
1220
+ * report the error for a single character only, rather than entire
1221
+ * component
1222
+ */
1223
+ loc: {
1224
+ start: component.loc.end,
1225
+ end: component.loc.end
1226
+ },
1227
+ messageId: "memberOrdering",
1228
+ /*
1229
+ * We delay as much work as possible till the autofixer is run because
1230
+ * most of the time the rule will be run without need for autofixing.
1231
+ */
1232
+ fix(fixer) {
1233
+ const { membersByName, ...strayComments } = categorizeComments(membersData, sourceCode);
1234
+ const normalizedRegions = getNormalizedRegions(membersByName);
1235
+ reParentOrphanFooterComments(normalizedRegions, strayComments.regionFooter);
1236
+ const newBody = buildNewBody(normalizedRegions, strayComments, sourceCode);
1237
+ return fixer.replaceText(component.body, newBody);
1238
+ }
1239
+ });
1240
+ }
1241
+ })
1242
+ });
1243
+ const blockListedCustomElementNames = /* @__PURE__ */ new Set([
1244
+ "annotation-xml",
1245
+ "color-profile",
1246
+ "font-face",
1247
+ "font-face-src",
1248
+ "font-face-uri",
1249
+ "font-face-format",
1250
+ "font-face-name",
1251
+ "missing-glyph"
1252
+ ]);
1253
+ function isCustomElementTag(tag) {
1254
+ return tag.includes("-") && !blockListedCustomElementNames.has(tag);
1255
+ }
1256
+ plugin.createRule({
1257
+ name: "no-create-element-component",
1258
+ defaultOptions: [],
1259
+ meta: {
1260
+ docs: {
1261
+ description: "This rule ensures that components are not created with `document.createElement()` to ensure imports are tracked by the compiler.",
1262
+ defaultLevel: "error"
1263
+ },
1264
+ fixable: "code",
1265
+ messages: {
1266
+ default: "Use JSX instead of `document.createElement()` to create components.\nDetails: https://qawebgis.esri.com/components/lumina/jsx#rendering-jsx-outside-the-component"
1267
+ },
1268
+ schema: [],
1269
+ type: "problem"
1270
+ },
1271
+ create(context) {
1272
+ const luminaJsxCheck = checkForLuminaJsx();
1273
+ return {
1274
+ ImportDeclaration: luminaJsxCheck,
1275
+ CallExpression(node) {
1276
+ if (!luminaJsxCheck.isLuminaJsx) {
1277
+ return;
1278
+ }
1279
+ if (isCreateElementComponent(node)) {
1280
+ return void context.report({
1281
+ node,
1282
+ messageId: "default"
1283
+ });
1284
+ }
1285
+ }
1286
+ };
1287
+ }
1288
+ });
1289
+ function isCreateElementComponent(node) {
1290
+ if (!(node.callee?.type === AST_NODE_TYPES.MemberExpression && node.callee?.object?.type === AST_NODE_TYPES.Identifier && node.callee?.object?.name === "document" && node.callee?.property?.type === AST_NODE_TYPES.Identifier && node.callee?.property?.name === "createElement" && node.arguments.length >= 1)) {
1291
+ return false;
1292
+ }
1293
+ const localNameArg = node.arguments[0];
1294
+ if (localNameArg.type === AST_NODE_TYPES.Literal && typeof localNameArg.value === "string") {
1295
+ return isCustomElementTag(localNameArg.value);
1296
+ }
1297
+ if (localNameArg.type === AST_NODE_TYPES.TemplateLiteral && localNameArg.expressions.length === 0) {
1298
+ return isCustomElementTag(localNameArg.quasis[0].value.raw);
1299
+ }
1300
+ return false;
1301
+ }
1302
+ const description$d = `Use @internal or @private JSDoc tag over @ignore. See https://qawebgis.esri.com/components/lumina/documenting-components#excluding-api-from-public-documentation`;
1303
+ plugin.createRule({
1304
+ name: "no-ignore-jsdoc-tag",
1305
+ meta: {
1306
+ docs: {
1307
+ description: description$d,
1308
+ defaultLevel: "error"
1309
+ },
1310
+ messages: {
1311
+ noIgnoreJsDocTag: description$d
1312
+ },
1313
+ type: "problem",
1314
+ schema: []
1315
+ },
1316
+ defaultOptions: [],
1317
+ create(context) {
1318
+ const luminaJsxCheck = checkForLuminaJsx();
1319
+ return {
1320
+ "ImportDeclaration": luminaJsxCheck,
1321
+ "Program:exit"() {
1322
+ if (!luminaJsxCheck.isLuminaJsx) {
1323
+ return;
1324
+ }
1325
+ Array.from(
1326
+ context.sourceCode.text.matchAll(reIgnore),
1327
+ (match) => context.report({
1328
+ messageId: "noIgnoreJsDocTag",
1329
+ loc: {
1330
+ start: context.sourceCode.getLocFromIndex(match.index + "* ".length),
1331
+ end: context.sourceCode.getLocFromIndex(match.index + match[0].length)
1332
+ }
1333
+ })
1334
+ );
1335
+ }
1336
+ };
1337
+ }
1338
+ });
1339
+ const reIgnore = /\* @ignore/gu;
1340
+ const description$c = `Detect incorrect usage of dynamic JSX tag name`;
1341
+ plugin.createRule({
1342
+ name: "no-incorrect-dynamic-tag-name",
1343
+ meta: {
1344
+ docs: {
1345
+ description: description$c,
1346
+ defaultLevel: "error"
1347
+ },
1348
+ messages: {
1349
+ incorrectDynamicTagName: `This is using incorrect dynamic tag name syntax. See documentation on how to use dynamic tag in Lumina's JSX: https://qawebgis.esri.com/components/lumina/jsx#dynamic-tag-name`
1350
+ },
1351
+ type: "problem",
1352
+ schema: []
1353
+ },
1354
+ defaultOptions: [],
1355
+ create(context) {
1356
+ const services = ESLintUtils.getParserServices(context);
1357
+ const luminaJsxCheck = checkForLuminaJsx();
1358
+ return {
1359
+ ImportDeclaration: luminaJsxCheck,
1360
+ JSXIdentifier(node) {
1361
+ if (!luminaJsxCheck.isLuminaJsx) {
1362
+ return;
1363
+ }
1364
+ const isInTagName = node.parent?.type !== AST_NODE_TYPES.JSXAttribute;
1365
+ if (!isInTagName) {
1366
+ return;
1367
+ }
1368
+ const isCapitalized = !node.name.startsWith(node.name.charAt(0).toLowerCase());
1369
+ if (!isCapitalized) {
1370
+ return;
1371
+ }
1372
+ const isCorrectDynamicTagName = node.name === "DynamicHtmlTag" || node.name === "DynamicSvgTag";
1373
+ if (isCorrectDynamicTagName) {
1374
+ return;
1375
+ }
1376
+ const type = services.getTypeAtLocation(node);
1377
+ const isStringType = type.flags & ts.TypeFlags.StringLike;
1378
+ if (isStringType) {
1379
+ context.report({
1380
+ messageId: "incorrectDynamicTagName",
1381
+ node
1382
+ });
1383
+ }
1384
+ }
1385
+ };
1386
+ }
1387
+ });
1388
+ const baseDescription$2 = `Do not pass an inline arrow function to a ref prop - such syntax creates a new function on each render, which makes Lit call ref callback again on each render.`;
1389
+ const description$b = `${baseDescription$2}
1390
+
1391
+ Alternatives: https://qawebgis.esri.com/components/lumina/jsx#refs`;
1392
+ plugin.createRule({
1393
+ name: "no-inline-arrow-in-ref",
1394
+ meta: {
1395
+ docs: {
1396
+ description: baseDescription$2,
1397
+ defaultLevel: "error"
1398
+ },
1399
+ messages: {
1400
+ errorInlineArrow: description$b
1401
+ },
1402
+ type: "problem",
1403
+ schema: []
1404
+ },
1405
+ defaultOptions: [],
1406
+ create(context) {
1407
+ const luminaJsxCheck = checkForLuminaJsx();
1408
+ return {
1409
+ ImportDeclaration: luminaJsxCheck,
1410
+ JSXAttribute(node) {
1411
+ if (!luminaJsxCheck.isLuminaJsx || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
1412
+ return;
1413
+ }
1414
+ const initializer = node.value.expression;
1415
+ if (initializer.type !== AST_NODE_TYPES.ArrowFunctionExpression && initializer.type !== AST_NODE_TYPES.FunctionExpression && (initializer.type !== AST_NODE_TYPES.CallExpression || !isBindThisCallee(initializer.callee))) {
1416
+ return;
1417
+ }
1418
+ context.report({
1419
+ messageId: "errorInlineArrow",
1420
+ node: initializer
1421
+ });
1422
+ }
1423
+ };
1424
+ }
1425
+ });
1426
+ const description$a = `directives={} prop value must be an array literal. Documentation: https://qawebgis.esri.com/components/lumina/jsx#lit-directives`;
1427
+ plugin.createRule({
1428
+ name: "no-invalid-directives-prop",
1429
+ meta: {
1430
+ docs: {
1431
+ description: description$a,
1432
+ defaultLevel: "error"
1433
+ },
1434
+ messages: {
1435
+ noInvalidDirectivesProp: description$a
1436
+ },
1437
+ type: "problem",
1438
+ schema: []
1439
+ },
1440
+ defaultOptions: [],
1441
+ create(context) {
1442
+ const luminaJsxCheck = checkForLuminaJsx();
1443
+ return {
1444
+ ImportDeclaration: luminaJsxCheck,
1445
+ JSXAttribute(node) {
1446
+ if (!luminaJsxCheck.isLuminaJsx) {
1447
+ return;
1448
+ }
1449
+ if (node.name.name !== "directives") {
1450
+ return;
1451
+ }
1452
+ const array = node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === AST_NODE_TYPES.ArrayExpression ? node.value.expression : void 0;
1453
+ if (array === void 0 || array.elements.includes(null)) {
1454
+ context.report({
1455
+ messageId: "noInvalidDirectivesProp",
1456
+ node: node.value ?? node
1457
+ });
1458
+ return;
1459
+ }
1460
+ array.elements.forEach((element) => {
1461
+ if (element?.type === AST_NODE_TYPES.SpreadElement) {
1462
+ context.report({
1463
+ messageId: "noInvalidDirectivesProp",
1464
+ node: element
1465
+ });
1466
+ }
1467
+ });
1468
+ }
1469
+ };
1470
+ }
1471
+ });
1472
+ const description$9 = `This spread syntax is not supported. Alternatives: https://qawebgis.esri.com/components/lumina/jsx#spread-attributes`;
1473
+ plugin.createRule({
1474
+ name: "no-jsx-spread",
1475
+ meta: {
1476
+ docs: {
1477
+ description: description$9,
1478
+ defaultLevel: "error"
1479
+ },
1480
+ messages: {
1481
+ noJsxSpread: description$9
1482
+ },
1483
+ type: "problem",
1484
+ schema: []
1485
+ },
1486
+ defaultOptions: [],
1487
+ create(context) {
1488
+ const luminaJsxCheck = checkForLuminaJsx();
1489
+ return {
1490
+ ImportDeclaration: luminaJsxCheck,
1491
+ JSXSpreadAttribute(node) {
1492
+ if (!luminaJsxCheck.isLuminaJsx) {
1493
+ return;
1494
+ }
1495
+ const name = node.parent.name;
1496
+ if (
1497
+ // Spread syntax is allowed in function calls like <this.render {...props} />
1498
+ name.type === AST_NODE_TYPES.JSXIdentifier && // Spread syntax is allowed in functions
1499
+ (name.name.toLowerCase() === name.name || name.name === "DynamicHtmlTag" || name.name === "DynamicSvgTag")
1500
+ ) {
1501
+ context.report({
1502
+ messageId: "noJsxSpread",
1503
+ node
1504
+ });
1505
+ }
1506
+ }
1507
+ };
1508
+ }
1509
+ });
1510
+ const baseDescription$1 = `Do not call this.listen()/this.listenOn() in connectedCallback.`;
1511
+ const description$8 = `${baseDescription$1}
1512
+
1513
+ Instead, call this.listen()/this.listenOn() in constructor(), load() or loaded().
1514
+
1515
+ Reason:
1516
+ this.listen() automatically creates and cleanups the listener on connect/disconnect.
1517
+ Since connectedCallback can be called multiple times, duplicate listeners may be created.`;
1518
+ plugin.createRule({
1519
+ name: "no-listen-in-connected-callback",
1520
+ meta: {
1521
+ docs: {
1522
+ description: baseDescription$1,
1523
+ defaultLevel: "error"
1524
+ },
1525
+ messages: {
1526
+ errorListenInConnectedCallback: description$8
1527
+ },
1528
+ type: "problem",
1529
+ schema: []
1530
+ },
1531
+ defaultOptions: [],
1532
+ create(context) {
1533
+ return {
1534
+ CallExpression(node) {
1535
+ const isListenCall = node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.object.type === AST_NODE_TYPES.ThisExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && (node.callee.property.name === "listen" || node.callee.property.name === "listenOn");
1536
+ if (!isListenCall) {
1537
+ return;
1538
+ }
1539
+ let hasConnectedCallbackParent = false;
1540
+ let currentParent = node.parent;
1541
+ while (currentParent) {
1542
+ if (currentParent.type === AST_NODE_TYPES.MethodDefinition) {
1543
+ if (currentParent.key.type === AST_NODE_TYPES.Identifier && currentParent.key.name === "connectedCallback") {
1544
+ hasConnectedCallbackParent = true;
1545
+ }
1546
+ break;
1547
+ }
1548
+ currentParent = currentParent.parent;
1549
+ }
1550
+ if (!hasConnectedCallbackParent) {
1551
+ return;
1552
+ }
1553
+ context.report({
1554
+ messageId: "errorListenInConnectedCallback",
1555
+ node
1556
+ });
1557
+ }
1558
+ };
1559
+ }
1560
+ });
1561
+ const description$7 = `To ensure Hot Module Replacement (HMR) works correctly, the file that defines the Lumina component must not export anything other than the component. Exceptions: type-only exports, and the \`exportsForTests\` object for exposing additional things for usages in tests only`;
1562
+ plugin.createRule({
1563
+ name: "no-non-component-exports",
1564
+ meta: {
1565
+ docs: {
1566
+ description: description$7,
1567
+ defaultLevel: "warn"
1568
+ },
1569
+ messages: {
1570
+ noNonComponentExports: description$7,
1571
+ noDefaultExports: `Default exports are not allowed in files that export Lumina component - only named exports are allowed. ${description$7}`,
1572
+ noExportAll: `\`export *\` exports are not allowed in files that export Lumina component - only named exports are allowed. ${description$7}`
1573
+ },
1574
+ type: "problem",
1575
+ schema: []
1576
+ },
1577
+ defaultOptions: [],
1578
+ create(context) {
1579
+ const declaredComponents = /* @__PURE__ */ new Set();
1580
+ const hasLuminaDeclarations = context.sourceCode.text.includes("interface DeclareElements");
1581
+ return {
1582
+ TSModuleDeclaration(node) {
1583
+ const luminaDeclarationInterface = extractDeclareElementsInterface(node);
1584
+ if (luminaDeclarationInterface === void 0) {
1585
+ return;
1586
+ }
1587
+ luminaDeclarationInterface.body.body.forEach((member) => {
1588
+ if (member.type !== AST_NODE_TYPES.TSPropertySignature || member.computed) {
1589
+ return;
1590
+ }
1591
+ const type = member.typeAnnotation?.typeAnnotation;
1592
+ if (type?.type !== AST_NODE_TYPES.TSTypeReference || type.typeName.type !== AST_NODE_TYPES.Identifier) {
1593
+ return;
1594
+ }
1595
+ const className = type.typeName.name;
1596
+ declaredComponents.add(className);
1597
+ });
1598
+ },
1599
+ ExportNamedDeclaration(node) {
1600
+ if (!hasLuminaDeclarations && declaredComponents.size === 0) {
1601
+ return;
1602
+ }
1603
+ if (node.exportKind === "type") {
1604
+ return;
1605
+ }
1606
+ if (node.declaration?.type === AST_NODE_TYPES.VariableDeclaration) {
1607
+ const isExportsForTests = node.declaration.declarations.every(
1608
+ (declaration) => declaration.id.type === AST_NODE_TYPES.Identifier && declaration.id.name === "exportsForTests"
1609
+ );
1610
+ if (isExportsForTests) {
1611
+ return;
1612
+ }
1613
+ } else if (node.declaration?.type === AST_NODE_TYPES.FunctionDeclaration) {
1614
+ const isExportsForTests = node.declaration.id?.name === "exportsForTests";
1615
+ if (isExportsForTests) {
1616
+ return;
1617
+ }
1618
+ } else if (
1619
+ // Type-only constructs
1620
+ node.declaration?.type === AST_NODE_TYPES.TSDeclareFunction || node.declaration?.type === AST_NODE_TYPES.TSInterfaceDeclaration || node.declaration?.type === AST_NODE_TYPES.TSTypeAliasDeclaration
1621
+ ) {
1622
+ return;
1623
+ } else if (node.declaration?.type === AST_NODE_TYPES.ClassDeclaration) {
1624
+ const isComponent = declaredComponents.has(node.declaration.id?.name ?? "");
1625
+ if (isComponent) {
1626
+ return;
1627
+ }
1628
+ }
1629
+ context.report({
1630
+ node,
1631
+ messageId: "noNonComponentExports"
1632
+ });
1633
+ },
1634
+ ExportDefaultDeclaration(node) {
1635
+ if (!hasLuminaDeclarations && declaredComponents.size === 0) {
1636
+ return;
1637
+ }
1638
+ context.report({
1639
+ node,
1640
+ messageId: "noDefaultExports"
1641
+ });
1642
+ },
1643
+ ExportAllDeclaration(node) {
1644
+ if (!hasLuminaDeclarations && declaredComponents.size === 0) {
1645
+ return;
1646
+ }
1647
+ if (node.exportKind === "type") {
1648
+ return;
1649
+ }
1650
+ context.report({
1651
+ node,
1652
+ messageId: "noExportAll"
1653
+ });
1654
+ }
1655
+ };
1656
+ }
1657
+ });
1658
+ const description$6 = `Do not start public property names with "on" as that can confuse frameworks into thinking this property is an event`;
1659
+ plugin.createRule({
1660
+ name: "no-property-name-start-with-on",
1661
+ meta: {
1662
+ docs: {
1663
+ description: description$6,
1664
+ defaultLevel: "error"
1665
+ },
1666
+ messages: {
1667
+ noPropertyNameStartWithOn: description$6
1668
+ },
1669
+ type: "problem",
1670
+ schema: []
1671
+ },
1672
+ defaultOptions: [],
1673
+ create(context) {
1674
+ return {
1675
+ PropertyDefinition(node) {
1676
+ const isPublicProperty = hasDecorator(node, "property");
1677
+ if (!isPublicProperty) {
1678
+ return;
1679
+ }
1680
+ const propertyName = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : node.key.type === AST_NODE_TYPES.Literal ? node.key.value : void 0;
1681
+ if (typeof propertyName !== "string") {
1682
+ return;
1683
+ }
1684
+ if (!propertyName.startsWith("on")) {
1685
+ return;
1686
+ }
1687
+ const comments = context.sourceCode.getCommentsBefore(node);
1688
+ const isDeprecated = comments.some((comment) => comment.value.includes("@deprecated"));
1689
+ if (isDeprecated) {
1690
+ return;
1691
+ }
1692
+ context.report({
1693
+ messageId: "noPropertyNameStartWithOn",
1694
+ node: node.key
1695
+ });
1696
+ }
1697
+ };
1698
+ }
1699
+ });
1700
+ const baseDescription = `Avoid accidentally rendering "false" to the screen (Lit stringifies booleans, rather than drop them like React/Stencil).`;
1701
+ const description$5 = `${baseDescription}
1702
+
1703
+ Lumina automatically handles some cases where "false" may get rendered to the screen, but the pattern this code is using is not handled.`;
1704
+ plugin.createRule({
1705
+ name: "no-render-false",
1706
+ meta: {
1707
+ docs: {
1708
+ description: baseDescription,
1709
+ defaultLevel: "warn"
1710
+ },
1711
+ messages: {
1712
+ errorFalseRendered: description$5
1713
+ },
1714
+ type: "problem",
1715
+ schema: [],
1716
+ fixable: "code"
1717
+ },
1718
+ defaultOptions: [],
1719
+ create(context) {
1720
+ const services = ESLintUtils.getParserServices(context);
1721
+ return {
1722
+ LogicalExpression(node) {
1723
+ if (node.operator !== "&&") {
1724
+ return;
1725
+ }
1726
+ const type = services.getTypeAtLocation(node.right);
1727
+ if (!isLuminaJsxType(type)) {
1728
+ return;
1729
+ }
1730
+ if (node.parent.type === AST_NODE_TYPES.JSXExpressionContainer) {
1731
+ return;
1732
+ }
1733
+ const right = unwrapExpression(node.right);
1734
+ if (right.type === AST_NODE_TYPES.JSXElement) {
1735
+ return;
1736
+ }
1737
+ if (node.parent.type === AST_NODE_TYPES.LogicalExpression) {
1738
+ return;
1739
+ }
1740
+ context.report({
1741
+ messageId: "errorFalseRendered",
1742
+ node,
1743
+ fix(fixer) {
1744
+ const leftText = context.sourceCode.getText(node.left);
1745
+ const rightText = context.sourceCode.getText(node.right);
1746
+ const replacement = `${leftText} ? ${rightText} : ""`;
1747
+ return fixer.replaceText(node, replacement);
1748
+ }
1749
+ });
1750
+ }
1751
+ };
1752
+ }
1753
+ });
1754
+ const description$4 = `Avoid using type assertions like 'event.target as ...', 'event.currentTarget as ...', or 'event.detail as ...'. Instead, improve the event argument type to avoid the need for assertions.`;
1755
+ plugin.createRule({
1756
+ name: "no-unnecessary-assertion-on-event",
1757
+ meta: {
1758
+ docs: {
1759
+ description: description$4,
1760
+ defaultLevel: "warn"
1761
+ },
1762
+ messages: {
1763
+ needlessInlineJsxEventTypeAnnotation: "There is no need for this type annotation as JSX types already define event types.",
1764
+ redundantTypeAssertion: "This type assertion is likely unnecessary.",
1765
+ avoidTypeAssertion: "This type assertion might be avoidable if you improve the event argument type. See examples in https://qawebgis.esri.com/components/lumina/events#listening-to-events-on-children-components"
1766
+ },
1767
+ hasSuggestions: true,
1768
+ fixable: "code",
1769
+ type: "suggestion",
1770
+ schema: []
1771
+ },
1772
+ defaultOptions: [],
1773
+ create(context) {
1774
+ const luminaJsxCheck = checkForLuminaJsx();
1775
+ return {
1776
+ ImportDeclaration: luminaJsxCheck,
1777
+ TSAsExpression(node) {
1778
+ if (!luminaJsxCheck.isLuminaJsx || node.expression.type !== AST_NODE_TYPES.MemberExpression || node.expression.object.type !== AST_NODE_TYPES.Identifier || node.expression.property.type !== AST_NODE_TYPES.Identifier || !["target", "currentTarget", "detail"].includes(node.expression.property.name)) {
1779
+ return;
1780
+ }
1781
+ const eventVariableName = node.expression.object.name;
1782
+ let parent = node.parent;
1783
+ let functionParent = void 0;
1784
+ while (parent != null) {
1785
+ if (parent.type === AST_NODE_TYPES.FunctionExpression || parent.type === AST_NODE_TYPES.FunctionDeclaration || parent.type === AST_NODE_TYPES.ArrowFunctionExpression) {
1786
+ functionParent = parent;
1787
+ break;
1788
+ }
1789
+ parent = parent.parent;
1790
+ }
1791
+ if (functionParent === void 0) {
1792
+ return;
1793
+ }
1794
+ const argument = functionParent.params.at(0);
1795
+ if (argument === void 0 || argument.type !== AST_NODE_TYPES.Identifier || argument.name !== eventVariableName) {
1796
+ return;
1797
+ }
1798
+ const reportPossiblyUnnecessaryTypeAssertion = () => context.report({
1799
+ messageId: "redundantTypeAssertion",
1800
+ node,
1801
+ // If it is definitely unnecessary, it will be removed by
1802
+ // the `@typescript-eslint/no-unnecessary-type-assertion` rule.
1803
+ // So we only report as a suggestion for manual review.
1804
+ suggest: [
1805
+ {
1806
+ messageId: "redundantTypeAssertion",
1807
+ fix(fixer) {
1808
+ return fixer.replaceTextRange(
1809
+ [node.range[0], node.range[1]],
1810
+ context.sourceCode.getText(node.expression)
1811
+ );
1812
+ }
1813
+ }
1814
+ ]
1815
+ });
1816
+ const isInlineListener = functionParent.parent.type === AST_NODE_TYPES.JSXExpressionContainer;
1817
+ if (isInlineListener) {
1818
+ if (argument.typeAnnotation === void 0) {
1819
+ reportPossiblyUnnecessaryTypeAssertion();
1820
+ return;
1821
+ } else {
1822
+ const typeAnnotation = argument.typeAnnotation;
1823
+ context.report({
1824
+ messageId: "needlessInlineJsxEventTypeAnnotation",
1825
+ node: typeAnnotation,
1826
+ fix(fixer) {
1827
+ return fixer.remove(typeAnnotation);
1828
+ }
1829
+ });
1830
+ return;
1831
+ }
1832
+ }
1833
+ if (argument.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSUnionType) {
1834
+ return;
1835
+ }
1836
+ context.report({
1837
+ messageId: "avoidTypeAssertion",
1838
+ node
1839
+ });
1840
+ }
1841
+ };
1842
+ }
1843
+ });
1844
+ const description$3 = 'There is no need for { attribute: "name" } in @property() when attribute name is trivially inferrable from the property name';
1845
+ plugin.createRule({
1846
+ name: "no-unnecessary-attribute-name",
1847
+ meta: {
1848
+ docs: {
1849
+ description: description$3,
1850
+ defaultLevel: "warn"
1851
+ },
1852
+ messages: {
1853
+ noUnnecessaryAttributeName: description$3
1854
+ },
1855
+ type: "suggestion",
1856
+ schema: [],
1857
+ fixable: "code"
1858
+ },
1859
+ defaultOptions: [],
1860
+ create(context) {
1861
+ return {
1862
+ Decorator(decorator) {
1863
+ const part = parsePropertyDecorator(decorator);
1864
+ if (part === void 0) {
1865
+ return;
1866
+ }
1867
+ const { properties } = part;
1868
+ const property = decorator.parent;
1869
+ if (property?.type !== AST_NODE_TYPES.MethodDefinition && property?.type !== AST_NODE_TYPES.PropertyDefinition) {
1870
+ return;
1871
+ }
1872
+ const attributeOption = getProperty(properties, "attribute");
1873
+ const attributeValue = attributeOption?.type === AST_NODE_TYPES.Literal && typeof attributeOption.value === "string" ? attributeOption.value : void 0;
1874
+ if (attributeOption === void 0 || attributeValue === void 0) {
1875
+ return;
1876
+ }
1877
+ const propertyName = getName(property);
1878
+ if (propertyName === void 0) {
1879
+ return;
1880
+ }
1881
+ const inferredName = camelToKebab(propertyName);
1882
+ if (attributeValue === inferredName) {
1883
+ context.report({
1884
+ node: attributeOption,
1885
+ messageId: "noUnnecessaryAttributeName",
1886
+ fix(fixer) {
1887
+ const nextToken = context.sourceCode.getTokenAfter(attributeOption.parent);
1888
+ const isTrailingComma = nextToken && nextToken.value === ",";
1889
+ return isTrailingComma ? fixer.removeRange([attributeOption.parent.range[0], nextToken.range[1]]) : fixer.remove(attributeOption.parent);
1890
+ }
1891
+ });
1892
+ }
1893
+ }
1894
+ };
1895
+ }
1896
+ });
1897
+ const description$2 = `.bind(this) is not necessary in Lit's event listener callbacks and ref callbacks as it is handled automatically.`;
1898
+ plugin.createRule({
1899
+ name: "no-unnecessary-bind-this",
1900
+ meta: {
1901
+ docs: {
1902
+ description: description$2,
1903
+ defaultLevel: "warn"
1904
+ },
1905
+ messages: {
1906
+ noUnnecessaryBindThis: description$2
1907
+ },
1908
+ type: "problem",
1909
+ schema: [],
1910
+ fixable: "code"
1911
+ },
1912
+ defaultOptions: [],
1913
+ create(context) {
1914
+ const luminaJsxCheck = checkForLuminaJsx();
1915
+ return {
1916
+ ImportDeclaration: luminaJsxCheck,
1917
+ JSXExpressionContainer(node) {
1918
+ if (!luminaJsxCheck.isLuminaJsx || node.parent.type !== AST_NODE_TYPES.JSXAttribute || node.parent.name.type !== AST_NODE_TYPES.JSXIdentifier) {
1919
+ return;
1920
+ }
1921
+ const name = node.parent.name.name;
1922
+ const isAutoBindable = name.startsWith("on") || name === "ref";
1923
+ if (!isAutoBindable) {
1924
+ return;
1925
+ }
1926
+ const expression = node.expression.type === AST_NODE_TYPES.ChainExpression ? node.expression.expression : node.expression;
1927
+ const isCallWithThis = (
1928
+ // expression(...)
1929
+ expression.type === AST_NODE_TYPES.CallExpression && // expression(expression)
1930
+ expression.arguments.length === 1 && // expression(this)
1931
+ expression.arguments[0].type === AST_NODE_TYPES.ThisExpression
1932
+ );
1933
+ if (!isCallWithThis) {
1934
+ return;
1935
+ }
1936
+ const callee = expression.callee;
1937
+ if (!isBindThisCallee(callee)) {
1938
+ return;
1939
+ }
1940
+ const tagName = context.sourceCode.getAncestors(node.parent).find((ancestor) => ancestor.type === AST_NODE_TYPES.JSXOpeningElement)?.name;
1941
+ const tagNameString = tagName?.type === AST_NODE_TYPES.JSXIdentifier ? tagName.name : void 0;
1942
+ if (tagNameString === void 0) {
1943
+ return;
1944
+ }
1945
+ const isHtmlElement = tagNameString.startsWith(tagNameString.charAt(0).toLowerCase()) || tagNameString === "DynamicHtmlTag" || tagNameString === "DynamicSvgTag";
1946
+ if (!isHtmlElement) {
1947
+ return;
1948
+ }
1949
+ context.report({
1950
+ messageId: "noUnnecessaryBindThis",
1951
+ node: callee.property,
1952
+ fix(fixer) {
1953
+ return fixer.replaceText(expression, context.sourceCode.getText(callee.object));
1954
+ }
1955
+ });
1956
+ }
1957
+ };
1958
+ }
1959
+ });
1960
+ const description$1 = `In most cases, key={index} is not necessary in Lumina in .map(). Details: https://qawebgis.esri.com/components/lumina/jsx#key-prop`;
1961
+ plugin.createRule({
1962
+ name: "no-unnecessary-key",
1963
+ meta: {
1964
+ docs: {
1965
+ description: description$1,
1966
+ defaultLevel: "warn"
1967
+ },
1968
+ messages: {
1969
+ noUnnecessaryKey: description$1
1970
+ },
1971
+ type: "suggestion",
1972
+ schema: [],
1973
+ fixable: "code"
1974
+ },
1975
+ defaultOptions: [],
1976
+ create(context) {
1977
+ const luminaJsxCheck = checkForLuminaJsx();
1978
+ return {
1979
+ ImportDeclaration: luminaJsxCheck,
1980
+ JSXExpressionContainer(node) {
1981
+ if (!luminaJsxCheck.isLuminaJsx) {
1982
+ return;
1983
+ }
1984
+ const keyValue = node.expression;
1985
+ if (keyValue.type !== AST_NODE_TYPES.Identifier) {
1986
+ return;
1987
+ }
1988
+ const keyAttribute = node.parent;
1989
+ if (keyAttribute?.type !== AST_NODE_TYPES.JSXAttribute || keyAttribute.name.type !== AST_NODE_TYPES.JSXIdentifier || keyAttribute.name.name !== "key") {
1990
+ return;
1991
+ }
1992
+ const mapArrowFunction = context.sourceCode.getAncestors(keyAttribute).find(
1993
+ (ancestor) => ancestor.type === AST_NODE_TYPES.ArrowFunctionExpression && ancestor.parent?.type === AST_NODE_TYPES.CallExpression && ancestor.parent?.callee.type === AST_NODE_TYPES.MemberExpression && ancestor.parent?.callee.property.type === AST_NODE_TYPES.Identifier && ancestor.parent?.callee.property.name === "map"
1994
+ );
1995
+ if (mapArrowFunction === void 0) {
1996
+ return;
1997
+ }
1998
+ const keyParameter = mapArrowFunction.params.at(1);
1999
+ if (keyParameter?.type !== AST_NODE_TYPES.Identifier || keyParameter.name !== keyValue.name) {
2000
+ return;
2001
+ }
2002
+ context.report({
2003
+ messageId: "noUnnecessaryKey",
2004
+ node: keyAttribute,
2005
+ fix(fixer) {
2006
+ const usesIndexOutsideKey = Array.from(
2007
+ context.sourceCode.getText(mapArrowFunction.body).matchAll(new RegExp(`\\b${keyValue.name}\\b`, "ug"))
2008
+ ).length > 1;
2009
+ const withoutAttribute = fixer.remove(keyAttribute);
2010
+ if (usesIndexOutsideKey || mapArrowFunction.params.length > 2) {
2011
+ return withoutAttribute;
2012
+ } else {
2013
+ return [withoutAttribute, fixer.remove(keyParameter)];
2014
+ }
2015
+ }
2016
+ });
2017
+ }
2018
+ };
2019
+ }
2020
+ });
2021
+ const description = `Validate component tag name`;
2022
+ const defaultOptions = [
2023
+ {
2024
+ namespaces: ["arcgis-"]
2025
+ }
2026
+ ];
2027
+ plugin.createRule({
2028
+ name: "tag-name-rules",
2029
+ meta: {
2030
+ docs: {
2031
+ description,
2032
+ defaultLevel: "error"
2033
+ },
2034
+ messages: {
2035
+ requireNamespace: "Custom element tag names in this project must start with one of the following namespaces: {{namespaces}}",
2036
+ noComputedTagName: "Computed tag names are not allowed",
2037
+ noNonStringTagName: 'Tag names must be strings like "arcgis-click" rather than numbers',
2038
+ noTagNameIdentifier: "Tag name must include a dash",
2039
+ unexpectedDeclareElementsEntry: "Unexpected entry in declare elements interface. Expected a property signature",
2040
+ unexpectedDeclarationType: "Unexpected declaration type. Expected component class identifier",
2041
+ duplicateDeclaration: "The same component may only be assigned to one class name. If you need multiple tag names, consider sub-classing the component",
2042
+ missingClassDeclaration: "Missing class declaration",
2043
+ unexpectedTypeArguments: "Unexpected type arguments in the component tag name declaration. Type arguments should be specified in the component class declaration",
2044
+ reservedTagName: "This is a reserved html element tag name. Declaring components with this name is forbidden by the custom elements specification.",
2045
+ duplicateDeclareGlobal: `There should only be a single "declare global" block in a file. Declaring multiple components in a single file is supported, but they should share the same "declare global {" block. Example:
2046
+ declare global {
2047
+ interface DeclareElements {
2048
+ "arcgis-test1": ArcgisTest1;
2049
+ "arcgis-test2": ArcgisTest2;
2050
+ }
2051
+ }`,
2052
+ mustSubclass: "Lumina component is required to subclass LitElement or a subclass of LitElement",
2053
+ mustNotSubclassHtmlElement: "Lumina component is not allowed to subclass HTMLElement classes directly. Subclass LitElement or a subclass of LitElement instead",
2054
+ tagNameClassNameMismatch: 'The custom element tag name "{{tagName}}" does not seem to match the class name it is assigned to: "{{className}}". Make sure tag name and class name use the same characters in the same order (case insensitive, hyphens removed). {{namespaceNotice}}',
2055
+ missingHyphen: "Custom element tag names need to include a hyphen",
2056
+ lowercaseTagName: "For consistency, custom element tag names should be defined in lowercase",
2057
+ invalidTagName: "Potentially invalid custom element name. Expected tag name to match regex: {{regex}}. This regex is stricter than what the custom elements spec allows in order to catch possible typos. Please contact Lumina maintainers if you need to loosen this check."
2058
+ },
2059
+ type: "problem",
2060
+ schema: [
2061
+ {
2062
+ type: "object",
2063
+ properties: {
2064
+ namespaces: {
2065
+ type: "array",
2066
+ items: {
2067
+ type: "string"
2068
+ }
2069
+ }
2070
+ },
2071
+ additionalProperties: false
2072
+ }
2073
+ ],
2074
+ fixable: "code"
2075
+ },
2076
+ defaultOptions,
2077
+ create(context, options) {
2078
+ const declaredComponents = /* @__PURE__ */ new Map();
2079
+ let seenDeclareGlobal = false;
2080
+ return {
2081
+ "TSModuleDeclaration"(node) {
2082
+ const luminaDeclarationInterface = extractDeclareElementsInterface(node);
2083
+ if (luminaDeclarationInterface === void 0) {
2084
+ return;
2085
+ }
2086
+ if (seenDeclareGlobal) {
2087
+ context.report({
2088
+ messageId: "duplicateDeclareGlobal",
2089
+ node
2090
+ });
2091
+ return;
2092
+ }
2093
+ seenDeclareGlobal = true;
2094
+ luminaDeclarationInterface.body.body.forEach((member) => {
2095
+ if (member.type !== AST_NODE_TYPES.TSPropertySignature) {
2096
+ context.report({
2097
+ messageId: "unexpectedDeclareElementsEntry",
2098
+ node: member
2099
+ });
2100
+ return;
2101
+ }
2102
+ if (member.computed) {
2103
+ context.report({
2104
+ messageId: "noComputedTagName",
2105
+ node: member.key
2106
+ });
2107
+ return;
2108
+ }
2109
+ if (member.key.type === AST_NODE_TYPES.Identifier) {
2110
+ context.report({
2111
+ messageId: "noTagNameIdentifier",
2112
+ node: member.key
2113
+ });
2114
+ return;
2115
+ }
2116
+ if (typeof member.key.value !== "string") {
2117
+ context.report({
2118
+ messageId: "noNonStringTagName",
2119
+ node: member.key
2120
+ });
2121
+ return;
2122
+ }
2123
+ const tagName = member.key.value;
2124
+ let namespaceFreeTagName = tagName;
2125
+ const namespaces = options[0].namespaces;
2126
+ if (namespaces.length > 0) {
2127
+ const namespace = namespaces.find((ns) => tagName.startsWith(ns));
2128
+ if (namespace === void 0) {
2129
+ context.report({
2130
+ messageId: "requireNamespace",
2131
+ node: member.key,
2132
+ data: {
2133
+ namespaces: namespaces.join(", ")
2134
+ }
2135
+ });
2136
+ return;
2137
+ } else {
2138
+ namespaceFreeTagName = tagName.slice(namespace.length);
2139
+ }
2140
+ }
2141
+ if (!tagName.includes("-")) {
2142
+ context.report({
2143
+ messageId: "missingHyphen",
2144
+ node: member.key
2145
+ });
2146
+ return;
2147
+ }
2148
+ if (tagName.toLowerCase() !== tagName) {
2149
+ context.report({
2150
+ messageId: "lowercaseTagName",
2151
+ node: member.key
2152
+ });
2153
+ return;
2154
+ }
2155
+ if (!reCustomElementName.test(tagName)) {
2156
+ context.report({
2157
+ messageId: "invalidTagName",
2158
+ node: member.key,
2159
+ data: {
2160
+ regex: reCustomElementName.toString()
2161
+ }
2162
+ });
2163
+ return;
2164
+ }
2165
+ if (blockListedCustomElementNames.has(tagName)) {
2166
+ context.report({
2167
+ messageId: "reservedTagName",
2168
+ node: member.key
2169
+ });
2170
+ return;
2171
+ }
2172
+ const type = member.typeAnnotation?.typeAnnotation;
2173
+ if (type?.type !== AST_NODE_TYPES.TSTypeReference || type.typeName.type !== AST_NODE_TYPES.Identifier) {
2174
+ context.report({
2175
+ messageId: "unexpectedDeclarationType",
2176
+ node: type ?? member
2177
+ });
2178
+ return;
2179
+ }
2180
+ if (type.typeArguments !== void 0) {
2181
+ context.report({
2182
+ messageId: "unexpectedTypeArguments",
2183
+ node: type
2184
+ });
2185
+ return;
2186
+ }
2187
+ const className = type.typeName.name;
2188
+ const classNameFromTagName = tagNameToNormalizedClassName(tagName);
2189
+ const classNameFromShortTagName = tagNameToNormalizedClassName(namespaceFreeTagName);
2190
+ if (classNameFromTagName !== className.toLowerCase() && classNameFromShortTagName !== className.toLowerCase()) {
2191
+ context.report({
2192
+ messageId: "tagNameClassNameMismatch",
2193
+ node: type.typeName,
2194
+ data: {
2195
+ tagName,
2196
+ className,
2197
+ namespaceNotice: namespaces.length > 0 ? `In addition, the following tag name prefixes are expected: ${namespaces.join(", ")}` : ""
2198
+ }
2199
+ });
2200
+ return;
2201
+ }
2202
+ if (declaredComponents.has(className)) {
2203
+ context.report({
2204
+ messageId: "duplicateDeclaration",
2205
+ node: type.typeName
2206
+ });
2207
+ return;
2208
+ }
2209
+ declaredComponents.set(className, type.typeName);
2210
+ });
2211
+ },
2212
+ "ClassDeclaration"(node) {
2213
+ if (node.id !== null && declaredComponents.has(node.id.name)) {
2214
+ declaredComponents.delete(node.id.name);
2215
+ if (node.superClass === null) {
2216
+ context.report({
2217
+ messageId: "mustSubclass",
2218
+ node
2219
+ });
2220
+ } else if (node.superClass.type === AST_NODE_TYPES.Identifier && node.superClass.name.startsWith("HTML") && node.superClass.name.endsWith("Element")) {
2221
+ context.report({
2222
+ messageId: "mustNotSubclassHtmlElement",
2223
+ node
2224
+ });
2225
+ }
2226
+ }
2227
+ },
2228
+ "Program:exit"() {
2229
+ for (const identifier of declaredComponents.values()) {
2230
+ context.report({
2231
+ messageId: "missingClassDeclaration",
2232
+ node: identifier
2233
+ });
2234
+ }
2235
+ }
2236
+ };
2237
+ }
2238
+ });
2239
+ const tagNameToNormalizedClassName = (tagName) => tagName.replaceAll("-", "").toLowerCase();
2240
+ const reCustomElementName = /^[a-z]+(?:-[a-z\d]+)+$/u;
2241
+ const luminaPlugin = plugin.finalize();
5
2242
  export {
6
- lumina_default as default
2243
+ luminaPlugin
7
2244
  };