@arcgis/eslint-config 5.2.0-next.2 → 5.2.0-next.20

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 (64) hide show
  1. package/dist/config/applications.d.ts +4 -3
  2. package/dist/config/applications.js +4 -4
  3. package/dist/config/extra.d.ts +4 -24
  4. package/dist/config/extra.js +2 -2
  5. package/dist/config/index.d.ts +3 -2
  6. package/dist/config/index.js +7 -10
  7. package/dist/config/lumina.d.ts +4 -3
  8. package/dist/config/lumina.js +8 -8
  9. package/dist/{estree-DW92hBTd.js → estree-CDc-die8.js} +1 -39
  10. package/dist/makePlugin-kNH-PFwP.js +41 -0
  11. package/dist/plugins/core/index.d.ts +4 -0
  12. package/dist/plugins/core/index.js +702 -0
  13. package/dist/plugins/lumina/index.d.ts +4 -5
  14. package/dist/plugins/lumina/index.js +2 -1
  15. package/dist/plugins/utils/makePlugin.d.ts +15 -16
  16. package/dist/plugins/webgis/index.d.ts +4 -5
  17. package/dist/plugins/webgis/index.js +794 -119
  18. package/dist/utils/defineBoundaries.d.ts +22 -38
  19. package/dist/utils/disableRules.d.ts +6 -6
  20. package/package.json +4 -2
  21. package/dist/config/storybook.d.ts +0 -2
  22. package/dist/plugins/lumina/plugin.d.ts +0 -8
  23. package/dist/plugins/lumina/rules/add-missing-jsx-import.d.ts +0 -2
  24. package/dist/plugins/lumina/rules/auto-add-type.d.ts +0 -3
  25. package/dist/plugins/lumina/rules/ban-events.d.ts +0 -6
  26. package/dist/plugins/lumina/rules/component-placement-rules.d.ts +0 -2
  27. package/dist/plugins/lumina/rules/consistent-event-naming.d.ts +0 -15
  28. package/dist/plugins/lumina/rules/consistent-nullability.d.ts +0 -2
  29. package/dist/plugins/lumina/rules/decorators-context.d.ts +0 -2
  30. package/dist/plugins/lumina/rules/explicit-setter-type.d.ts +0 -19
  31. package/dist/plugins/lumina/rules/member-ordering/build.d.ts +0 -4
  32. package/dist/plugins/lumina/rules/member-ordering/comments.d.ts +0 -19
  33. package/dist/plugins/lumina/rules/member-ordering/config.d.ts +0 -36
  34. package/dist/plugins/lumina/rules/member-ordering/normalize.d.ts +0 -10
  35. package/dist/plugins/lumina/rules/member-ordering.d.ts +0 -2
  36. package/dist/plugins/lumina/rules/no-create-element-component.d.ts +0 -2
  37. package/dist/plugins/lumina/rules/no-ignore-jsdoc-tag.d.ts +0 -2
  38. package/dist/plugins/lumina/rules/no-incorrect-dynamic-tag-name.d.ts +0 -3
  39. package/dist/plugins/lumina/rules/no-inline-arrow-in-ref.d.ts +0 -2
  40. package/dist/plugins/lumina/rules/no-inline-exposure-jsdoc-tag.d.ts +0 -2
  41. package/dist/plugins/lumina/rules/no-invalid-directives-prop.d.ts +0 -2
  42. package/dist/plugins/lumina/rules/no-jsdoc-xref-links.d.ts +0 -2
  43. package/dist/plugins/lumina/rules/no-jsx-spread.d.ts +0 -2
  44. package/dist/plugins/lumina/rules/no-listen-in-connected-callback.d.ts +0 -2
  45. package/dist/plugins/lumina/rules/no-non-component-exports.d.ts +0 -2
  46. package/dist/plugins/lumina/rules/no-property-name-start-with-on.d.ts +0 -2
  47. package/dist/plugins/lumina/rules/no-render-false.d.ts +0 -3
  48. package/dist/plugins/lumina/rules/no-unnecessary-assertion-on-event.d.ts +0 -3
  49. package/dist/plugins/lumina/rules/no-unnecessary-attribute-name.d.ts +0 -2
  50. package/dist/plugins/lumina/rules/no-unnecessary-bind-this.d.ts +0 -2
  51. package/dist/plugins/lumina/rules/no-unnecessary-key.d.ts +0 -2
  52. package/dist/plugins/lumina/rules/tag-name-rules.d.ts +0 -8
  53. package/dist/plugins/lumina/utils/checker.d.ts +0 -4
  54. package/dist/plugins/lumina/utils/estree.d.ts +0 -33
  55. package/dist/plugins/lumina/utils/tags.d.ts +0 -14
  56. package/dist/plugins/utils/helpers.d.ts +0 -2
  57. package/dist/plugins/webgis/plugin.d.ts +0 -8
  58. package/dist/plugins/webgis/rules/consistent-logging.d.ts +0 -2
  59. package/dist/plugins/webgis/rules/no-dts-files.d.ts +0 -2
  60. package/dist/plugins/webgis/rules/no-import-outside-src.d.ts +0 -2
  61. package/dist/plugins/webgis/rules/no-story-render-args-type-annotation.d.ts +0 -2
  62. package/dist/plugins/webgis/rules/no-touching-jsdoc.d.ts +0 -2
  63. package/dist/plugins/webgis/rules/no-unsafe-hash-links.d.ts +0 -2
  64. package/dist/plugins/webgis/rules/require-js-in-imports.d.ts +0 -2
@@ -1,111 +1,164 @@
1
- import { m as makeEslintPlugin, e as extractDeclareElementsInterface, g as getComponentDeclaration } from "../../estree-DW92hBTd.js";
1
+ import { m as makeEslintPlugin } from "../../makePlugin-kNH-PFwP.js";
2
+ import { TSESTree, AST_TOKEN_TYPES, AST_NODE_TYPES, TSESLint } from "@typescript-eslint/utils";
3
+ import path from "node:path";
2
4
  import { resolve } from "path/posix";
3
- import { AST_NODE_TYPES } from "@typescript-eslint/utils";
5
+ import globals from "globals";
6
+ import { uncapitalize, kebabToPascal } from "@arcgis/toolkit/string";
7
+ import { e as extractDeclareElementsInterface, g as getComponentDeclaration } from "../../estree-CDc-die8.js";
4
8
  const plugin = makeEslintPlugin(
5
9
  "webgis",
6
10
  (rule) => `https://devtopia.esri.com/WebGIS/arcgis-web-components/tree/main/packages/support-packages/eslint-config/src/plugins/webgis/rules/${rule}.ts`
7
11
  );
8
12
  const isTestFile = (filePath) => filePath.includes("/test") || filePath.includes(".test") || filePath.includes(".spec") || filePath.includes("e2e") || filePath.includes("__") || filePath.includes("/.");
9
13
  const isStorybookFile = (filePath) => filePath.includes(".stories");
