@atlaskit/button 16.1.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +1485 -0
  2. package/LICENSE +13 -0
  3. package/README.md +13 -0
  4. package/__perf__/button.tsx +19 -0
  5. package/__perf__/custom.tsx +19 -0
  6. package/__perf__/customised.tsx +11 -0
  7. package/__perf__/default.tsx +5 -0
  8. package/__perf__/loading.tsx +5 -0
  9. package/__perf__/utils/example-runner.tsx +48 -0
  10. package/__perf__/utils/interaction-tasks.tsx +98 -0
  11. package/button-group/package.json +7 -0
  12. package/codemods/15.0.0-lite-mode.ts +49 -0
  13. package/codemods/15.1.1-data-testid.ts +173 -0
  14. package/codemods/__tests__/15.0.0-lite-mode/optimistic.ts +646 -0
  15. package/codemods/__tests__/15.0.0-lite-mode/safe.ts +223 -0
  16. package/codemods/__tests__/15.0.0-lite-mode/shared.ts +257 -0
  17. package/codemods/__tests__/15.1.1-data-testid/rename-data-testid.ts +186 -0
  18. package/codemods/__tests__/_framework.ts +47 -0
  19. package/codemods/helpers/15.0.0-runner.ts +169 -0
  20. package/codemods/helpers/helpers-generic.ts +662 -0
  21. package/codemods/optimistic-15.0.0-lite-mode.ts +279 -0
  22. package/codemods/readme.md +1 -0
  23. package/custom-theme-button/package.json +7 -0
  24. package/dist/cjs/button-group.js +50 -0
  25. package/dist/cjs/button.js +104 -0
  26. package/dist/cjs/custom-theme-button/custom-theme-button-types.js +5 -0
  27. package/dist/cjs/custom-theme-button/custom-theme-button.js +229 -0
  28. package/dist/cjs/custom-theme-button/index.js +23 -0
  29. package/dist/cjs/custom-theme-button/theme.js +108 -0
  30. package/dist/cjs/entry-points/button-group.js +15 -0
  31. package/dist/cjs/entry-points/custom-theme-button.js +25 -0
  32. package/dist/cjs/entry-points/loading-button.js +15 -0
  33. package/dist/cjs/entry-points/standard-button.js +15 -0
  34. package/dist/cjs/entry-points/types.js +5 -0
  35. package/dist/cjs/index.js +51 -0
  36. package/dist/cjs/loading-button.js +34 -0
  37. package/dist/cjs/shared/block-events.js +44 -0
  38. package/dist/cjs/shared/button-base.js +158 -0
  39. package/dist/cjs/shared/colors.js +409 -0
  40. package/dist/cjs/shared/css.js +265 -0
  41. package/dist/cjs/shared/get-is-only-single-icon.js +26 -0
  42. package/dist/cjs/shared/loading-spinner.js +45 -0
  43. package/dist/cjs/types.js +5 -0
  44. package/dist/cjs/version.json +5 -0
  45. package/dist/es2019/button-group.js +36 -0
  46. package/dist/es2019/button.js +69 -0
  47. package/dist/es2019/custom-theme-button/custom-theme-button-types.js +1 -0
  48. package/dist/es2019/custom-theme-button/custom-theme-button.js +164 -0
  49. package/dist/es2019/custom-theme-button/index.js +2 -0
  50. package/dist/es2019/custom-theme-button/theme.js +81 -0
  51. package/dist/es2019/entry-points/button-group.js +1 -0
  52. package/dist/es2019/entry-points/custom-theme-button.js +1 -0
  53. package/dist/es2019/entry-points/loading-button.js +1 -0
  54. package/dist/es2019/entry-points/standard-button.js +1 -0
  55. package/dist/es2019/entry-points/types.js +1 -0
  56. package/dist/es2019/index.js +6 -0
  57. package/dist/es2019/loading-button.js +17 -0
  58. package/dist/es2019/shared/block-events.js +37 -0
  59. package/dist/es2019/shared/button-base.js +127 -0
  60. package/dist/es2019/shared/colors.js +393 -0
  61. package/dist/es2019/shared/css.js +249 -0
  62. package/dist/es2019/shared/get-is-only-single-icon.js +19 -0
  63. package/dist/es2019/shared/loading-spinner.js +33 -0
  64. package/dist/es2019/types.js +1 -0
  65. package/dist/es2019/version.json +5 -0
  66. package/dist/esm/button-group.js +35 -0
  67. package/dist/esm/button.js +79 -0
  68. package/dist/esm/custom-theme-button/custom-theme-button-types.js +1 -0
  69. package/dist/esm/custom-theme-button/custom-theme-button.js +203 -0
  70. package/dist/esm/custom-theme-button/index.js +2 -0
  71. package/dist/esm/custom-theme-button/theme.js +90 -0
  72. package/dist/esm/entry-points/button-group.js +1 -0
  73. package/dist/esm/entry-points/custom-theme-button.js +1 -0
  74. package/dist/esm/entry-points/loading-button.js +1 -0
  75. package/dist/esm/entry-points/standard-button.js +1 -0
  76. package/dist/esm/entry-points/types.js +1 -0
  77. package/dist/esm/index.js +6 -0
  78. package/dist/esm/loading-button.js +19 -0
  79. package/dist/esm/shared/block-events.js +36 -0
  80. package/dist/esm/shared/button-base.js +135 -0
  81. package/dist/esm/shared/colors.js +393 -0
  82. package/dist/esm/shared/css.js +245 -0
  83. package/dist/esm/shared/get-is-only-single-icon.js +19 -0
  84. package/dist/esm/shared/loading-spinner.js +35 -0
  85. package/dist/esm/types.js +1 -0
  86. package/dist/esm/version.json +5 -0
  87. package/dist/types/button-group.d.ts +18 -0
  88. package/dist/types/button.d.ts +8 -0
  89. package/dist/types/custom-theme-button/custom-theme-button-types.d.ts +21 -0
  90. package/dist/types/custom-theme-button/custom-theme-button.d.ts +6 -0
  91. package/dist/types/custom-theme-button/index.d.ts +2 -0
  92. package/dist/types/custom-theme-button/theme.d.ts +21 -0
  93. package/dist/types/entry-points/button-group.d.ts +1 -0
  94. package/dist/types/entry-points/custom-theme-button.d.ts +2 -0
  95. package/dist/types/entry-points/loading-button.d.ts +2 -0
  96. package/dist/types/entry-points/standard-button.d.ts +2 -0
  97. package/dist/types/entry-points/types.d.ts +4 -0
  98. package/dist/types/index.d.ts +8 -0
  99. package/dist/types/loading-button.d.ts +11 -0
  100. package/dist/types/shared/block-events.d.ts +3 -0
  101. package/dist/types/shared/button-base.d.ts +10 -0
  102. package/dist/types/shared/colors.d.ts +31 -0
  103. package/dist/types/shared/css.d.ts +22 -0
  104. package/dist/types/shared/get-is-only-single-icon.d.ts +2 -0
  105. package/dist/types/shared/loading-spinner.d.ts +4 -0
  106. package/dist/types/types.d.ts +51 -0
  107. package/extract-react-types/custom-theme-button-props.tsx +7 -0
  108. package/extract-react-types/loading-button-props.tsx +5 -0
  109. package/extract-react-types/shared-props.tsx +5 -0
  110. package/loading-button/package.json +7 -0
  111. package/package.json +83 -0
  112. package/standard-button/package.json +7 -0
  113. package/types/package.json +7 -0
