@trackunit/eslint-plugin-trackunit 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +117 -0
- package/package.json +31 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/config/fragments/ignores.d.ts +2 -0
- package/src/lib/config/fragments/ignores.js +18 -0
- package/src/lib/config/fragments/ignores.js.map +1 -0
- package/src/lib/config/fragments/import-rules.d.ts +3 -0
- package/src/lib/config/fragments/import-rules.js +58 -0
- package/src/lib/config/fragments/import-rules.js.map +1 -0
- package/src/lib/config/fragments/jest-overrides.d.ts +2 -0
- package/src/lib/config/fragments/jest-overrides.js +30 -0
- package/src/lib/config/fragments/jest-overrides.js.map +1 -0
- package/src/lib/config/fragments/jsdoc-rules.d.ts +3 -0
- package/src/lib/config/fragments/jsdoc-rules.js +71 -0
- package/src/lib/config/fragments/jsdoc-rules.js.map +1 -0
- package/src/lib/config/fragments/module-boundaries.d.ts +2 -0
- package/src/lib/config/fragments/module-boundaries.js +92 -0
- package/src/lib/config/fragments/module-boundaries.js.map +1 -0
- package/src/lib/config/fragments/react-rules.d.ts +5 -0
- package/src/lib/config/fragments/react-rules.js +137 -0
- package/src/lib/config/fragments/react-rules.js.map +1 -0
- package/src/lib/config/fragments/restricted-imports.d.ts +2 -0
- package/src/lib/config/fragments/restricted-imports.js +58 -0
- package/src/lib/config/fragments/restricted-imports.js.map +1 -0
- package/src/lib/config/fragments/testing-library.d.ts +2 -0
- package/src/lib/config/fragments/testing-library.js +7 -0
- package/src/lib/config/fragments/testing-library.js.map +1 -0
- package/src/lib/config/fragments/typescript-rules.d.ts +2 -0
- package/src/lib/config/fragments/typescript-rules.js +97 -0
- package/src/lib/config/fragments/typescript-rules.js.map +1 -0
- package/src/lib/config/index.d.ts +863 -0
- package/src/lib/config/index.js +10 -0
- package/src/lib/config/index.js.map +1 -0
- package/src/lib/config/plugins.d.ts +90 -0
- package/src/lib/config/plugins.js +44 -0
- package/src/lib/config/plugins.js.map +1 -0
- package/src/lib/config/presets/base.d.ts +265 -0
- package/src/lib/config/presets/base.js +145 -0
- package/src/lib/config/presets/base.js.map +1 -0
- package/src/lib/config/presets/e2e.d.ts +10 -0
- package/src/lib/config/presets/e2e.js +19 -0
- package/src/lib/config/presets/e2e.js.map +1 -0
- package/src/lib/config/presets/public-api.d.ts +147 -0
- package/src/lib/config/presets/public-api.js +62 -0
- package/src/lib/config/presets/public-api.js.map +1 -0
- package/src/lib/config/presets/react.d.ts +598 -0
- package/src/lib/config/presets/react.js +97 -0
- package/src/lib/config/presets/react.js.map +1 -0
- package/src/lib/config/presets/server.d.ts +36 -0
- package/src/lib/config/presets/server.js +37 -0
- package/src/lib/config/presets/server.js.map +1 -0
- package/src/lib/config/utils.d.ts +6 -0
- package/src/lib/config/utils.js +28 -0
- package/src/lib/config/utils.js.map +1 -0
- package/src/lib/config-helpers/create-skip-when.d.ts +35 -0
- package/src/lib/config-helpers/create-skip-when.js +54 -0
- package/src/lib/config-helpers/create-skip-when.js.map +1 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.d.ts +16 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js +83 -0
- package/src/lib/rules/cva-merge-base-classes-as-array/cva-merge-base-classes-as-array.js.map +1 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.d.ts +4 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js +297 -0
- package/src/lib/rules/design-guideline-button-icon-size-match/design-guideline-button-icon-size-match.js.map +1 -0
- package/src/lib/rules/no-internal-barrel-files/examples.d.ts +80 -0
- package/src/lib/rules/no-internal-barrel-files/examples.js +84 -0
- package/src/lib/rules/no-internal-barrel-files/examples.js.map +1 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.d.ts +29 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js +178 -0
- package/src/lib/rules/no-internal-barrel-files/no-internal-barrel-files.js.map +1 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.d.ts +5 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js +67 -0
- package/src/lib/rules/no-internal-graphql-when-tagged-with-gql-public/no-internal-graphql-when-tagged-with-gql-public.js.map +1 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.d.ts +2 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js +34 -0
- package/src/lib/rules/no-jest-mock-trackunit-react-core-hooks/no-jest-mock-trackunit-react-core-hooks.js.map +1 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.d.ts +16 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js +55 -0
- package/src/lib/rules/no-template-strings-in-classname-prop/no-template-strings-in-classname-prop.js.map +1 -0
- package/src/lib/rules/no-typescript-assertion/examples.d.ts +1 -0
- package/src/lib/rules/no-typescript-assertion/examples.js +45 -0
- package/src/lib/rules/no-typescript-assertion/examples.js.map +1 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.d.ts +20 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js +83 -0
- package/src/lib/rules/no-typescript-assertion/no-typescript-assertion.js.map +1 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.d.ts +73 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js +333 -0
- package/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.d.ts +56 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js +225 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/name-suggestion-strategies.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.d.ts +49 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js +75 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/prefer-event-specific-callback-naming.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.d.ts +32 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js +143 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/string-based.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.d.ts +27 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js +196 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/strategies/type-based.js.map +1 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.d.ts +76 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js +245 -0
- package/src/lib/rules/prefer-event-specific-callback-naming/utils.js.map +1 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.d.ts +4 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.js +289 -0
- package/src/lib/rules/prefer-field-components/prefer-field-components.js.map +1 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.d.ts +26 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js +402 -0
- package/src/lib/rules/prefer-mouse-event-handler-in-react-props/prefer-mouse-event-handler-in-react-props.js.map +1 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.d.ts +13 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js +271 -0
- package/src/lib/rules/require-classname-alternatives/require-classname-alternatives.js.map +1 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.d.ts +15 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js +245 -0
- package/src/lib/rules/require-list-item-virtualization-props/require-list-item-virtualization-props.js.map +1 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.d.ts +17 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js +133 -0
- package/src/lib/rules/require-optional-prop-initialization/require-optional-prop-initialization.js.map +1 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.d.ts +12 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js +128 -0
- package/src/lib/rules/require-optional-prop-initialization/suggestion-utils.js.map +1 -0
- package/src/lib/rules-map.d.ts +66 -0
- package/src/lib/rules-map.js +34 -0
- package/src/lib/rules-map.js.map +1 -0
- package/src/lib/utils/ast-utils.d.ts +85 -0
- package/src/lib/utils/ast-utils.js +530 -0
- package/src/lib/utils/ast-utils.js.map +1 -0
- package/src/lib/utils/classname-utils.d.ts +150 -0
- package/src/lib/utils/classname-utils.js +492 -0
- package/src/lib/utils/classname-utils.js.map +1 -0
- package/src/lib/utils/file-utils.d.ts +14 -0
- package/src/lib/utils/file-utils.js +106 -0
- package/src/lib/utils/file-utils.js.map +1 -0
- package/src/lib/utils/import-utils.d.ts +85 -0
- package/src/lib/utils/import-utils.js +193 -0
- package/src/lib/utils/import-utils.js.map +1 -0
- package/src/lib/utils/nx-utils.d.ts +59 -0
- package/src/lib/utils/nx-utils.js +103 -0
- package/src/lib/utils/nx-utils.js.map +1 -0
- package/src/lib/utils/package-utils.d.ts +38 -0
- package/src/lib/utils/package-utils.js +74 -0
- package/src/lib/utils/package-utils.js.map +1 -0
- package/src/lib/utils/typescript-utils.d.ts +29 -0
- package/src/lib/utils/typescript-utils.js +213 -0
- package/src/lib/utils/typescript-utils.js.map +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce destructured imports instead of namespace imports for configured packages.
|
|
3
|
+
* This rule helps maintain consistent import styles by converting namespace imports and detecting global UMD usage.
|
|
4
|
+
* @example
|
|
5
|
+
* // ❌ Bad (namespace import)
|
|
6
|
+
* import React from "react";
|
|
7
|
+
* const Component = () => {
|
|
8
|
+
* React.useEffect(() => {}, []);
|
|
9
|
+
* return <div />;
|
|
10
|
+
* };
|
|
11
|
+
*
|
|
12
|
+
* // ✅ Good (destructured import)
|
|
13
|
+
* import { useEffect } from "react";
|
|
14
|
+
* const Component = () => {
|
|
15
|
+
* useEffect(() => {}, []);
|
|
16
|
+
* return <div />;
|
|
17
|
+
* };
|
|
18
|
+
* @example
|
|
19
|
+
* // ❌ Bad (global UMD usage without import)
|
|
20
|
+
* interface Props {
|
|
21
|
+
* children: React.ReactNode;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // ✅ Good (explicit import)
|
|
25
|
+
* import { ReactNode } from "react";
|
|
26
|
+
* interface Props {
|
|
27
|
+
* children: ReactNode;
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
31
|
+
type MessageIds = "preferDestructured" | "preferDestructuredGlobal";
|
|
32
|
+
type PackageConfig = {
|
|
33
|
+
/** Mapping of package names to their global UMD variable names (e.g., { "react": "React", "lodash": "_" }) */
|
|
34
|
+
packages: Record<string, string>;
|
|
35
|
+
};
|
|
36
|
+
type Options = [PackageConfig?];
|
|
37
|
+
/**
|
|
38
|
+
* ESLint rule: prefer-destructured-imports
|
|
39
|
+
*
|
|
40
|
+
* ## Configuration
|
|
41
|
+
*
|
|
42
|
+
* This rule requires explicit configuration to specify which packages to enforce.
|
|
43
|
+
* Without configuration, the rule does nothing.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // .eslintrc.js - Enforce for React
|
|
47
|
+
* {
|
|
48
|
+
* "rules": {
|
|
49
|
+
* "@trackunit/prefer-destructured-imports": ["error", {
|
|
50
|
+
* "packages": {
|
|
51
|
+
* "react": "React"
|
|
52
|
+
* }
|
|
53
|
+
* }]
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* @example
|
|
57
|
+
* // .eslintrc.js - Multiple packages
|
|
58
|
+
* {
|
|
59
|
+
* "rules": {
|
|
60
|
+
* "@trackunit/prefer-destructured-imports": ["error", {
|
|
61
|
+
* "packages": {
|
|
62
|
+
* "react": "React",
|
|
63
|
+
* "lodash": "_",
|
|
64
|
+
* "jquery": "$"
|
|
65
|
+
* }
|
|
66
|
+
* }]
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
export declare const preferDestructuredImports: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
71
|
+
name: string;
|
|
72
|
+
};
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Enforce destructured imports instead of namespace imports for configured packages.
|
|
4
|
+
* This rule helps maintain consistent import styles by converting namespace imports and detecting global UMD usage.
|
|
5
|
+
* @example
|
|
6
|
+
* // ❌ Bad (namespace import)
|
|
7
|
+
* import React from "react";
|
|
8
|
+
* const Component = () => {
|
|
9
|
+
* React.useEffect(() => {}, []);
|
|
10
|
+
* return <div />;
|
|
11
|
+
* };
|
|
12
|
+
*
|
|
13
|
+
* // ✅ Good (destructured import)
|
|
14
|
+
* import { useEffect } from "react";
|
|
15
|
+
* const Component = () => {
|
|
16
|
+
* useEffect(() => {}, []);
|
|
17
|
+
* return <div />;
|
|
18
|
+
* };
|
|
19
|
+
* @example
|
|
20
|
+
* // ❌ Bad (global UMD usage without import)
|
|
21
|
+
* interface Props {
|
|
22
|
+
* children: React.ReactNode;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* // ✅ Good (explicit import)
|
|
26
|
+
* import { ReactNode } from "react";
|
|
27
|
+
* interface Props {
|
|
28
|
+
* children: ReactNode;
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.preferDestructuredImports = void 0;
|
|
33
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
34
|
+
const import_utils_1 = require("../../utils/import-utils");
|
|
35
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}.ts`);
|
|
36
|
+
/**
|
|
37
|
+
* ESLint rule: prefer-destructured-imports
|
|
38
|
+
*
|
|
39
|
+
* ## Configuration
|
|
40
|
+
*
|
|
41
|
+
* This rule requires explicit configuration to specify which packages to enforce.
|
|
42
|
+
* Without configuration, the rule does nothing.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // .eslintrc.js - Enforce for React
|
|
46
|
+
* {
|
|
47
|
+
* "rules": {
|
|
48
|
+
* "@trackunit/prefer-destructured-imports": ["error", {
|
|
49
|
+
* "packages": {
|
|
50
|
+
* "react": "React"
|
|
51
|
+
* }
|
|
52
|
+
* }]
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* @example
|
|
56
|
+
* // .eslintrc.js - Multiple packages
|
|
57
|
+
* {
|
|
58
|
+
* "rules": {
|
|
59
|
+
* "@trackunit/prefer-destructured-imports": ["error", {
|
|
60
|
+
* "packages": {
|
|
61
|
+
* "react": "React",
|
|
62
|
+
* "lodash": "_",
|
|
63
|
+
* "jquery": "$"
|
|
64
|
+
* }
|
|
65
|
+
* }]
|
|
66
|
+
* }
|
|
67
|
+
* }
|
|
68
|
+
*/
|
|
69
|
+
exports.preferDestructuredImports = createRule({
|
|
70
|
+
name: "prefer-destructured-imports",
|
|
71
|
+
meta: {
|
|
72
|
+
type: "suggestion",
|
|
73
|
+
docs: {
|
|
74
|
+
description: "Prefer destructured imports instead of namespace imports for certain packages",
|
|
75
|
+
},
|
|
76
|
+
fixable: "code",
|
|
77
|
+
schema: [
|
|
78
|
+
{
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
packages: {
|
|
82
|
+
type: "object",
|
|
83
|
+
description: "Mapping of package names to their global UMD variable names (e.g., { 'react': 'React' })",
|
|
84
|
+
additionalProperties: {
|
|
85
|
+
type: "string",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
messages: {
|
|
93
|
+
preferDestructured: "Prefer destructured imports instead of namespace imports",
|
|
94
|
+
preferDestructuredGlobal: "Import '{{members}}' from '{{packageName}}' instead of using the global {{globalName}} variable",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
defaultOptions: [],
|
|
98
|
+
create(context) {
|
|
99
|
+
const sourceCode = context.sourceCode;
|
|
100
|
+
const options = context.options[0];
|
|
101
|
+
// Return early if no packages are configured
|
|
102
|
+
if (options === undefined || Object.keys(options.packages).length === 0) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
const packageToGlobalName = options.packages;
|
|
106
|
+
const targetPackages = Object.keys(packageToGlobalName);
|
|
107
|
+
// Helper to check if a string is a valid/configured package name
|
|
108
|
+
const isValidPackageName = (value) => {
|
|
109
|
+
return targetPackages.includes(value);
|
|
110
|
+
};
|
|
111
|
+
// Track which global identifiers are actually imported (to avoid false positives)
|
|
112
|
+
const importedGlobalIdentifiers = new Set();
|
|
113
|
+
const namespaceImports = new Map();
|
|
114
|
+
const usagesByImport = new Map();
|
|
115
|
+
// Track usages of global identifiers that aren't imported (e.g., React.ReactElement without importing React)
|
|
116
|
+
const globalUsages = new Map();
|
|
117
|
+
return {
|
|
118
|
+
ImportDeclaration(node) {
|
|
119
|
+
const sourceValue = node.source.value;
|
|
120
|
+
if (typeof sourceValue === "string" && isValidPackageName(sourceValue)) {
|
|
121
|
+
const globalName = packageToGlobalName[sourceValue];
|
|
122
|
+
// Get any existing destructured imports
|
|
123
|
+
const existingSpecifiers = new Set(node.specifiers
|
|
124
|
+
.filter((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier)
|
|
125
|
+
.map(s => s.local.name));
|
|
126
|
+
// Find the namespace or default import if it exists
|
|
127
|
+
const namespaceOrDefaultSpecifier = node.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier || s.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
|
|
128
|
+
if (namespaceOrDefaultSpecifier) {
|
|
129
|
+
const importName = namespaceOrDefaultSpecifier.local.name;
|
|
130
|
+
// Only mark this global as imported if the import name matches the global name
|
|
131
|
+
// This prevents false positives when someone imports React as a different name
|
|
132
|
+
if (importName === globalName) {
|
|
133
|
+
importedGlobalIdentifiers.add(globalName);
|
|
134
|
+
}
|
|
135
|
+
// If this import also has destructured specifiers, store it as both
|
|
136
|
+
if (existingSpecifiers.size > 0) {
|
|
137
|
+
namespaceImports.set("destructured", { node, existingSpecifiers });
|
|
138
|
+
}
|
|
139
|
+
namespaceImports.set(importName, { node, existingSpecifiers });
|
|
140
|
+
usagesByImport.set(importName, new Map());
|
|
141
|
+
}
|
|
142
|
+
else if (existingSpecifiers.size > 0) {
|
|
143
|
+
// Store pure destructured imports
|
|
144
|
+
namespaceImports.set("destructured", { node, existingSpecifiers });
|
|
145
|
+
// Do NOT mark the global as imported - we want to catch global usage even with destructured imports
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
// Handle React.something in regular code
|
|
150
|
+
MemberExpression(node) {
|
|
151
|
+
const objectNode = node.object;
|
|
152
|
+
if (objectNode.type === utils_1.AST_NODE_TYPES.Identifier && node.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
153
|
+
// Check if this is a tracked namespace import
|
|
154
|
+
if (namespaceImports.has(objectNode.name)) {
|
|
155
|
+
const usages = usagesByImport.get(objectNode.name);
|
|
156
|
+
if (usages) {
|
|
157
|
+
usages.set(node.range[0], {
|
|
158
|
+
name: node.property.name,
|
|
159
|
+
start: node.range[0],
|
|
160
|
+
end: node.range[1],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Check if this is a global identifier usage (e.g., React.ReactElement without import)
|
|
165
|
+
else if (!importedGlobalIdentifiers.has(objectNode.name)) {
|
|
166
|
+
// Find if this identifier matches any of our configured global names
|
|
167
|
+
for (const packageName of targetPackages) {
|
|
168
|
+
const globalName = packageToGlobalName[packageName];
|
|
169
|
+
if (objectNode.name === globalName) {
|
|
170
|
+
if (!globalUsages.has(globalName)) {
|
|
171
|
+
globalUsages.set(globalName, {
|
|
172
|
+
packageName: packageName,
|
|
173
|
+
members: new Set(),
|
|
174
|
+
nodes: [],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const usage = globalUsages.get(globalName);
|
|
178
|
+
if (usage) {
|
|
179
|
+
usage.members.add(node.property.name);
|
|
180
|
+
usage.nodes.push(node);
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
// Handle React.ReactNode in type annotations
|
|
189
|
+
TSQualifiedName(node) {
|
|
190
|
+
if (node.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
191
|
+
// Check if this is a tracked namespace import
|
|
192
|
+
if (namespaceImports.has(node.left.name)) {
|
|
193
|
+
const usages = usagesByImport.get(node.left.name);
|
|
194
|
+
if (usages) {
|
|
195
|
+
usages.set(node.range[0], {
|
|
196
|
+
name: node.right.name,
|
|
197
|
+
start: node.range[0],
|
|
198
|
+
end: node.range[1],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Check if this is a global identifier usage
|
|
203
|
+
else if (!importedGlobalIdentifiers.has(node.left.name)) {
|
|
204
|
+
for (const packageName of targetPackages) {
|
|
205
|
+
const globalName = packageToGlobalName[packageName];
|
|
206
|
+
if (node.left.name === globalName) {
|
|
207
|
+
if (!globalUsages.has(globalName)) {
|
|
208
|
+
globalUsages.set(globalName, {
|
|
209
|
+
packageName: packageName,
|
|
210
|
+
members: new Set(),
|
|
211
|
+
nodes: [],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const usage = globalUsages.get(globalName);
|
|
215
|
+
if (usage) {
|
|
216
|
+
usage.members.add(node.right.name);
|
|
217
|
+
usage.nodes.push(node);
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
// Note: TSQualifiedName already handles React.ReactElement, React.RefObject, etc.
|
|
226
|
+
// This visitor is kept for potential future expansion but currently does nothing
|
|
227
|
+
// to avoid duplicate tracking with TSQualifiedName
|
|
228
|
+
TSTypeReference(_node) {
|
|
229
|
+
// TSQualifiedName visitor handles all qualified type names (React.Something)
|
|
230
|
+
// No action needed here
|
|
231
|
+
},
|
|
232
|
+
"Program:exit"() {
|
|
233
|
+
// Report errors for namespace imports
|
|
234
|
+
for (const [importName, { node: importNode }] of namespaceImports) {
|
|
235
|
+
const usages = usagesByImport.get(importName);
|
|
236
|
+
if (!usages || usages.size === 0) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const sourceValue = importNode.source.value;
|
|
240
|
+
if (typeof sourceValue !== "string" || !isValidPackageName(sourceValue)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
context.report({
|
|
244
|
+
node: importNode,
|
|
245
|
+
messageId: "preferDestructured",
|
|
246
|
+
fix(fixer) {
|
|
247
|
+
const fixes = [];
|
|
248
|
+
// Collect all specifiers needed from usages (e.g., ["useState", "useEffect"] from React.useState, React.useEffect)
|
|
249
|
+
const specifiersFromUsages = Array.from(usages.values()).map(usage => usage.name);
|
|
250
|
+
// Check if there's a separate destructured import to merge with
|
|
251
|
+
const existingDestructured = namespaceImports.get("destructured");
|
|
252
|
+
const otherDestructuredImport = existingDestructured && existingDestructured.node !== importNode
|
|
253
|
+
? existingDestructured.node
|
|
254
|
+
: undefined;
|
|
255
|
+
// Replace the namespace import with destructured imports (merges if other import exists)
|
|
256
|
+
fixes.push(...(0, import_utils_1.replaceNamespaceWithDestructured)({
|
|
257
|
+
sourceCode,
|
|
258
|
+
fixer,
|
|
259
|
+
packageName: sourceValue,
|
|
260
|
+
namespaceImportToReplace: importNode,
|
|
261
|
+
specifiersToAdd: specifiersFromUsages,
|
|
262
|
+
mergeIntoImport: otherDestructuredImport,
|
|
263
|
+
}));
|
|
264
|
+
// Remove namespace/default prefix from all usages
|
|
265
|
+
for (const { start, end } of usages.values()) {
|
|
266
|
+
const text = sourceCode.getText().slice(start, end);
|
|
267
|
+
const newText = text.replace(new RegExp(`^${importName}\\.`), "");
|
|
268
|
+
fixes.push(fixer.replaceTextRange([start, end], newText));
|
|
269
|
+
}
|
|
270
|
+
return fixes;
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// Report errors for global UMD variable usages (e.g., React.ReactElement without import)
|
|
275
|
+
for (const [globalName, { packageName, members, nodes }] of globalUsages) {
|
|
276
|
+
if (members.size === 0 || nodes.length === 0) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Report on the first usage node
|
|
280
|
+
const firstNode = nodes[0];
|
|
281
|
+
if (!firstNode) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const membersList = Array.from(members);
|
|
285
|
+
const existingDestructured = namespaceImports.get("destructured");
|
|
286
|
+
// Filter out members that are already imported (no need to add them again)
|
|
287
|
+
const membersToAdd = existingDestructured
|
|
288
|
+
? membersList.filter(member => !existingDestructured.existingSpecifiers.has(member))
|
|
289
|
+
: membersList;
|
|
290
|
+
context.report({
|
|
291
|
+
node: firstNode,
|
|
292
|
+
messageId: "preferDestructuredGlobal",
|
|
293
|
+
data: {
|
|
294
|
+
globalName,
|
|
295
|
+
packageName,
|
|
296
|
+
members: membersList.sort().join(", "), // Sort only for display in error message
|
|
297
|
+
},
|
|
298
|
+
fix(fixer) {
|
|
299
|
+
const fixes = [];
|
|
300
|
+
// Only update imports if there are new members to add
|
|
301
|
+
if (membersToAdd.length > 0) {
|
|
302
|
+
fixes.push(...(0, import_utils_1.addImportSpecifiers)({
|
|
303
|
+
sourceCode,
|
|
304
|
+
fixer,
|
|
305
|
+
packageName,
|
|
306
|
+
specifiers: membersToAdd,
|
|
307
|
+
existingImport: existingDestructured?.node,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
// Replace all global usages (React.X → X)
|
|
311
|
+
// Deduplicate nodes by range and sort by position (reverse order to maintain positions)
|
|
312
|
+
const uniqueNodesByRange = new Map();
|
|
313
|
+
for (const node of nodes) {
|
|
314
|
+
const rangeKey = `${node.range[0]}-${node.range[1]}`;
|
|
315
|
+
if (!uniqueNodesByRange.has(rangeKey)) {
|
|
316
|
+
uniqueNodesByRange.set(rangeKey, node);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const sortedNodes = Array.from(uniqueNodesByRange.values()).sort((a, b) => b.range[0] - a.range[0]);
|
|
320
|
+
for (const node of sortedNodes) {
|
|
321
|
+
const nodeText = sourceCode.getText(node);
|
|
322
|
+
const newText = nodeText.replace(new RegExp(`^${globalName}\\.`), "");
|
|
323
|
+
fixes.push(fixer.replaceText(node, newText));
|
|
324
|
+
}
|
|
325
|
+
return fixes;
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
//# sourceMappingURL=prefer-destructured-imports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-destructured-imports.js","sourceRoot":"","sources":["../../../../../../../../libs/eslint/plugin-trackunit/src/lib/rules/prefer-destructured-imports/prefer-destructured-imports.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Enforce destructured imports instead of namespace imports for configured packages.\n * This rule helps maintain consistent import styles by converting namespace imports and detecting global UMD usage.\n * @example\n * // ❌ Bad (namespace import)\n * import React from \"react\";\n * const Component = () => {\n * React.useEffect(() => {}, []);\n * return <div />;\n * };\n *\n * // ✅ Good (destructured import)\n * import { useEffect } from \"react\";\n * const Component = () => {\n * useEffect(() => {}, []);\n * return <div />;\n * };\n * @example\n * // ❌ Bad (global UMD usage without import)\n * interface Props {\n * children: React.ReactNode;\n * }\n *\n * // ✅ Good (explicit import)\n * import { ReactNode } from \"react\";\n * interface Props {\n * children: ReactNode;\n * }\n */\n\nimport { AST_NODE_TYPES, ESLintUtils, TSESTree } from \"@typescript-eslint/utils\";\nimport { addImportSpecifiers, replaceNamespaceWithDestructured } from \"../../utils/import-utils\";\n\nconst createRule = ESLintUtils.RuleCreator(\n name => `https://github.com/trackunit/manager/blob/main/libs/eslint/plugin-trackunit/src/lib/rules/${name}.ts`\n);\n\ntype MessageIds = \"preferDestructured\" | \"preferDestructuredGlobal\";\n\ntype PackageConfig = {\n /** Mapping of package names to their global UMD variable names (e.g., { \"react\": \"React\", \"lodash\": \"_\" }) */\n packages: Record<string, string>;\n};\n\ntype Options = [PackageConfig?];\n\n/**\n * ESLint rule: prefer-destructured-imports\n *\n * ## Configuration\n *\n * This rule requires explicit configuration to specify which packages to enforce.\n * Without configuration, the rule does nothing.\n *\n * @example\n * // .eslintrc.js - Enforce for React\n * {\n * \"rules\": {\n * \"@trackunit/prefer-destructured-imports\": [\"error\", {\n * \"packages\": {\n * \"react\": \"React\"\n * }\n * }]\n * }\n * }\n * @example\n * // .eslintrc.js - Multiple packages\n * {\n * \"rules\": {\n * \"@trackunit/prefer-destructured-imports\": [\"error\", {\n * \"packages\": {\n * \"react\": \"React\",\n * \"lodash\": \"_\",\n * \"jquery\": \"$\"\n * }\n * }]\n * }\n * }\n */\nexport const preferDestructuredImports = createRule<Options, MessageIds>({\n name: \"prefer-destructured-imports\",\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Prefer destructured imports instead of namespace imports for certain packages\",\n },\n fixable: \"code\",\n schema: [\n {\n type: \"object\",\n properties: {\n packages: {\n type: \"object\",\n description: \"Mapping of package names to their global UMD variable names (e.g., { 'react': 'React' })\",\n additionalProperties: {\n type: \"string\",\n },\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n preferDestructured: \"Prefer destructured imports instead of namespace imports\",\n preferDestructuredGlobal:\n \"Import '{{members}}' from '{{packageName}}' instead of using the global {{globalName}} variable\",\n },\n },\n defaultOptions: [],\n create(context) {\n const sourceCode = context.sourceCode;\n const options = context.options[0];\n\n // Return early if no packages are configured\n if (options === undefined || Object.keys(options.packages).length === 0) {\n return {};\n }\n\n const packageToGlobalName = options.packages;\n const targetPackages = Object.keys(packageToGlobalName);\n\n // Helper to check if a string is a valid/configured package name\n const isValidPackageName = (value: string): boolean => {\n return targetPackages.includes(value);\n };\n\n // Track which global identifiers are actually imported (to avoid false positives)\n const importedGlobalIdentifiers = new Set<string>();\n\n const namespaceImports = new Map<string, { node: TSESTree.ImportDeclaration; existingSpecifiers: Set<string> }>();\n const usagesByImport = new Map<string, Map<number, { name: string; start: number; end: number }>>();\n\n // Track usages of global identifiers that aren't imported (e.g., React.ReactElement without importing React)\n const globalUsages = new Map<string, { packageName: string; members: Set<string>; nodes: Array<TSESTree.Node> }>();\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n const sourceValue = node.source.value;\n if (typeof sourceValue === \"string\" && isValidPackageName(sourceValue)) {\n const globalName = packageToGlobalName[sourceValue];\n\n // Get any existing destructured imports\n const existingSpecifiers = new Set(\n node.specifiers\n .filter((s): s is TSESTree.ImportSpecifier => s.type === AST_NODE_TYPES.ImportSpecifier)\n .map(s => s.local.name)\n );\n\n // Find the namespace or default import if it exists\n const namespaceOrDefaultSpecifier = node.specifiers.find(\n (s): s is TSESTree.ImportDefaultSpecifier | TSESTree.ImportNamespaceSpecifier =>\n s.type === AST_NODE_TYPES.ImportDefaultSpecifier || s.type === AST_NODE_TYPES.ImportNamespaceSpecifier\n );\n\n if (namespaceOrDefaultSpecifier) {\n const importName = namespaceOrDefaultSpecifier.local.name;\n // Only mark this global as imported if the import name matches the global name\n // This prevents false positives when someone imports React as a different name\n if (importName === globalName) {\n importedGlobalIdentifiers.add(globalName);\n }\n\n // If this import also has destructured specifiers, store it as both\n if (existingSpecifiers.size > 0) {\n namespaceImports.set(\"destructured\", { node, existingSpecifiers });\n }\n namespaceImports.set(importName, { node, existingSpecifiers });\n usagesByImport.set(importName, new Map());\n } else if (existingSpecifiers.size > 0) {\n // Store pure destructured imports\n namespaceImports.set(\"destructured\", { node, existingSpecifiers });\n // Do NOT mark the global as imported - we want to catch global usage even with destructured imports\n }\n }\n },\n\n // Handle React.something in regular code\n MemberExpression(node: TSESTree.MemberExpression) {\n const objectNode = node.object;\n\n if (objectNode.type === AST_NODE_TYPES.Identifier && node.property.type === AST_NODE_TYPES.Identifier) {\n // Check if this is a tracked namespace import\n if (namespaceImports.has(objectNode.name)) {\n const usages = usagesByImport.get(objectNode.name);\n if (usages) {\n usages.set(node.range[0], {\n name: node.property.name,\n start: node.range[0],\n end: node.range[1],\n });\n }\n }\n // Check if this is a global identifier usage (e.g., React.ReactElement without import)\n else if (!importedGlobalIdentifiers.has(objectNode.name)) {\n // Find if this identifier matches any of our configured global names\n for (const packageName of targetPackages) {\n const globalName = packageToGlobalName[packageName];\n if (objectNode.name === globalName) {\n if (!globalUsages.has(globalName)) {\n globalUsages.set(globalName, {\n packageName: packageName,\n members: new Set(),\n nodes: [],\n });\n }\n const usage = globalUsages.get(globalName);\n if (usage) {\n usage.members.add(node.property.name);\n usage.nodes.push(node);\n }\n break;\n }\n }\n }\n }\n },\n\n // Handle React.ReactNode in type annotations\n TSQualifiedName(node: TSESTree.TSQualifiedName) {\n if (node.left.type === AST_NODE_TYPES.Identifier) {\n // Check if this is a tracked namespace import\n if (namespaceImports.has(node.left.name)) {\n const usages = usagesByImport.get(node.left.name);\n if (usages) {\n usages.set(node.range[0], {\n name: node.right.name,\n start: node.range[0],\n end: node.range[1],\n });\n }\n }\n // Check if this is a global identifier usage\n else if (!importedGlobalIdentifiers.has(node.left.name)) {\n for (const packageName of targetPackages) {\n const globalName = packageToGlobalName[packageName];\n if (node.left.name === globalName) {\n if (!globalUsages.has(globalName)) {\n globalUsages.set(globalName, {\n packageName: packageName,\n members: new Set(),\n nodes: [],\n });\n }\n const usage = globalUsages.get(globalName);\n if (usage) {\n usage.members.add(node.right.name);\n usage.nodes.push(node);\n }\n break;\n }\n }\n }\n }\n },\n\n // Note: TSQualifiedName already handles React.ReactElement, React.RefObject, etc.\n // This visitor is kept for potential future expansion but currently does nothing\n // to avoid duplicate tracking with TSQualifiedName\n TSTypeReference(_node: TSESTree.TSTypeReference) {\n // TSQualifiedName visitor handles all qualified type names (React.Something)\n // No action needed here\n },\n\n \"Program:exit\"() {\n // Report errors for namespace imports\n for (const [importName, { node: importNode }] of namespaceImports) {\n const usages = usagesByImport.get(importName);\n if (!usages || usages.size === 0) {\n continue;\n }\n\n const sourceValue = importNode.source.value;\n\n if (typeof sourceValue !== \"string\" || !isValidPackageName(sourceValue)) {\n continue;\n }\n\n context.report({\n node: importNode,\n messageId: \"preferDestructured\",\n fix(fixer) {\n const fixes = [];\n\n // Collect all specifiers needed from usages (e.g., [\"useState\", \"useEffect\"] from React.useState, React.useEffect)\n const specifiersFromUsages = Array.from(usages.values()).map(usage => usage.name);\n\n // Check if there's a separate destructured import to merge with\n const existingDestructured = namespaceImports.get(\"destructured\");\n const otherDestructuredImport =\n existingDestructured && existingDestructured.node !== importNode\n ? existingDestructured.node\n : undefined;\n\n // Replace the namespace import with destructured imports (merges if other import exists)\n fixes.push(\n ...replaceNamespaceWithDestructured({\n sourceCode,\n fixer,\n packageName: sourceValue,\n namespaceImportToReplace: importNode,\n specifiersToAdd: specifiersFromUsages,\n mergeIntoImport: otherDestructuredImport,\n })\n );\n\n // Remove namespace/default prefix from all usages\n for (const { start, end } of usages.values()) {\n const text = sourceCode.getText().slice(start, end);\n const newText = text.replace(new RegExp(`^${importName}\\\\.`), \"\");\n fixes.push(fixer.replaceTextRange([start, end], newText));\n }\n\n return fixes;\n },\n });\n }\n\n // Report errors for global UMD variable usages (e.g., React.ReactElement without import)\n for (const [globalName, { packageName, members, nodes }] of globalUsages) {\n if (members.size === 0 || nodes.length === 0) {\n continue;\n }\n\n // Report on the first usage node\n const firstNode = nodes[0];\n if (!firstNode) {\n continue;\n }\n\n const membersList = Array.from(members);\n const existingDestructured = namespaceImports.get(\"destructured\");\n\n // Filter out members that are already imported (no need to add them again)\n const membersToAdd = existingDestructured\n ? membersList.filter(member => !existingDestructured.existingSpecifiers.has(member))\n : membersList;\n\n context.report({\n node: firstNode,\n messageId: \"preferDestructuredGlobal\",\n data: {\n globalName,\n packageName,\n members: membersList.sort().join(\", \"), // Sort only for display in error message\n },\n fix(fixer) {\n const fixes = [];\n\n // Only update imports if there are new members to add\n if (membersToAdd.length > 0) {\n fixes.push(\n ...addImportSpecifiers({\n sourceCode,\n fixer,\n packageName,\n specifiers: membersToAdd,\n existingImport: existingDestructured?.node,\n })\n );\n }\n\n // Replace all global usages (React.X → X)\n // Deduplicate nodes by range and sort by position (reverse order to maintain positions)\n const uniqueNodesByRange = new Map<string, TSESTree.Node>();\n for (const node of nodes) {\n const rangeKey = `${node.range[0]}-${node.range[1]}`;\n if (!uniqueNodesByRange.has(rangeKey)) {\n uniqueNodesByRange.set(rangeKey, node);\n }\n }\n\n const sortedNodes = Array.from(uniqueNodesByRange.values()).sort((a, b) => b.range[0] - a.range[0]);\n\n for (const node of sortedNodes) {\n const nodeText = sourceCode.getText(node);\n const newText = nodeText.replace(new RegExp(`^${globalName}\\\\.`), \"\");\n fixes.push(fixer.replaceText(node, newText));\n }\n\n return fixes;\n },\n });\n }\n },\n };\n },\n});\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy-based name suggestion system for the prefer-event-specific-callback-naming rule.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a structured, extensible approach to generating suggested names
|
|
5
|
+
* for callback functions based on different naming patterns. Each strategy handles a
|
|
6
|
+
* specific type of problematic name and knows how to transform it correctly.
|
|
7
|
+
*
|
|
8
|
+
* Strategies are evaluated in order - the first matching strategy wins.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Context passed to name suggestion strategies.
|
|
12
|
+
*/
|
|
13
|
+
export type NameSuggestionContext = {
|
|
14
|
+
/** The current callback name (e.g., "onPrimary", "onClose", "handleClick") */
|
|
15
|
+
callbackName: string;
|
|
16
|
+
/** The event handler prop name (e.g., "onClick", "primaryAction", "onSubmit") */
|
|
17
|
+
eventName: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A strategy for suggesting renamed callback names.
|
|
21
|
+
* Each strategy handles a specific pattern of problematic naming.
|
|
22
|
+
*/
|
|
23
|
+
export type NameSuggestionStrategy = {
|
|
24
|
+
/** Human-readable name for debugging and logging */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Returns true if this strategy should handle the given context */
|
|
27
|
+
matches: (ctx: NameSuggestionContext) => boolean;
|
|
28
|
+
/** Returns the suggested name for the callback */
|
|
29
|
+
suggest: (ctx: NameSuggestionContext) => string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Generates a suggested name for a callback based on the event it's passed to.
|
|
33
|
+
*
|
|
34
|
+
* Uses a strategy pattern to handle different naming conventions appropriately.
|
|
35
|
+
* The first matching strategy determines the suggested name.
|
|
36
|
+
*
|
|
37
|
+
* @param ctx - The naming context with callback and event names
|
|
38
|
+
* @returns The suggested name for the callback
|
|
39
|
+
* @example
|
|
40
|
+
* getSuggestedName({ callbackName: "onClose", eventName: "onClick" })
|
|
41
|
+
* // Returns: "onClickClose"
|
|
42
|
+
*
|
|
43
|
+
* getSuggestedName({ callbackName: "onPrimary", eventName: "primaryAction" })
|
|
44
|
+
* // Returns: "onClickPrimary"
|
|
45
|
+
*/
|
|
46
|
+
export declare const getSuggestedName: (ctx: NameSuggestionContext) => string;
|
|
47
|
+
/**
|
|
48
|
+
* Exported for testing purposes - allows testing individual strategies.
|
|
49
|
+
*/
|
|
50
|
+
export declare const testableStrategies: {
|
|
51
|
+
actionSuffixEventStrategy: NameSuggestionStrategy;
|
|
52
|
+
handlerSuffixEventStrategy: NameSuggestionStrategy;
|
|
53
|
+
onPrefixCallbackToClickEventStrategy: NameSuggestionStrategy;
|
|
54
|
+
onPrefixCallbackToGenericEventStrategy: NameSuggestionStrategy;
|
|
55
|
+
defaultCapitalizeStrategy: NameSuggestionStrategy;
|
|
56
|
+
};
|