@arcgis/eslint-config 5.2.0-next.4 → 5.2.0-next.40

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