@@ -0,0 +1,186 @@
1
+ jest.autoMockOff();
2
+
3
+ import transformer from '../../15.1.1-data-testid';
4
+ import { check } from '../_framework';
5
+
6
+ check({
7
+ transformer,
8
+ it: 'should rename data-testid to testId (default import)',
9
+ original: `
10
+ import DSButton from '@atlaskit/button';
11
+
12
+ function App() {
13
+ return <DSButton data-testid="my-testid">click me</DSButton>;
14
+ }
15
+ `,
16
+ expected: `
17
+ import DSButton from '@atlaskit/button';
18
+
19
+ function App() {
20
+ return <DSButton testId="my-testid">click me</DSButton>;
21
+ }
22
+ `,
23
+ });
24
+
25
+ check({
26
+ transformer,
27
+ it: 'should rename data-testid to testId (standard button)',
28
+ original: `
29
+ import Button from '@atlaskit/button/standard-button';
30
+
31
+ function App() {
32
+ return <Button data-testid="my-testid">click me</Button>;
33
+ }
34
+ `,
35
+ expected: `
36
+ import Button from '@atlaskit/button/standard-button';
37
+
38
+ function App() {
39
+ return <Button testId="my-testid">click me</Button>;
40
+ }
41
+ `,
42
+ });
43
+
44
+ check({
45
+ transformer,
46
+ it: 'should rename data-testid to testId (loading button)',
47
+ original: `
48
+ import Button from '@atlaskit/button/loading-button';
49
+
50
+ function App() {
51
+ return <Button data-testid="my-testid">click me</Button>;
52
+ }
53
+ `,
54
+ expected: `
55
+ import Button from '@atlaskit/button/loading-button';
56
+
57
+ function App() {
58
+ return <Button testId="my-testid">click me</Button>;
59
+ }
60
+ `,
61
+ });
62
+
63
+ check({
64
+ transformer,
65
+ it: 'should rename data-testid to testId (custom theme button)',
66
+ original: `
67
+ import Button from '@atlaskit/button/custom-theme-button';
68
+
69
+ function App() {
70
+ return <Button data-testid="my-testid">click me</Button>;
71
+ }
72
+ `,
73
+ expected: `
74
+ import Button from '@atlaskit/button/custom-theme-button';
75
+
76
+ function App() {
77
+ return <Button testId="my-testid">click me</Button>;
78
+ }
79
+ `,
80
+ });
81
+
82
+ check({
83
+ transformer,
84
+ it: 'should rename data-testid to testId (named imports)',
85
+ original: `
86
+ import { StandardButton, LoadingButton, CustomThemeButton } from '@atlaskit/button';
87
+
88
+ function App() {
89
+ return <>
90
+ <StandardButton data-testid="my-testid">click me</StandardButton>
91
+ <LoadingButton data-testid="my-testid">click me</LoadingButton>
92
+ <CustomThemeButton data-testid="my-testid">click me</CustomThemeButton>
93
+ </>;
94
+ }
95
+ `,
96
+ expected: `
97
+ import { StandardButton, LoadingButton, CustomThemeButton } from '@atlaskit/button';
98
+
99
+ function App() {
100
+ return <>
101
+ <StandardButton testId="my-testid">click me</StandardButton>
102
+ <LoadingButton testId="my-testid">click me</LoadingButton>
103
+ <CustomThemeButton testId="my-testid">click me</CustomThemeButton>
104
+ </>;
105
+ }
106
+ `,
107
+ });
108
+
109
+ check({
110
+ transformer,
111
+ it: 'should rename data-testid to testId (named imports with alias)',
112
+ original: `
113
+ import { StandardButton as SB, LoadingButton as LB, CustomThemeButton as CTB } from '@atlaskit/button';
114
+
115
+ function App() {
116
+ return <>
117
+ <SB data-testid="my-testid">click me</SB>
118
+ <LB data-testid="my-testid">click me</LB>
119
+ <CTB data-testid="my-testid">click me</CTB>
120
+ </>;
121
+ }
122
+ `,
123
+ expected: `
124
+ import { StandardButton as SB, LoadingButton as LB, CustomThemeButton as CTB } from '@atlaskit/button';
125
+
126
+ function App() {
127
+ return <>
128
+ <SB testId="my-testid">click me</SB>
129
+ <LB testId="my-testid">click me</LB>
130
+ <CTB testId="my-testid">click me</CTB>
131
+ </>;
132
+ }
133
+ `,
134
+ });
135
+
136
+ check({
137
+ transformer,
138
+ it:
139
+ 'should add a warning when both the testId and data-testid props are being used',
140
+ original: `
141
+ import StandardButton from '@atlaskit/button';
142
+
143
+ function App() {
144
+ return <StandardButton testId="oh-my" data-testid="oh-dear">Click me</StandardButton>
145
+ }
146
+ `,
147
+ expected: `
148
+ /* TODO: (from codemod) Cannot rename data-testid to testId on StandardButton.
149
+ A StandardButton was detected with both data-testid and testId props.
150
+ Please remove the data-testid prop and check your tests */
151
+ import StandardButton from '@atlaskit/button';
152
+
153
+ function App() {
154
+ return <StandardButton testId="oh-my" data-testid="oh-dear">Click me</StandardButton>
155
+ }
156
+ `,
157
+ });
158
+
159
+ check({
160
+ transformer,
161
+ it: 'should not touch unrelated testids',
162
+ original: `
163
+ import DSButton from '@atlaskit/button';
164
+ import Something from '@atlaskit/foobar';
165
+
166
+ function App() {
167
+ return <>
168
+ <DSButton data-testid="my-testid">
169
+ <Something data-testid="hello">click me</Something>
170
+ </DSButton>
171
+ </>;
172
+ }
173
+ `,
174
+ expected: `
175
+ import DSButton from '@atlaskit/button';
176
+ import Something from '@atlaskit/foobar';
177
+
178
+ function App() {
179
+ return <>
180
+ <DSButton testId="my-testid">
181
+ <Something data-testid="hello">click me</Something>
182
+ </DSButton>
183
+ </>;
184
+ }
185
+ `,
186
+ });
@@ -0,0 +1,47 @@
1
+ import { API, FileInfo, Options } from 'jscodeshift';
2
+
3
+ const applyTransform = require('jscodeshift/dist/testUtils').applyTransform;
4
+
5
+ type Transformer = (file: FileInfo, jscodeshift: API, options: Options) => void;
6
+
7
+ type TestArgs = {
8
+ it: string;
9
+ original: string;
10
+ expected: string;
11
+ transformer: Transformer;
12
+ mode?: 'only' | 'skip' | 'standard';
13
+ before?: () => void;
14
+ after?: () => void;
15
+ };
16
+
17
+ function noop() {}
18
+
19
+ export function check({
20
+ it: name,
21
+ original,
22
+ expected,
23
+ transformer,
24
+ before = noop,
25
+ after = noop,
26
+ mode = 'standard',
27
+ }: TestArgs) {
28
+ const run = mode === 'only' ? it.only : mode === 'skip' ? it.skip : it;
29
+
30
+ run(name, () => {
31
+ before();
32
+ try {
33
+ const output: string = applyTransform(
34
+ { default: transformer, parser: 'tsx' },
35
+ {},
36
+ { source: original },
37
+ );
38
+ expect(output).toBe(expected.trim());
39
+ } catch (e) {
40
+ // a failed assertion will throw
41
+ after();
42
+ throw e;
43
+ }
44
+ // will only be hit if we don't throw
45
+ after();
46
+ });
47
+ }
@@ -0,0 +1,169 @@
1
+ import { NodePath } from 'ast-types/lib/node-path';
2
+ import core, { FileInfo, JSXElement } from 'jscodeshift';
3
+ import { Collection } from 'jscodeshift/src/Collection';
4
+
5
+ import {
6
+ addToImport,
7
+ changeImportFor,
8
+ hasImportDeclaration,
9
+ removeImport,
10
+ tryCreateImport,
11
+ } from './helpers-generic';
12
+
13
+ export function convertButtonType({
14
+ j,
15
+ base,
16
+ elements,
17
+ name,
18
+ newPackageName,
19
+ }: {
20
+ j: core.JSCodeshift;
21
+ base: Collection<any>;
22
+ elements: NodePath<JSXElement, JSXElement>[];
23
+ name: string;
24
+ newPackageName: string;
25
+ }) {
26
+ // Don't need to do anything if there are no elements of this type
27
+ if (!elements.length) {
28
+ return;
29
+ }
30
+
31
+ tryCreateImport({
32
+ j,
33
+ base,
34
+ relativeToPackage: '@atlaskit/button',
35
+ packageName: newPackageName,
36
+ });
37
+
38
+ addToImport({
39
+ j,
40
+ base,
41
+ importSpecifier: j.importDefaultSpecifier(j.identifier(name)),
42
+ packageName: newPackageName,
43
+ });
44
+
45
+ elements.forEach((path: NodePath<JSXElement, JSXElement>) => {
46
+ path.replace(
47
+ j.jsxElement(
48
+ j.jsxOpeningElement(
49
+ // name: new!
50
+ j.jsxIdentifier(name),
51
+ // keep the old attributes
52
+ path.value.openingElement.attributes,
53
+ // self closing
54
+ path.value.openingElement.selfClosing,
55
+ ),
56
+ // Only create a closing element if the original had one
57
+ // <Button /> => <Button /> (rather than <Button></Button>)
58
+ path.value.closingElement
59
+ ? j.jsxClosingElement(j.jsxIdentifier(name))
60
+ : null,
61
+ path.value.children,
62
+ ),
63
+ );
64
+ });
65
+ }
66
+
67
+ export function changeType({
68
+ j,
69
+ base,
70
+ oldName,
71
+ newName,
72
+ fallbackNameAlias,
73
+ }: {
74
+ j: core.JSCodeshift;
75
+ base: Collection<any>;
76
+ oldName: string;
77
+ newName: string;
78
+ fallbackNameAlias: string;
79
+ }) {
80
+ changeImportFor({
81
+ j,
82
+ base,
83
+ option: {
84
+ type: 'change-name',
85
+ oldName,
86
+ newName,
87
+ fallbackNameAlias,
88
+ },
89
+ oldPackagePath: '@atlaskit/button',
90
+ newPackagePath: '@atlaskit/button/types',
91
+ });
92
+ changeImportFor({
93
+ j,
94
+ base,
95
+ option: {
96
+ type: 'change-name',
97
+ oldName,
98
+ newName,
99
+ fallbackNameAlias,
100
+ },
101
+ oldPackagePath: '@atlaskit/button/types',
102
+ newPackagePath: '@atlaskit/button/types',
103
+ });
104
+ }
105
+
106
+ export function transformButton({
107
+ j,
108
+ file,
109
+ custom: fn,
110
+ }: {
111
+ j: core.JSCodeshift;
112
+ file: FileInfo;
113
+ custom: (base: Collection<any>) => void;
114
+ }) {
115
+ const base: Collection<any> = j(file.source);
116
+
117
+ // Exit early if not relevant
118
+ // We are doing this so we don't touch the formatting of unrelated files
119
+ const willChange: boolean =
120
+ hasImportDeclaration(j, file.source, '@atlaskit/button') ||
121
+ hasImportDeclaration(j, file.source, '@atlaskit/button/types');
122
+
123
+ if (!willChange) {
124
+ return file.source;
125
+ }
126
+
127
+ changeType({
128
+ j,
129
+ base,
130
+ oldName: 'ButtonAppearances',
131
+ newName: 'Appearance',
132
+ fallbackNameAlias: 'DSButtonAppearance',
133
+ });
134
+
135
+ changeImportFor({
136
+ j,
137
+ base,
138
+ // Not changing the name
139
+ option: {
140
+ type: 'keep-name',
141
+ name: 'Theme',
142
+ behaviour: 'keep-as-named-import',
143
+ },
144
+ oldPackagePath: '@atlaskit/button',
145
+ newPackagePath: '@atlaskit/button/custom-theme-button',
146
+ });
147
+
148
+ changeImportFor({
149
+ j,
150
+ base,
151
+ option: {
152
+ type: 'keep-name',
153
+ name: 'ButtonGroup',
154
+ behaviour: 'move-to-default-import',
155
+ },
156
+ oldPackagePath: '@atlaskit/button',
157
+ newPackagePath: '@atlaskit/button/button-group',
158
+ });
159
+
160
+ fn(base);
161
+
162
+ removeImport({
163
+ j,
164
+ base,
165
+ packageName: '@atlaskit/button',
166
+ });
167
+
168
+ return base.toSource({ quote: 'single' });
169
+ }
@@ -0,0 +1,662 @@
1
+ import { NodePath } from 'ast-types/lib/node-path';
2
+ import core, {
3
+ ASTPath,
4
+ ImportDeclaration,
5
+ ImportDefaultSpecifier,
6
+ ImportSpecifier,
7
+ JSXAttribute,
8
+ JSXElement,
9
+ Node,
10
+ Program,
11
+ } from 'jscodeshift';
12
+ import { Collection } from 'jscodeshift/src/Collection';
13
+
14
+ export type Nullable<T> = T | null;
15
+
16
+ export function hasImportDeclaration(
17
+ j: core.JSCodeshift,
18
+ source: string,
19
+ importPath: string,
20
+ ): boolean {
21
+ return (
22
+ j(source)
23
+ .find(j.ImportDeclaration)
24
+ .filter(
25
+ (path: ASTPath<ImportDeclaration>) =>
26
+ path.node.source.value === importPath,
27
+ ).length > 0
28
+ );
29
+ }
30
+
31
+ export function getDefaultSpecifierName({
32
+ j,
33
+ base,
34
+ packageName,
35
+ }: {
36
+ j: core.JSCodeshift;
37
+ base: Collection<any>;
38
+ packageName: string;
39
+ }): Nullable<string> {
40
+ const specifiers = base
41
+ .find(j.ImportDeclaration)
42
+ .filter((path) => path.node.source.value === packageName)
43
+ .find(j.ImportDefaultSpecifier);
44
+
45
+ if (!specifiers.length) {
46
+ return null;
47
+ }
48
+ return specifiers.nodes()[0]!.local!.name;
49
+ }
50
+
51
+ export function getJSXAttributesByName({
52
+ j,
53
+ element,
54
+ attributeName,
55
+ }: {
56
+ j: core.JSCodeshift;
57
+ element: JSXElement;
58
+ attributeName: string;
59
+ }): Collection<JSXAttribute> {
60
+ return j(element)
61
+ .find(j.JSXOpeningElement)
62
+ .find(j.JSXAttribute)
63
+ .filter((attribute) => {
64
+ const matches = j(attribute)
65
+ // This will find identifiers on nested jsx elements
66
+ // so we are going to do a filter to ensure we are only
67
+ // going one level deep
68
+ .find(j.JSXIdentifier)
69
+ .filter((identifer) => {
70
+ j(identifer).closest(j.JSXOpeningElement);
71
+ // Checking we are on the same level as the jsx element
72
+ const closest = j(identifer).closest(j.JSXOpeningElement).nodes()[0];
73
+
74
+ if (!closest) {
75
+ return false;
76
+ }
77
+ return (
78
+ closest.name.type === 'JSXIdentifier' &&
79
+ element.openingElement.name.type === 'JSXIdentifier' &&
80
+ element.openingElement.name.name === closest.name.name
81
+ );
82
+ })
83
+ .filter((identifier) => identifier.value.name === attributeName);
84
+ return Boolean(matches.length);
85
+ });
86
+ }
87
+
88
+ export function hasJSXAttributesByName({
89
+ j,
90
+ element,
91
+ attributeName,
92
+ }: {
93
+ j: core.JSCodeshift;
94
+ element: JSXElement;
95
+ attributeName: string;
96
+ }): boolean {
97
+ return getJSXAttributesByName({ j, element, attributeName }).length > 0;
98
+ }
99
+
100
+ export function removeImport({
101
+ j,
102
+ base,
103
+ packageName,
104
+ }: {
105
+ j: core.JSCodeshift;
106
+ base: Collection<any>;
107
+ packageName: string;
108
+ }) {
109
+ base
110
+ .find(j.ImportDeclaration)
111
+ .filter((path) => path.node.source.value === packageName)
112
+ .remove();
113
+ }
114
+
115
+ export function tryCreateImport({
116
+ j,
117
+ base,
118
+ relativeToPackage,
119
+ packageName,
120
+ }: {
121
+ j: core.JSCodeshift;
122
+ base: Collection<any>;
123
+ relativeToPackage: string;
124
+ packageName: string;
125
+ }) {
126
+ const exists: boolean =
127
+ base
128
+ .find(j.ImportDeclaration)
129
+ .filter((path) => path.value.source.value === packageName).length > 0;
130
+
131
+ if (exists) {
132
+ return;
133
+ }
134
+
135
+ base
136
+ .find(j.ImportDeclaration)
137
+ .filter((path) => path.value.source.value === relativeToPackage)
138
+ .insertBefore(j.importDeclaration([], j.literal(packageName)));
139
+ }
140
+
141
+ export function addToImport({
142
+ j,
143
+ base,
144
+ importSpecifier,
145
+ packageName,
146
+ }: {
147
+ j: core.JSCodeshift;
148
+ base: Collection<any>;
149
+ importSpecifier: ImportSpecifier | ImportDefaultSpecifier;
150
+ packageName: string;
151
+ }) {
152
+ base
153
+ .find(j.ImportDeclaration)
154
+ .filter((path) => path.value.source.value === packageName)
155
+ .replaceWith((declaration) => {
156
+ return j.importDeclaration(
157
+ [
158
+ // we are appending to the existing specifiers
159
+ // We are doing a filter hear because sometimes specifiers can be removed
160
+ // but they hand around in the declaration
161
+ ...(declaration.value.specifiers || []).filter(
162
+ (item) => item.type === 'ImportSpecifier' && item.imported != null,
163
+ ),
164
+ importSpecifier,
165
+ ],
166
+ j.literal(packageName),
167
+ );
168
+ });
169
+ }
170
+
171
+ export function doesIdentifierExist({
172
+ j,
173
+ base,
174
+ name,
175
+ }: {
176
+ j: core.JSCodeshift;
177
+ base: Collection<any>;
178
+ name: string;
179
+ }): boolean {
180
+ return (
181
+ base.find(j.Identifier).filter((identifer) => identifer.value.name === name)
182
+ .length > 0
183
+ );
184
+ }
185
+
186
+ export function isUsingSupportedSpread({
187
+ j,
188
+ base,
189
+ element,
190
+ }: {
191
+ j: core.JSCodeshift;
192
+ base: Collection<any>;
193
+ element: NodePath<JSXElement, JSXElement>;
194
+ }): boolean {
195
+ const isUsingSpread: boolean =
196
+ j(element).find(j.JSXSpreadAttribute).length > 0;
197
+
198
+ if (!isUsingSpread) {
199
+ return true;
200
+ }
201
+
202
+ return (
203
+ j(element)
204
+ .find(j.JSXSpreadAttribute)
205
+ .filter((spread) => {
206
+ const argument = spread.value.argument;
207
+ // in place expression is supported
208
+ if (argument.type === 'ObjectExpression') {
209
+ return true;
210
+ }
211
+
212
+ // Supporting identifiers that point to an a local object expression
213
+ if (argument.type === 'Identifier') {
214
+ return (
215
+ base.find(j.VariableDeclarator).filter((declarator): boolean => {
216
+ return Boolean(
217
+ declarator.value.id.type === 'Identifier' &&
218
+ declarator.value.init &&
219
+ declarator.value.init.type === 'ObjectExpression',
220
+ );
221
+ }).length > 0
222
+ );
223
+ }
224
+
225
+ // We don't support anything else
226
+ return false;
227
+ }).length > 0
228
+ );
229
+ }
230
+
231
+ export function isOnlyUsingNameForJSX({
232
+ j,
233
+ base,
234
+ name,
235
+ }: {
236
+ j: core.JSCodeshift;
237
+ base: Collection<any>;
238
+ name: string;
239
+ }): boolean {
240
+ const jsxIdentifierCount: number = base
241
+ .find(j.JSXIdentifier)
242
+ .filter((identifier) => identifier.value.name === name).length;
243
+
244
+ // Not used in JSX at all
245
+ if (jsxIdentifierCount === 0) {
246
+ return false;
247
+ }
248
+
249
+ const nonJSXIdentifierCount: number = base
250
+ .find(j.Identifier)
251
+ .filter((identifier) => {
252
+ if (identifier.value.name !== name) {
253
+ return false;
254
+ }
255
+
256
+ // @ts-ignore
257
+ if (identifier.value.type === 'JSXIdentifier') {
258
+ return false;
259
+ }
260
+
261
+ // Excluding exports
262
+ if (j(identifier).closest(j.ImportDefaultSpecifier).length) {
263
+ return false;
264
+ }
265
+ if (j(identifier).closest(j.ImportSpecifier).length) {
266
+ return false;
267
+ }
268
+
269
+ return true;
270
+ }).length;
271
+
272
+ if (nonJSXIdentifierCount > 0) {
273
+ return false;
274
+ }
275
+
276
+ return true;
277
+ }
278
+
279
+ export function getSafeImportName({
280
+ j,
281
+ base,
282
+ currentDefaultSpecifierName,
283
+ desiredName,
284
+ fallbackName,
285
+ }: {
286
+ j: core.JSCodeshift;
287
+ base: Collection<any>;
288
+ currentDefaultSpecifierName: string;
289
+ desiredName: string;
290
+ fallbackName: string;
291
+ }) {
292
+ if (currentDefaultSpecifierName === desiredName) {
293
+ return desiredName;
294
+ }
295
+
296
+ const isUsed: boolean = doesIdentifierExist({ j, base, name: desiredName });
297
+
298
+ return isUsed ? fallbackName : desiredName;
299
+ }
300
+
301
+ export function isUsingThroughSpread({
302
+ j,
303
+ base,
304
+ element,
305
+ propName,
306
+ }: {
307
+ j: core.JSCodeshift;
308
+ base: Collection<any>;
309
+ element: NodePath<JSXElement, JSXElement>;
310
+ propName: string;
311
+ }): boolean {
312
+ if (!isUsingSupportedSpread({ j, base, element })) {
313
+ return false;
314
+ }
315
+
316
+ const isUsedThroughExpression: boolean =
317
+ j(element)
318
+ .find(j.JSXSpreadAttribute)
319
+ .find(j.ObjectExpression)
320
+ .filter((item) => {
321
+ const match: boolean =
322
+ item.value.properties.filter(
323
+ (property) =>
324
+ property.type === 'ObjectProperty' &&
325
+ property.key.type === 'Identifier' &&
326
+ property.key.name === propName,
327
+ ).length > 0;
328
+
329
+ return match;
330
+ }).length > 0;
331
+
332
+ if (isUsedThroughExpression) {
333
+ return true;
334
+ }
335
+
336
+ const isUsedThroughIdentifier: boolean =
337
+ j(element)
338
+ .find(j.JSXSpreadAttribute)
339
+ .find(j.Identifier)
340
+ .filter((identifier): boolean => {
341
+ return (
342
+ base
343
+ .find(j.VariableDeclarator)
344
+ .filter(
345
+ (declarator) =>
346
+ declarator.value.id.type === 'Identifier' &&
347
+ declarator.value.id.name === identifier.value.name,
348
+ )
349
+ .filter((declarator) => {
350
+ const value = declarator.value;
351
+ if (value.id.type !== 'Identifier') {
352
+ return false;
353
+ }
354
+
355
+ if (value.id.name !== identifier.value.name) {
356
+ return false;
357
+ }
358
+
359
+ if (!value.init) {
360
+ return false;
361
+ }
362
+
363
+ if (value.init.type !== 'ObjectExpression') {
364
+ return false;
365
+ }
366
+
367
+ const match: boolean =
368
+ value.init.properties.filter(
369
+ (property) =>
370
+ property.type === 'ObjectProperty' &&
371
+ property.key.type === 'Identifier' &&
372
+ property.key.name === propName,
373
+ ).length > 0;
374
+
375
+ return match;
376
+ }).length > 0
377
+ );
378
+ }).length > 0;
379
+
380
+ return isUsedThroughIdentifier;
381
+ }
382
+
383
+ export function isUsingProp({
384
+ j,
385
+ base,
386
+ element,
387
+ propName,
388
+ }: {
389
+ j: core.JSCodeshift;
390
+ base: Collection<any>;
391
+ element: NodePath<JSXElement, JSXElement>;
392
+ propName: string;
393
+ }): boolean {
394
+ return (
395
+ hasJSXAttributesByName({
396
+ j,
397
+ element: element.value,
398
+ attributeName: propName,
399
+ }) ||
400
+ isUsingThroughSpread({
401
+ j,
402
+ base,
403
+ element,
404
+ propName,
405
+ })
406
+ );
407
+ }
408
+
409
+ // not replacing newlines (which \s does)
410
+ const spacesAndTabs: RegExp = /[ \t]{2,}/g;
411
+ const lineStartWithSpaces: RegExp = /^[ \t]*/gm;
412
+
413
+ function clean(value: string): string {
414
+ return (
415
+ value
416
+ .replace(spacesAndTabs, ' ')
417
+ .replace(lineStartWithSpaces, '')
418
+ // using .trim() to clear the any newlines before the first text and after last text
419
+ .trim()
420
+ );
421
+ }
422
+
423
+ export function addCommentToStartOfFile({
424
+ j,
425
+ base,
426
+ message,
427
+ }: {
428
+ j: core.JSCodeshift;
429
+ base: Collection<Node>;
430
+ message: string;
431
+ }) {
432
+ addCommentBefore({
433
+ j,
434
+ target: base.find(j.Program),
435
+ message,
436
+ });
437
+ }
438
+
439
+ export function addCommentBefore({
440
+ j,
441
+ target,
442
+ message,
443
+ }: {
444
+ j: core.JSCodeshift;
445
+ target:
446
+ | Collection<Node>
447
+ | Collection<Program>
448
+ | Collection<ImportDeclaration>;
449
+ message: string;
450
+ }) {
451
+ const content: string = ` TODO: (from codemod) ${clean(message)} `;
452
+ target.forEach((path) => {
453
+ path.value.comments = path.value.comments || [];
454
+
455
+ const exists = path.value.comments.find(
456
+ (comment) => comment.value === content,
457
+ );
458
+
459
+ // avoiding duplicates of the same comment
460
+ if (exists) {
461
+ return;
462
+ }
463
+
464
+ path.value.comments.push(j.commentBlock(content));
465
+ });
466
+ }
467
+
468
+ export function shiftDefaultImport({
469
+ j,
470
+ base,
471
+ defaultName,
472
+ oldPackagePath,
473
+ newPackagePath,
474
+ }: {
475
+ j: core.JSCodeshift;
476
+ base: Collection<any>;
477
+ defaultName: string;
478
+ oldPackagePath: string;
479
+ newPackagePath: string;
480
+ }) {
481
+ tryCreateImport({
482
+ j,
483
+ base,
484
+ relativeToPackage: oldPackagePath,
485
+ packageName: newPackagePath,
486
+ });
487
+
488
+ addToImport({
489
+ j,
490
+ base,
491
+ importSpecifier: j.importDefaultSpecifier(j.identifier(defaultName)),
492
+ packageName: newPackagePath,
493
+ });
494
+
495
+ // removing old default specifier
496
+ base
497
+ .find(j.ImportDeclaration)
498
+ .filter((path) => path.node.source.value === oldPackagePath)
499
+ .find(j.ImportDefaultSpecifier)
500
+ .remove();
501
+ }
502
+
503
+ type Option =
504
+ | {
505
+ type: 'change-name';
506
+ oldName: string;
507
+ newName: string;
508
+ fallbackNameAlias: string;
509
+ }
510
+ | {
511
+ type: 'keep-name';
512
+ name: string;
513
+ behaviour: 'move-to-default-import' | 'keep-as-named-import';
514
+ };
515
+
516
+ // try to avoid this one if you can. I'm not super happy with it
517
+ export function changeImportFor({
518
+ j,
519
+ base,
520
+ option,
521
+ oldPackagePath,
522
+ newPackagePath,
523
+ }: {
524
+ j: core.JSCodeshift;
525
+ base: Collection<any>;
526
+ option: Option;
527
+ oldPackagePath: string;
528
+ newPackagePath: string;
529
+ }) {
530
+ const currentName: string =
531
+ option.type === 'change-name' ? option.oldName : option.name;
532
+ const desiredName: string =
533
+ option.type === 'change-name' ? option.newName : option.name;
534
+
535
+ const isUsingName: boolean =
536
+ base
537
+ .find(j.ImportDeclaration)
538
+ .filter((path) => path.node.source.value === oldPackagePath)
539
+ .find(j.ImportSpecifier)
540
+ .find(j.Identifier)
541
+ .filter((identifier) => identifier.value.name === currentName).length > 0;
542
+
543
+ if (!isUsingName) {
544
+ return;
545
+ }
546
+
547
+ const existingAlias: Nullable<string> =
548
+ base
549
+ .find(j.ImportDeclaration)
550
+ .filter((path) => path.node.source.value === oldPackagePath)
551
+ .find(j.ImportSpecifier)
552
+ .nodes()
553
+ .map(
554
+ (specifier): Nullable<string> => {
555
+ if (specifier.imported.name !== currentName) {
556
+ return null;
557
+ }
558
+ // If aliased: return the alias
559
+ if (specifier.local && specifier.local.name !== currentName) {
560
+ return specifier.local.name;
561
+ }
562
+
563
+ return null;
564
+ },
565
+ )
566
+ .filter(Boolean)[0] || null;
567
+
568
+ base
569
+ .find(j.ImportDeclaration)
570
+ .filter((path) => path.node.source.value === oldPackagePath)
571
+ .find(j.ImportSpecifier)
572
+ .find(j.Identifier)
573
+ .filter((identifier) => {
574
+ if (identifier.value.name === currentName) {
575
+ return true;
576
+ }
577
+ if (identifier.value.name === existingAlias) {
578
+ return true;
579
+ }
580
+ return false;
581
+ })
582
+ .remove();
583
+
584
+ // Check to see if need to create new package path
585
+ // Try create an import declaration just before the old import
586
+ tryCreateImport({
587
+ j,
588
+ base,
589
+ relativeToPackage: oldPackagePath,
590
+ packageName: newPackagePath,
591
+ });
592
+
593
+ if (option.type === 'keep-name') {
594
+ const newSpecifier: ImportSpecifier | ImportDefaultSpecifier = (() => {
595
+ if (option.behaviour === 'keep-as-named-import') {
596
+ if (existingAlias) {
597
+ return j.importSpecifier(
598
+ j.identifier(desiredName),
599
+ j.identifier(existingAlias),
600
+ );
601
+ }
602
+
603
+ return j.importSpecifier(j.identifier(desiredName));
604
+ }
605
+
606
+ // moving to default specifier
607
+ return j.importDefaultSpecifier(
608
+ j.identifier(existingAlias || desiredName),
609
+ );
610
+ })();
611
+
612
+ // We don't need to touch anything else in the file
613
+
614
+ addToImport({
615
+ j,
616
+ base,
617
+ importSpecifier: newSpecifier,
618
+ packageName: newPackagePath,
619
+ });
620
+ return;
621
+ }
622
+
623
+ const isNewNameAvailable: boolean =
624
+ base.find(j.Identifier).filter((i) => i.value.name === option.newName)
625
+ .length === 0;
626
+
627
+ const newSpecifier: ImportSpecifier = (() => {
628
+ if (existingAlias) {
629
+ return j.importSpecifier(
630
+ j.identifier(desiredName),
631
+ j.identifier(existingAlias),
632
+ );
633
+ }
634
+
635
+ if (isNewNameAvailable) {
636
+ return j.importSpecifier(j.identifier(desiredName));
637
+ }
638
+
639
+ // new type name is not available: need to use a new alias
640
+ return j.importSpecifier(
641
+ j.identifier(desiredName),
642
+ j.identifier(option.fallbackNameAlias),
643
+ );
644
+ })();
645
+
646
+ addToImport({
647
+ j,
648
+ base,
649
+ importSpecifier: newSpecifier,
650
+ packageName: newPackagePath,
651
+ });
652
+
653
+ // Change usages of old type in file
654
+ base
655
+ .find(j.Identifier)
656
+ .filter((identifier) => identifier.value.name === option.oldName)
657
+ .replaceWith(
658
+ j.identifier(
659
+ isNewNameAvailable ? option.newName : option.fallbackNameAlias,
660
+ ),
661
+ );
662
+ }