10
- const description$6 = `Imports of files outside the src/ folder are not-portable and likely to break for consumers of this package.`;
14
+ function hasPublicJsDocTag(sourceCode, node) {
15
+ const jsDocOwner = node.decorators?.at(0) ?? (node.parent?.type === TSESTree.AST_NODE_TYPES.ExportNamedDeclaration || node.parent?.type === TSESTree.AST_NODE_TYPES.ExportDefaultDeclaration ? node.parent : void 0) ?? node;
16
+ const commentTarget = jsDocOwner;
17
+ if (commentTarget === void 0) {
18
+ return false;
19
+ }
20
+ return sourceCode.getCommentsBefore(commentTarget).some((comment) => comment.type === AST_TOKEN_TYPES.Block && sourceCode.getText(comment).includes("* @public"));
21
+ }
11
22
  plugin.createRule({
12
- name: "no-import-outside-src",
23
+ name: "consistent-constructor-arguments",
13
24
  meta: {
14
25
  docs: {
15
- description: description$6,
16
- defaultLevel: "error"
26
+ description: "Consistent constructor arguments for Accessor-based classes.",
27
+ // Disabled by default because the rule is most relevant for autocasting
28
+ // and only on public classes. We are discouraging autocasting in new
29
+ // code.
30
+ defaultLevel: "off"
17
31
  },
18
32
  messages: {
19
- noImportOutsideSrc: description$6
33
+ name: `By convention, the first parameter on an Accessor-based class must be called "properties". This is needed for autocasting - https://webgis.esri.com/references/api-extractor/api-node-kinds#constructor. If your class has constructors overloads, at least one of them must have a "properties" parameter.`,
34
+ type: `By convention, the constructor argument type of Accessor-based classes must be called \`{{ expectedName }}\` (found \`{{ actualName }}\` instead).
35
+
36
+ If you wish to use a loose type like any/object, create a type alias \`type {{ expectedName }}\` and assign such loose type to that.`
20
37
  },
21
- type: "problem",
38
+ type: "suggestion",
22
39
  schema: []
23
40
  },
24
41
  defaultOptions: [],
25
42
  create(context) {
26
- const fileName = context.filename;
27
- const srcIndex = fileName.indexOf("/src/");
28
- if (srcIndex === -1) {
29
- return {};
30
- }
31
- const basePath = fileName.slice(0, srcIndex + "/src/".length);
32
- if (isTestFile(fileName) || isStorybookFile(fileName)) {
43
+ if (isTestFile(context.filename)) {
33
44
  return {};
34
45
  }
35
46
  return {
36
- ImportDeclaration(node) {
37
- const specifier = node.source.value;
38
- const isRelativeAncestor = specifier.startsWith("../");
39
- if (!isRelativeAncestor) {
47
+ ClassDeclaration(node) {
48
+ if (!hasPublicJsDocTag(context.sourceCode, node)) {
40
49
  return;
41
50
  }
42
- const relativePath = resolve(fileName, "..", specifier);
43
- if (!relativePath.startsWith(basePath) && !relativePath.endsWith(".json")) {
51
+ const hasSubclassDecorator = node.decorators?.some(
52
+ (decorator) => decorator.expression.type === TSESTree.AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === TSESTree.AST_NODE_TYPES.Identifier && decorator.expression.callee.name === "subclass"
53
+ );
54
+ const className = node.id?.name;
55
+ const isTopLevelClass = node.parent?.type === TSESTree.AST_NODE_TYPES.Program || node.parent?.type === TSESTree.AST_NODE_TYPES.ExportNamedDeclaration || node.parent?.type === TSESTree.AST_NODE_TYPES.ExportDefaultDeclaration;
56
+ if (!hasSubclassDecorator || className === void 0 || !isTopLevelClass) {
57
+ return;
58
+ }
59
+ let firstConstructor;
60
+ let hasPropertiesParameter = false;
61
+ node.body.body.forEach((element) => {
62
+ if (element.type !== TSESTree.AST_NODE_TYPES.MethodDefinition || element.kind !== "constructor") {
63
+ return;
64
+ }
65
+ firstConstructor ??= element;
66
+ const firstParameterNode = element.value.params.at(0);
67
+ const firstParameter = firstParameterNode?.type === TSESTree.AST_NODE_TYPES.AssignmentPattern ? firstParameterNode.left : firstParameterNode;
68
+ const isPropertiesParameter = firstParameter?.type === TSESTree.AST_NODE_TYPES.Identifier && firstParameter.name === "properties";
69
+ hasPropertiesParameter ||= isPropertiesParameter;
70
+ if (!isPropertiesParameter) {
71
+ return;
72
+ }
73
+ const typeAnnotation = firstParameter.typeAnnotation?.typeAnnotation;
74
+ const typeAnnotationName = typeAnnotation?.type === TSESTree.AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === TSESTree.AST_NODE_TYPES.Identifier ? typeAnnotation.typeName.name : void 0;
75
+ const expectedTypeName = `${className}Properties`;
76
+ if (typeAnnotationName === expectedTypeName) {
77
+ return;
78
+ }
79
+ const extendsText = node.superClass ? context.sourceCode.getText(node.superClass) : "";
80
+ const isCollectionClass = extendsText.includes("Collection.ofType") || extendsText.includes("(Collection)") || extendsText === "Collection" || extendsText === "OwningCollection" || context.filename.endsWith(`esri${path.sep}core${path.sep}Collection.ts`);
81
+ if (isCollectionClass) {
82
+ return;
83
+ }
84
+ const typeIsTypeParameter = node.typeParameters?.params.some(
85
+ (parameter) => parameter.name.name === typeAnnotationName
86
+ );
87
+ if (typeIsTypeParameter) {
88
+ return;
89
+ }
44
90
  context.report({
45
- messageId: "noImportOutsideSrc",
46
- node: node.source
91
+ node: typeAnnotation ?? firstParameter,
92
+ messageId: "type",
93
+ data: {
94
+ actualName: typeAnnotation === void 0 ? "<empty>" : context.sourceCode.getText(firstParameter.typeAnnotation?.typeAnnotation),
95
+ expectedName: expectedTypeName
96
+ }
47
97
  });
98
+ });
99
+ if (firstConstructor === void 0) {
100
+ return;
101
+ }
102
+ if (!hasPropertiesParameter) {
103
+ context.report({
104
+ node: firstConstructor,
105
+ messageId: "name"
106
+ });
107
+ return;
48
108
  }
49
109
  }
50
110
  };
51
111
  }
52
112
  });
53
- const description$5 = `Having two JSDoc comments next to each other is most likely a mistake - consider combining them into one, or separating them for clarity.`;
113
+ const description$6 = `Enforce consistent logging so that ArcGIS developers can easily debug errors or warnings logged by our web components, which may lack a meaningful context in compiled code. See [our documentation on @arcgis/toolkit/log](https://webgis.esri.com/references/toolkit/log).`;
54
114
  plugin.createRule({
55
- name: "no-touching-jsdoc",
115
+ name: "consistent-logging",
56
116
  meta: {
57
117
  docs: {
58
- description: description$5,
59
- defaultLevel: "warn"
118
+ description: description$6,
119
+ defaultLevel: "off"
60
120
  },
61
121
  messages: {
62
- noTouchingJsDoc: description$5
122
+ consistentLoggingWarning: `For consistency, use the log utility from @arcgis/toolkit/log instead of console.{{logMethodName}}.`
63
123
  },
64
- type: "problem",
65
- schema: []
124
+ schema: [],
125
+ fixable: void 0,
126
+ type: "suggestion"
66
127
  },
67
128
  defaultOptions: [],
68
129
  create(context) {
130
+ if (isTestFile(context.filename) || isStorybookFile(context.filename)) {
131
+ return {};
132
+ }
69
133
  return {
70
- Program() {
71
- Array.from(context.sourceCode.text.matchAll(reTouchingJsDoc), (match) => {
72
- const previousJsDocStart = context.sourceCode.text.slice(0, match.index).lastIndexOf("/**");
73
- if (previousJsDocStart === -1) {
74
- return void 0;
75
- }
76
- const previousJsDocContent = context.sourceCode.text.slice(
77
- previousJsDocStart + "/**".length,
78
- match.index - "*/".length
79
- );
80
- const containsOtherComments = previousJsDocContent.includes("*/");
81
- if (containsOtherComments) {
82
- return void 0;
83
- }
134
+ // NOTE: CallExpression is not enough. Use MemberExpression to handle callbacks, e.g. .catch(console.error)
135
+ MemberExpression(node) {
136
+ if (node.type === AST_NODE_TYPES.MemberExpression && node.object.type === AST_NODE_TYPES.Identifier && node.object.name === "console" && node.property.type === AST_NODE_TYPES.Identifier) {
137
+ const logMethodName = node.property.name;
84
138
  context.report({
85
- messageId: "noTouchingJsDoc",
86
- loc: {
87
- start: context.sourceCode.getLocFromIndex(match.index),
88
- end: context.sourceCode.getLocFromIndex(match.index + match[0].length)
139
+ node,
140
+ messageId: "consistentLoggingWarning",
141
+ data: {
142
+ logMethodName
89
143
  }
90
144
  });
91
- return void 0;
92
- });
145
+ }
93
146
  }
94
147
  };
95
148
  }
96
149
  });
97
- const reTouchingJsDoc = /\*\/\s+\/\*\*/gu;
98
- const description$4 = `@arcgis/core imports need to end with .js for better compatibility with ESM CDN builds for @arcgis/core and other packages.`;
99
- const packagesToEnforce = ["@arcgis/core/", "@amcharts/amcharts4/", "@amcharts/amcharts5/"];
100
150
  plugin.createRule({
101
- name: "require-js-in-imports",
151
+ name: "consistent-mixin-syntax",
102
152
  meta: {
103
153
  docs: {
104
- description: description$4,
105
- defaultLevel: "warn"
154
+ description: "Enforce consistent mixin pattern. See https://webgis.esri.com/sdk/contributing/core/core/mixins",
155
+ // Enabled by default because the rule targets a very specific pattern
156
+ // and that pattern is tricky to get right without this rule.
157
+ defaultLevel: "error"
106
158
  },
107
159
  messages: {
108
- requireJsInCoreImport: description$4
160
+ updateMixinCode: "Mixin typings need to be updated.",
161
+ typeAnnotationRequired: "Explicit type annotation is required."
109
162
  },
110
163
  type: "problem",
111
164
  fixable: "code",
@@ -113,43 +166,330 @@ plugin.createRule({
113
166
  },
114
167
  defaultOptions: [],
115
168
  create(context) {
116
- if (isTestFile(context.filename)) {
169
+ const sourceCode = context.sourceCode.getText();
170
+ const isMixin = sourceCode.includes("TBase");
171
+ if (!isMixin) {
117
172
  return {};
118
173
  }
119
- function updateSpecifier(node) {
120
- if (node.source.type !== AST_NODE_TYPES.Literal) {
121
- return;
122
- }
123
- const specifier = node.source.value;
124
- const lastSlashIndex = specifier?.toString().lastIndexOf("/") ?? 0;
125
- const dotSearchIndex = lastSlashIndex === -1 ? 0 : lastSlashIndex + 1;
126
- if (typeof specifier !== "string" || !packagesToEnforce.some((pkg) => specifier.startsWith(pkg)) || // Already ends with .js or .json
127
- specifier.includes(".", dotSearchIndex)) {
128
- return;
174
+ return {
175
+ VariableDeclarator(node) {
176
+ if (node.id.type !== AST_NODE_TYPES.Identifier || node.init?.type !== AST_NODE_TYPES.ArrowFunctionExpression) {
177
+ return;
178
+ }
179
+ const body = node.init.body;
180
+ if (body.type !== AST_NODE_TYPES.BlockStatement) {
181
+ return;
182
+ }
183
+ const firstTypeParameter = node.init.typeParameters?.params[0];
184
+ if (firstTypeParameter?.name.name !== "TBase") {
185
+ return;
186
+ }
187
+ let baseName = node.id.name;
188
+ if (baseName.startsWith("Exported")) {
189
+ baseName = baseName.slice("Exported".length);
190
+ }
191
+ if (baseName.endsWith("Mixin")) {
192
+ baseName = baseName.slice(0, -"Mixin".length);
193
+ }
194
+ const restTypeParameters = node.init.typeParameters?.params?.slice(1).map((param) => context.sourceCode.getText(param)).join(", ") ?? "";
195
+ const formattedRestTypeParameters = restTypeParameters.length === 0 ? "" : `<${restTypeParameters}>`;
196
+ const restTypeArguments = node.init.typeParameters?.params?.slice(1).map((param) => context.sourceCode.getText(param.name)).join(", ") ?? "";
197
+ const formattedRestTypeArguments = restTypeArguments.length === 0 ? "" : `<${restTypeArguments}>`;
198
+ {
199
+ const returnTypeCode = `typeof Public${baseName}${formattedRestTypeArguments} & TBase`;
200
+ if (node.init.returnType === void 0) {
201
+ const closeParenToken = context.sourceCode.getFirstToken(node.init, (token) => token.value === ")");
202
+ if (closeParenToken !== null) {
203
+ context.report({
204
+ node: node.init,
205
+ messageId: "updateMixinCode",
206
+ fix: (fixer) => {
207
+ const returnTypeText = `: ${returnTypeCode}`;
208
+ return fixer.insertTextAfter(closeParenToken, returnTypeText);
209
+ }
210
+ });
211
+ }
212
+ } else if (context.sourceCode.getText(node.init.returnType.typeAnnotation) !== returnTypeCode) {
213
+ const typeAnnotation = node.init.returnType.typeAnnotation;
214
+ context.report({
215
+ node: typeAnnotation,
216
+ messageId: "updateMixinCode",
217
+ fix: (fixer) => fixer.replaceText(typeAnnotation, returnTypeCode)
218
+ });
219
+ }
220
+ }
221
+ let hadMissingTypeError = false;
222
+ let hadTypedBaseVariable = false;
223
+ let privateClassName = baseName;
224
+ let isAbstract = false;
225
+ const members = [];
226
+ for (const statement of body.body) {
227
+ if (statement.type === AST_NODE_TYPES.VariableDeclaration && statement.declarations.length === 1 && statement.declarations[0].id.type === AST_NODE_TYPES.Identifier && statement.declarations[0].id.name === "TypedBase" && statement.declarations[0].init?.type === AST_NODE_TYPES.TSAsExpression) {
228
+ hadTypedBaseVariable = true;
229
+ } else if (statement.type === AST_NODE_TYPES.TSInterfaceDeclaration) {
230
+ for (const member of statement.body.body) {
231
+ members.push(context.sourceCode.getText(member));
232
+ }
233
+ } else if (statement.type === AST_NODE_TYPES.ClassDeclaration) {
234
+ privateClassName = statement.id.name;
235
+ isAbstract = statement.abstract;
236
+ {
237
+ const extendsPart = statement.superClass;
238
+ if (extendsPart === null) {
239
+ context.report({
240
+ node: statement,
241
+ messageId: "updateMixinCode",
242
+ fix: (fixer) => fixer.insertTextAfter(statement.id, ` extends TypedBase`)
243
+ });
244
+ } else if (context.sourceCode.getText(extendsPart) !== "TypedBase") {
245
+ context.report({
246
+ node: statement,
247
+ messageId: "updateMixinCode",
248
+ fix: (fixer) => fixer.replaceText(extendsPart, "TypedBase")
249
+ });
250
+ }
251
+ }
252
+ for (const member of statement.body.body) {
253
+ const isAbstractMethod = member.type === AST_NODE_TYPES.TSAbstractMethodDefinition;
254
+ const isAbstractProperty = member.type === AST_NODE_TYPES.TSAbstractPropertyDefinition;
255
+ if (member.type === AST_NODE_TYPES.MethodDefinition || isAbstractMethod) {
256
+ if (member.key.type === AST_NODE_TYPES.PrivateIdentifier) {
257
+ continue;
258
+ }
259
+ const memberName = context.sourceCode.getText(member.key);
260
+ const formattedMemberName = member.computed ? `[${memberName}]` : memberName;
261
+ if (memberName === "constructor") {
262
+ continue;
263
+ }
264
+ const modifiers = `${member.static ? "static " : ""}${member.accessibility ? `${member.accessibility} ` : ""}${isAbstractMethod ? "abstract " : ""}`;
265
+ const kindName = member.kind === "get" || member.kind === "set" ? `${member.kind} ` : "";
266
+ const functionString = functionToTypeString(
267
+ member.value,
268
+ context,
269
+ member.kind === "set" ? "set" : void 0
270
+ );
271
+ if (functionString === void 0) {
272
+ hadMissingTypeError = true;
273
+ continue;
274
+ }
275
+ members.push(`${modifiers}${kindName}${formattedMemberName}${functionString};`);
276
+ } else if (member.type === AST_NODE_TYPES.PropertyDefinition || isAbstractProperty) {
277
+ if (member.key.type === AST_NODE_TYPES.PrivateIdentifier) {
278
+ continue;
279
+ }
280
+ const memberName = context.sourceCode.getText(member.key);
281
+ const formattedMemberName = member.computed ? `[${memberName}]` : memberName;
282
+ const typeAnnotation = member.typeAnnotation?.typeAnnotation;
283
+ let type = typeAnnotation ? context.sourceCode.getText(typeAnnotation) : member.value === null ? void 0 : expressionToTypeString(member.value, context, false);
284
+ if (type === void 0) {
285
+ type = "any";
286
+ context.report({
287
+ node: member,
288
+ messageId: "typeAnnotationRequired"
289
+ });
290
+ hadMissingTypeError = true;
291
+ }
292
+ const modifiers = `${member.static ? "static " : ""}${member.accessibility ? `${member.accessibility} ` : ""}${isAbstractProperty ? "abstract " : ""}${member.readonly ? "readonly " : ""}`;
293
+ const optionalText = member.optional ? "?" : "";
294
+ members.push(`${modifiers}${formattedMemberName}${optionalText}: ${type};`);
295
+ }
296
+ }
297
+ } else if (statement.type === AST_NODE_TYPES.ReturnStatement) {
298
+ const returnText = context.sourceCode.getText(statement);
299
+ const expectedReturnText = `return ${privateClassName} as typeof Public${baseName}${formattedRestTypeArguments} & TBase;`;
300
+ if (returnText.replaceAll(reWhiteSpaceOrComma, "") !== expectedReturnText.replaceAll(reWhiteSpaceOrComma, "")) {
301
+ context.report({
302
+ node: statement,
303
+ messageId: "updateMixinCode",
304
+ fix: (fixer) => fixer.replaceText(statement, expectedReturnText)
305
+ });
306
+ }
307
+ }
308
+ }
309
+ const typeConstraint = firstTypeParameter.constraint;
310
+ const typeConstraintText = typeConstraint === void 0 ? "Constructor<Accessor>" : context.sourceCode.getText(typeConstraint);
311
+ if (typeConstraint !== void 0) {
312
+ const typeParameterText = context.sourceCode.getText(firstTypeParameter);
313
+ context.report({
314
+ node: typeConstraint,
315
+ messageId: "updateMixinCode",
316
+ fix: (fixer) => fixer.replaceText(firstTypeParameter, typeParameterText.split("extends")[0])
317
+ });
318
+ }
319
+ if (!hadTypedBaseVariable) {
320
+ context.report({
321
+ loc: body.loc.start,
322
+ messageId: "updateMixinCode",
323
+ fix: (fixer) => fixer.insertTextAfterRange(
324
+ [
325
+ context.sourceCode.getIndexFromLoc(body.loc.start) + 1,
326
+ context.sourceCode.getIndexFromLoc(body.loc.start) + 1
327
+ ],
328
+ `
329
+ const TypedBase = Base as unknown as ${typeConstraintText};`
330
+ )
331
+ });
332
+ }
333
+ if (hadMissingTypeError) {
334
+ return;
335
+ }
336
+ const localRegionStart = `${regionStart} ${baseName}`;
337
+ const abstractString = isAbstract ? "abstract " : "";
338
+ const autoGeneratedCode = `${localRegionStart}
339
+ declare ${abstractString}class Public${baseName}${formattedRestTypeParameters} {
340
+ constructor(...args: any[]);
341
+ ${members.map((member) => ` ${member}`).join("\n")}
342
+ }
343
+ ${regionEnd}`;
344
+ const regionStartIndex = sourceCode.indexOf(localRegionStart);
345
+ if (regionStartIndex === -1) {
346
+ context.report({
347
+ loc: node.loc.end,
348
+ messageId: "updateMixinCode",
349
+ fix: (fixer) => fixer.insertTextAfter(node, `
350
+
351
+ ${autoGeneratedCode}
352
+ `)
353
+ });
354
+ } else {
355
+ let regionEndString = regionEnd;
356
+ let regionEndIndex = sourceCode.indexOf(regionEndString, regionStartIndex);
357
+ if (regionEndIndex === -1) {
358
+ regionEndString = regionEndCrlf;
359
+ regionEndIndex = sourceCode.indexOf(regionEndString, regionStartIndex);
360
+ }
361
+ if (regionEndIndex === -1) {
362
+ regionEndIndex = sourceCode.length;
363
+ } else {
364
+ regionEndIndex += regionEndString.length;
365
+ }
366
+ const oldRegionCode = sourceCode.slice(regionStartIndex, regionEndIndex);
367
+ const oldRegionCodeWithoutWhitespace = oldRegionCode.replaceAll(reWhiteSpaceOrComma, "");
368
+ const newRegionCodeWithoutWhitespace = autoGeneratedCode.replaceAll(reWhiteSpaceOrComma, "");
369
+ if (oldRegionCodeWithoutWhitespace === newRegionCodeWithoutWhitespace) {
370
+ return;
371
+ }
372
+ const line = sourceCode.slice(0, regionStartIndex).split("\n").length;
373
+ context.report({
374
+ // Region start:
375
+ loc: {
376
+ line,
377
+ column: 0
378
+ },
379
+ messageId: "updateMixinCode",
380
+ fix: (fixer) => fixer.replaceTextRange([regionStartIndex, regionEndIndex], autoGeneratedCode)
381
+ });
382
+ }
129
383
  }
384
+ };
385
+ }
386
+ });
387
+ const reWhiteSpaceOrComma = /[\s,]+/gu;
388
+ const regionStart = "// #region Autogenerated";
389
+ const regionEnd = "// #endregion\n";
390
+ const regionEndCrlf = "// #endregion\r\n";
391
+ function functionToTypeString(definition, context, kind = void 0) {
392
+ let hadMissingTypeError = false;
393
+ const parameters = definition.params.map((param, index) => {
394
+ if (param.type === AST_NODE_TYPES.TSParameterProperty) {
395
+ throw new Error("Unsupported");
396
+ }
397
+ const name = param.type === AST_NODE_TYPES.RestElement && param.argument.type === AST_NODE_TYPES.Identifier ? `...${param.argument.name}` : param.type === AST_NODE_TYPES.Identifier ? param.name : param.type === AST_NODE_TYPES.AssignmentPattern && param.left.type === AST_NODE_TYPES.Identifier ? param.left.name : `_arg${index}`;
398
+ const optional = "optional" in param && param.optional || param.type === AST_NODE_TYPES.AssignmentPattern ? "?" : "";
399
+ const typeAnnotation = param.type === AST_NODE_TYPES.AssignmentPattern ? param?.left.typeAnnotation?.typeAnnotation : param.typeAnnotation?.typeAnnotation;
400
+ let type = typeAnnotation === void 0 ? param.type === AST_NODE_TYPES.AssignmentPattern ? expressionToTypeString(param.right, context, false) : void 0 : context.sourceCode.getText(typeAnnotation);
401
+ if (type === void 0) {
402
+ type = "any";
130
403
  context.report({
131
- node: node.source,
132
- messageId: "requireJsInCoreImport",
133
- fix: (fixer) => fixer.replaceText(node.source, `"${specifier}.js"`)
404
+ node: param,
405
+ messageId: "typeAnnotationRequired"
134
406
  });
407
+ hadMissingTypeError = true;
135
408
  }
136
- return {
137
- ImportDeclaration: updateSpecifier,
138
- ImportExpression: updateSpecifier
139
- };
409
+ return `${name}${optional}: ${type}`;
410
+ }).join(", ");
411
+ if (hadMissingTypeError) {
412
+ return;
140
413
  }
141
- });
142
- const description$3 = "Using .d.ts files is discouraged. Prefer .ts files instead, as they are type-checked and not in global scope.";
414
+ const returnType = kind === "set" ? "" : definition.returnType?.typeAnnotation === void 0 ? definition.body?.type === AST_NODE_TYPES.BlockStatement && definition.body.body.length === 1 && definition.body.body[0]?.type === AST_NODE_TYPES.ReturnStatement && definition.body.body[0].argument !== null ? expressionToTypeString(definition.body.body[0].argument, context, false) : void 0 : context.sourceCode.getText(definition.returnType.typeAnnotation);
415
+ if (returnType === void 0) {
416
+ context.report({
417
+ node: definition,
418
+ messageId: "typeAnnotationRequired"
419
+ });
420
+ return;
421
+ }
422
+ const returnToken = kind === "signature" ? " => " : ": ";
423
+ const formattedReturnType = returnType === "" ? "" : `${returnToken}${returnType}`;
424
+ const typeArguments = definition.typeParameters === void 0 ? "" : context.sourceCode.getText(definition.typeParameters);
425
+ return `${typeArguments}(${parameters})${formattedReturnType}`;
426
+ }
427
+ function expressionToTypeString(expression, context, asConst) {
428
+ const text = context.sourceCode.getText(expression);
429
+ if (text === "undefined") {
430
+ return text;
431
+ }
432
+ if (text === "true" || text === "false") {
433
+ return asConst ? text : "boolean";
434
+ }
435
+ if (expression.type === AST_NODE_TYPES.Literal) {
436
+ return asConst ? typeof expression.value === "string" ? JSON.stringify(expression.value) : expression.value?.toString() ?? "null" : typeof expression.value === "string" ? "string" : typeof expression.value === "number" ? "number" : typeof expression.value === "bigint" ? "bigint" : typeof expression.value === "boolean" ? "boolean" : typeof expression.value === "object" && expression.value !== null ? "RegExp" : "null";
437
+ }
438
+ if (expression.type === AST_NODE_TYPES.UnaryExpression && expression.operator === "-" && expression.argument.type === AST_NODE_TYPES.Literal) {
439
+ const value = expression.argument.value;
440
+ if (typeof value === "number" || typeof value === "bigint") {
441
+ return asConst ? `-${value}` : "number";
442
+ }
443
+ }
444
+ if (expression.type === AST_NODE_TYPES.TemplateLiteral) {
445
+ return "string";
446
+ }
447
+ if (expression.type === AST_NODE_TYPES.TSAsExpression || expression.type === AST_NODE_TYPES.TSTypeAssertion) {
448
+ const typeAnnotation = expression.typeAnnotation;
449
+ if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier) {
450
+ if (typeAnnotation.typeName.name === "const") {
451
+ return expressionToTypeString(expression.expression, context, true);
452
+ }
453
+ }
454
+ return context.sourceCode.getText(typeAnnotation);
455
+ }
456
+ if (expression.type === AST_NODE_TYPES.TSSatisfiesExpression) {
457
+ const typeAnnotation = expression.typeAnnotation;
458
+ return asConst ? expressionToTypeString(expression.expression, context, true) : context.sourceCode.getText(typeAnnotation);
459
+ }
460
+ if (expression.type === AST_NODE_TYPES.TSNonNullExpression) {
461
+ return expressionToTypeString(expression.expression, context, asConst);
462
+ }
463
+ if (expression.type === AST_NODE_TYPES.NewExpression && expression.callee.type === AST_NODE_TYPES.Identifier) {
464
+ return `${expression.callee.name}${expression.typeArguments ? context.sourceCode.getText(expression.typeArguments) : ""}`;
465
+ }
466
+ if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
467
+ const left = expressionToTypeString(expression.consequent, context, asConst);
468
+ const right = expressionToTypeString(expression.alternate, context, asConst);
469
+ if (left === void 0 || right === void 0) {
470
+ return;
471
+ }
472
+ return left === right ? left : `${left} | ${right}`;
473
+ }
474
+ if (expression.type === AST_NODE_TYPES.Identifier) {
475
+ return `typeof ${expression.name}`;
476
+ }
477
+ if (expression.type === AST_NODE_TYPES.FunctionExpression || expression.type === AST_NODE_TYPES.ArrowFunctionExpression) {
478
+ return functionToTypeString(expression, context, "signature");
479
+ }
480
+ return;
481
+ }
482
+ const description$5 = "Using .d.ts files is discouraged. Prefer .ts files instead, as they are type-checked and not in global scope.";
143
483
  const allowedNames = /* @__PURE__ */ new Set(["vite-env.d.ts", "components.d.ts"]);
144
484
  plugin.createRule({
145
485
  name: "no-dts-files",
146
486
  meta: {
147
487
  docs: {
148
- description: description$3,
488
+ description: description$5,
149
489
  defaultLevel: "warn"
150
490
  },
151
491
  messages: {
152
- avoidDtsFiles: description$3
492
+ avoidDtsFiles: description$5
153
493
  },
154
494
  type: "suggestion",
155
495
  schema: []
@@ -179,44 +519,315 @@ plugin.createRule({
179
519
  };
180
520
  }
181
521
  });
182
- const description$2 = `Enforce consistent logging so that ArcGIS developers can easily debug errors or warnings logged by our web components, which may lack a meaningful context in compiled code. See [our documentation on @arcgis/toolkit/log](https://webgis.esri.com/references/toolkit/log).`;
183
522
  plugin.createRule({
184
- name: "consistent-logging",
523
+ name: "no-empty-catch",
185
524
  meta: {
186
525
  docs: {
187
- description: description$2,
526
+ description: "Disallow empty catch calls since empty catch calls in a promise chain don't catch any exceptions",
527
+ // Enabled by default because this caught real bugs and has autofix
528
+ defaultLevel: "error"
529
+ },
530
+ messages: { emptyCatch: "no empty catch calls" },
531
+ schema: [],
532
+ type: "problem",
533
+ fixable: "code"
534
+ },
535
+ defaultOptions: [],
536
+ create(context) {
537
+ return {
538
+ "CallExpression[callee.property.name='catch'][arguments.length=0]"(node) {
539
+ const [, rangeEnd] = node.range;
540
+ context.report({
541
+ node,
542
+ messageId: "emptyCatch",
543
+ fix: (fixer) => fixer.insertTextBeforeRange([rangeEnd - 1, rangeEnd], "() => {}")
544
+ });
545
+ }
546
+ };
547
+ }
548
+ });
549
+ const description$4 = `Imports of files outside the src/ folder are not-portable and likely to break for consumers of this package.`;
550
+ plugin.createRule({
551
+ name: "no-import-outside-src",
552
+ meta: {
553
+ docs: {
554
+ description: description$4,
555
+ defaultLevel: "error"
556
+ },
557
+ messages: {
558
+ noImportOutsideSrc: description$4
559
+ },
560
+ type: "problem",
561
+ schema: []
562
+ },
563
+ defaultOptions: [],
564
+ create(context) {
565
+ const fileName = context.filename;
566
+ const srcIndex = fileName.indexOf("/src/");
567
+ if (srcIndex === -1) {
568
+ return {};
569
+ }
570
+ const basePath = fileName.slice(0, srcIndex + "/src/".length);
571
+ if (isTestFile(fileName) || isStorybookFile(fileName)) {
572
+ return {};
573
+ }
574
+ return {
575
+ ImportDeclaration(node) {
576
+ const specifier = node.source.value;
577
+ const isRelativeAncestor = specifier.startsWith("../");
578
+ if (!isRelativeAncestor) {
579
+ return;
580
+ }
581
+ const relativePath = resolve(fileName, "..", specifier);
582
+ if (!relativePath.startsWith(basePath) && !relativePath.endsWith(".json")) {
583
+ context.report({
584
+ messageId: "noImportOutsideSrc",
585
+ node: node.source
586
+ });
587
+ }
588
+ }
589
+ };
590
+ }
591
+ });
592
+ plugin.createRule({
593
+ name: "no-instanceof",
594
+ meta: {
595
+ docs: {
596
+ description: "Disallow `instanceof` outside an allowlist.",
597
+ // Disabling by default because this matters for
598
+ // lower-level libraries more than higher-level libraries or applications.
599
+ // Also, many false-positives.
188
600
  defaultLevel: "off"
189
- // NOTE: this is turned on conditionally in root eslint config
190
601
  },
602
+ schema: [
603
+ {
604
+ type: "object",
605
+ properties: {
606
+ allow: { type: "array", items: { type: "string" }, uniqueItems: true }
607
+ },
608
+ additionalProperties: false
609
+ }
610
+ ],
191
611
  messages: {
192
- consistentLoggingWarning: `For consistency, use the log utility from @arcgis/toolkit/log instead of console.{{logMethodName}}.`
612
+ instanceOf: "Avoid `instanceof` because it statically pulls the module defining the constructor into the bundle. Instead, use a Symbol together with a type guard function."
613
+ },
614
+ type: "problem"
615
+ },
616
+ defaultOptions: [{}],
617
+ create(context) {
618
+ const options = normalizeOptions(context.options[0]);
619
+ const sourceCode = context.sourceCode;
620
+ return {
621
+ "BinaryExpression[operator='instanceof']"(node) {
622
+ const right = node.right;
623
+ if (right.type === AST_NODE_TYPES.ThisExpression) {
624
+ return;
625
+ }
626
+ if (right.type !== AST_NODE_TYPES.Identifier) {
627
+ context.report({ node: right, messageId: "instanceOf" });
628
+ return;
629
+ }
630
+ const scope = sourceCode.getScope(node);
631
+ if (!isAllowedIdentifier(right.name, scope, options)) {
632
+ context.report({ node: right, messageId: "instanceOf" });
633
+ }
634
+ }
635
+ };
636
+ }
637
+ });
638
+ function normalizeOptions(options) {
639
+ return {
640
+ allowSet: new Set(options?.allow ?? [])
641
+ };
642
+ }
643
+ function resolveVariable(scope, name) {
644
+ for (let current = scope; current; current = current.upper) {
645
+ const variable = current.variables.find((entry) => entry.name === name);
646
+ if (variable) {
647
+ return variable;
648
+ }
649
+ }
650
+ return;
651
+ }
652
+ const builtInTypes = new Set(Object.keys({ ...globals.browser, ...globals.builtin }));
653
+ function isAllowedIdentifier(name, scope, options) {
654
+ if (options.allowSet.has(name)) {
655
+ return true;
656
+ }
657
+ const variable = resolveVariable(scope, name);
658
+ if (builtInTypes.has(name) && isUnshadowedBuiltIn(variable)) {
659
+ return true;
660
+ }
661
+ return isClassVariable(variable);
662
+ }
663
+ function isUnshadowedBuiltIn(variable) {
664
+ if (!variable) {
665
+ return true;
666
+ }
667
+ return variable.scope.type === TSESLint.Scope.ScopeType.global && variable.defs.length === 0;
668
+ }
669
+ function isClassVariable(variable) {
670
+ return variable?.defs.some((definition) => isClassDefinition(definition)) ?? false;
671
+ }
672
+ function isClassDefinition(definition) {
673
+ const definitionType = definition.type;
674
+ if (definitionType === TSESLint.Scope.DefinitionType.ClassName) {
675
+ return true;
676
+ }
677
+ if (definitionType !== TSESLint.Scope.DefinitionType.Variable) {
678
+ return false;
679
+ }
680
+ const variableNode = definition.node;
681
+ if (variableNode?.type !== AST_NODE_TYPES.VariableDeclarator) {
682
+ return false;
683
+ }
684
+ return variableNode.init?.type === AST_NODE_TYPES.ClassExpression;
685
+ }
686
+ plugin.createRule({
687
+ name: "no-kebab-case-props",
688
+ meta: {
689
+ docs: {
690
+ description: "Disallow kebab-case props on custom JSX elements.",
691
+ // Off by default till
692
+ // https://devtopia.esri.com/WebGIS/arcgis-web-components/issues/5364#issuecomment-6321339
693
+ // is resolved
694
+ defaultLevel: "off"
695
+ },
696
+ messages: {
697
+ kebab: "JSX props should use camelCase instead of kebab-case, except for data- or aria- attributes"
193
698
  },
194
699
  schema: [],
195
- fixable: void 0,
196
- type: "suggestion"
700
+ type: "problem",
701
+ fixable: "code"
197
702
  },
198
703
  defaultOptions: [],
199
704
  create(context) {
200
- if (isTestFile(context.filename) || isStorybookFile(context.filename)) {
705
+ return {
706
+ // looks for JSXAttributes that have a hyphen in their name
707
+ "JSXAttribute[name.name=/-/]"(node) {
708
+ const attrName = node.name.type === AST_NODE_TYPES.JSXIdentifier ? node.name.name : null;
709
+ if (!attrName || attrName.startsWith("aria-") || attrName.startsWith("data-")) {
710
+ return;
711
+ }
712
+ if (node.parent.name.type === AST_NODE_TYPES.JSXIdentifier) {
713
+ const tagName = node.parent.name.name;
714
+ if (isNativeTagName(tagName)) {
715
+ return;
716
+ }
717
+ }
718
+ context.report({
719
+ node,
720
+ messageId: "kebab",
721
+ fix: (fixer) => fixer.replaceText(node.name, uncapitalize(kebabToPascal(attrName)))
722
+ });
723
+ }
724
+ };
725
+ }
726
+ });
727
+ function isNativeTagName(tagName) {
728
+ const normalizedTagName = tagName.toLowerCase();
729
+ const isCapitalized = /^[A-Z]/u.test(tagName);
730
+ if (isCapitalized) {
731
+ return false;
732
+ }
733
+ return !normalizedTagName.includes("-") || blockListedCustomElementNames.has(normalizedTagName);
734
+ }
735
+ const blockListedCustomElementNames = /* @__PURE__ */ new Set([
736
+ "annotation-xml",
737
+ "color-profile",
738
+ "font-face",
739
+ "font-face-src",
740
+ "font-face-uri",
741
+ "font-face-format",
742
+ "font-face-name",
743
+ "missing-glyph"
744
+ ]);
745
+ const description$3 = "In Storybook render functions, let `args` infer from `Meta<T>` or `StoryObj<T>` instead of annotating the render parameter.";
746
+ plugin.createRule({
747
+ name: "no-story-render-args-type-annotation",
748
+ meta: {
749
+ docs: {
750
+ description: description$3,
751
+ defaultLevel: "off"
752
+ },
753
+ messages: {
754
+ noStoryRenderArgsTypeAnnotation: "Do not annotate `render(args)` in stories. Type the story with `Meta<T>` or `StoryObj<T>` and let `args` infer from that."
755
+ },
756
+ schema: [],
757
+ type: "suggestion",
758
+ fixable: "code"
759
+ },
760
+ defaultOptions: [],
761
+ create(context) {
762
+ if (!isStorybookFile(context.filename)) {
201
763
  return {};
202
764
  }
203
765
  return {
204
- // NOTE: CallExpression is not enough. Use MemberExpression to handle callbacks, e.g. .catch(console.error)
205
- MemberExpression(node) {
206
- if (node.type === AST_NODE_TYPES.MemberExpression && node.object.type === AST_NODE_TYPES.Identifier && node.object.name === "console" && node.property.type === AST_NODE_TYPES.Identifier) {
207
- const logMethodName = node.property.name;
766
+ Property(node) {
767
+ if (!isRenderProperty(node)) {
768
+ return;
769
+ }
770
+ const [firstParameter] = node.value.params;
771
+ if (firstParameter?.type !== AST_NODE_TYPES.Identifier || firstParameter.typeAnnotation === void 0) {
772
+ return;
773
+ }
774
+ context.report({
775
+ node: firstParameter.typeAnnotation,
776
+ messageId: "noStoryRenderArgsTypeAnnotation",
777
+ fix: (fixer) => fixer.remove(firstParameter.typeAnnotation)
778
+ });
779
+ }
780
+ };
781
+ }
782
+ });
783
+ function isRenderProperty(node) {
784
+ return node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "render" && (node.value.type === AST_NODE_TYPES.ArrowFunctionExpression || node.value.type === AST_NODE_TYPES.FunctionExpression);
785
+ }
786
+ const description$2 = `Having two JSDoc comments next to each other is most likely a mistake - consider combining them into one, or separating them for clarity.`;
787
+ plugin.createRule({
788
+ name: "no-touching-jsdoc",
789
+ meta: {
790
+ docs: {
791
+ description: description$2,
792
+ defaultLevel: "warn"
793
+ },
794
+ messages: {
795
+ noTouchingJsDoc: description$2
796
+ },
797
+ type: "problem",
798
+ schema: []
799
+ },
800
+ defaultOptions: [],
801
+ create(context) {
802
+ return {
803
+ Program() {
804
+ Array.from(context.sourceCode.text.matchAll(reTouchingJsDoc), (match) => {
805
+ const previousJsDocStart = context.sourceCode.text.slice(0, match.index).lastIndexOf("/**");
806
+ if (previousJsDocStart === -1) {
807
+ return void 0;
808
+ }
809
+ const previousJsDocContent = context.sourceCode.text.slice(
810
+ previousJsDocStart + "/**".length,
811
+ match.index - "*/".length
812
+ );
813
+ const containsOtherComments = previousJsDocContent.includes("*/");
814
+ if (containsOtherComments) {
815
+ return void 0;
816
+ }
208
817
  context.report({
209
- node,
210
- messageId: "consistentLoggingWarning",
211
- data: {
212
- logMethodName
818
+ messageId: "noTouchingJsDoc",
819
+ loc: {
820
+ start: context.sourceCode.getLocFromIndex(match.index),
821
+ end: context.sourceCode.getLocFromIndex(match.index + match[0].length)
213
822
  }
214
823
  });
215
- }
824
+ return void 0;
825
+ });
216
826
  }
217
827
  };
218
828
  }
219
829
  });
830
+ const reTouchingJsDoc = /\*\/\s+\/\*\*/gu;
220
831
  const description$1 = `Do not use links like [](#something) as they are not portable across .d.ts, and are not validated at build time. Use {@link } syntax, or absolute links. See https://webgis.esri.com/references/api-extractor/tags-reference#link)`;
221
832
  plugin.createRule({
222
833
  name: "no-unsafe-hash-links",
@@ -272,47 +883,111 @@ function computeReplacement(declareElementsInterface, anchor) {
272
883
  const tagName = declaration.key.value;
273
884
  return `components/${tagName}#${anchor}`;
274
885
  }
275
- const description = "In Storybook render functions, let `args` infer from `Meta<T>` or `StoryObj<T>` instead of annotating the render parameter.";
276
886
  plugin.createRule({
277
- name: "no-story-render-args-type-annotation",
887
+ name: "require-await-for-async-screenshot-assert",
278
888
  meta: {
279
889
  docs: {
280
- description,
281
- defaultLevel: "off"
282
- },
283
- messages: {
284
- noStoryRenderArgsTypeAnnotation: "Do not annotate `render(args)` in stories. Type the story with `Meta<T>` or `StoryObj<T>` and let `args` infer from that."
890
+ description: "Async tests must await a screenshot assertion.",
891
+ // Enabled by default because it targets a specific pattern and has autofix
892
+ defaultLevel: "error"
285
893
  },
894
+ messages: { asyncAssert: "Async tests must await a screenshot assertion." },
286
895
  schema: [],
287
- type: "suggestion",
896
+ type: "problem",
288
897
  fixable: "code"
289
898
  },
290
899
  defaultOptions: [],
291
900
  create(context) {
292
- if (!isStorybookFile(context.filename)) {
901
+ if (!isTestFile(context.filename)) {
293
902
  return {};
294
903
  }
295
904
  return {
296
- Property(node) {
297
- if (!isRenderProperty(node)) {
298
- return;
299
- }
300
- const [firstParameter] = node.value.params;
301
- if (firstParameter?.type !== AST_NODE_TYPES.Identifier || firstParameter.typeAnnotation === void 0) {
302
- return;
303
- }
905
+ "CallExpression[callee.name='test'] > ArrowFunctionExpression[async] ExpressionStatement > CallExpression[callee.object.name='assert'][callee.property.name='screenshot']"(node) {
906
+ const [rangeStart] = node.range;
304
907
  context.report({
305
- node: firstParameter.typeAnnotation,
306
- messageId: "noStoryRenderArgsTypeAnnotation",
307
- fix: (fixer) => fixer.remove(firstParameter.typeAnnotation)
908
+ node,
909
+ messageId: "asyncAssert",
910
+ fix: (fixer) => fixer.insertTextBeforeRange([rangeStart - 1, rangeStart], " await")
308
911
  });
309
912
  }
310
913
  };
311
914
  }
312
915
  });
313
- function isRenderProperty(node) {
314
- return node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "render" && (node.value.type === AST_NODE_TYPES.ArrowFunctionExpression || node.value.type === AST_NODE_TYPES.FunctionExpression);
315
- }
916
+ plugin.createRule({
917
+ name: "require-button-type",
918
+ meta: {
919
+ docs: {
920
+ description: "Prevent usage of button elements without an explicit type attribute.",
921
+ // Disabling by default for now because it found many cases (that
922
+ // presumably work correctly today) so enabling it is a bit too disruptive.
923
+ // We should coordinate enabling it with enabling other lint rules to
924
+ // announce at once.
925
+ defaultLevel: "off"
926
+ },
927
+ messages: { missingType: "An explicit type attribute is required on buttons" },
928
+ schema: [],
929
+ type: "problem",
930
+ fixable: "code"
931
+ },
932
+ defaultOptions: [],
933
+ create(context) {
934
+ return {
935
+ "JSXOpeningElement[name.name='button']"(node) {
936
+ if (!node.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === "type")) {
937
+ context.report({
938
+ node,
939
+ messageId: "missingType",
940
+ fix: (fixer) => fixer.insertTextAfter(node.name, ' type="button"')
941
+ });
942
+ }
943
+ }
944
+ };
945
+ }
946
+ });
947
+ const description = `@arcgis/core imports need to end with .js for better compatibility with ESM CDN builds for @arcgis/core and other packages.`;
948
+ const packagesToEnforce = ["@arcgis/core/", "@amcharts/amcharts4/", "@amcharts/amcharts5/"];
949
+ plugin.createRule({
950
+ name: "require-js-in-imports",
951
+ meta: {
952
+ docs: {
953
+ description,
954
+ defaultLevel: "warn"
955
+ },
956
+ messages: {
957
+ requireJsInCoreImport: description
958
+ },
959
+ type: "problem",
960
+ fixable: "code",
961
+ schema: []
962
+ },
963
+ defaultOptions: [],
964
+ create(context) {
965
+ if (isTestFile(context.filename)) {
966
+ return {};
967
+ }
968
+ function updateSpecifier(node) {
969
+ if (node.source.type !== AST_NODE_TYPES.Literal) {
970
+ return;
971
+ }
972
+ const specifier = node.source.value;
973
+ const lastSlashIndex = specifier?.toString().lastIndexOf("/") ?? 0;
974
+ const dotSearchIndex = lastSlashIndex === -1 ? 0 : lastSlashIndex + 1;
975
+ if (typeof specifier !== "string" || !packagesToEnforce.some((pkg) => specifier.startsWith(pkg)) || // Already ends with .js or .json
976
+ specifier.includes(".", dotSearchIndex)) {
977
+ return;
978
+ }
979
+ context.report({
980
+ node: node.source,
981
+ messageId: "requireJsInCoreImport",
982
+ fix: (fixer) => fixer.replaceText(node.source, `"${specifier}.js"`)
983
+ });
984
+ }
985
+ return {
986
+ ImportDeclaration: updateSpecifier,
987
+ ImportExpression: updateSpecifier
988
+ };
989
+ }
990
+ });
316
991
  const webgisPlugin = plugin.finalize();
317
992
  export {
318
993
  webgisPlugin