@mittwald/flow-codemods 1.2.0-next.0 → 1.2.0-next.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 (30) hide show
  1. package/dist/migrations/accent-box-color-to-background-color/transform.js +175 -0
  2. package/dist/migrations/action-prop-to-on-action/transform.js +82 -0
  3. package/dist/migrations/align-to-combine/transform.js +166 -0
  4. package/dist/migrations/button-color-accent-to-success/transform.js +126 -0
  5. package/dist/migrations/button-props-interfaces/transform.js +178 -0
  6. package/dist/migrations/color-primary-to-default/transform.js +135 -0
  7. package/dist/migrations/imports-to-package-root/transform.js +93 -0
  8. package/dist/migrations/muted-action-error-to-abort-action-error/transform.js +198 -0
  9. package/dist/migrations/package.json +3 -0
  10. package/dist/migrations/password-tools-rule/transform.js +173 -0
  11. package/dist/migrations/password-tools-subpath-renamed/transform.js +51 -0
  12. package/dist/migrations.generated.d.ts.map +1 -1
  13. package/dist/migrations.generated.js +2 -2
  14. package/dist/run/jscodeshift.d.ts +4 -1
  15. package/dist/run/jscodeshift.d.ts.map +1 -1
  16. package/dist/run/jscodeshift.js +60 -22
  17. package/dist/tools/package.json +3 -0
  18. package/dist/tools/to-remote-package.js +29 -0
  19. package/package.json +4 -6
  20. package/src/migrations/accent-box-color-to-background-color/transform.ts +0 -218
  21. package/src/migrations/action-prop-to-on-action/transform.ts +0 -110
  22. package/src/migrations/align-to-combine/transform.ts +0 -212
  23. package/src/migrations/button-color-accent-to-success/transform.ts +0 -164
  24. package/src/migrations/button-props-interfaces/transform.ts +0 -229
  25. package/src/migrations/color-primary-to-default/transform.ts +0 -173
  26. package/src/migrations/imports-to-package-root/transform.ts +0 -107
  27. package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +0 -260
  28. package/src/migrations/password-tools-rule/transform.ts +0 -219
  29. package/src/migrations/password-tools-subpath-renamed/transform.ts +0 -61
  30. package/src/tools/to-remote-package.ts +0 -37
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Moves the background value of `AccentBox` from `color` to `backgroundColor`
5
+ * (alpha.786).
6
+ *
7
+ * `color` did not go away, it changed meaning: it used to set the background
8
+ * (`"blue" | "green" | "gradient" | "neutral"`) and now sets the content color
9
+ * (`"default" | "dark" | "light" | "dark-static" | "light-static"`). A blanket
10
+ * rename would therefore break every element that already uses the new meaning.
11
+ * The transform decides per value instead: a value from the new content-color
12
+ * union stays on `color`, every other literal moves to `backgroundColor`.
13
+ *
14
+ * The scope is deliberately narrow. Only JSX elements that resolve to
15
+ * `AccentBox` — imported (named or as a namespace) from
16
+ * `@mittwald/flow-react-components` or
17
+ * `@mittwald/flow-remote-react-components`, including their subpath entries —
18
+ * are touched.
19
+ *
20
+ * Two cases are left alone, because both are undecidable without knowing the
21
+ * value:
22
+ *
23
+ * - Values it cannot read (`color={expression}`), and expressions that mix both
24
+ * meanings (`color={flag ? "blue" : "dark"}`). The same expression means the
25
+ * background in old code and the content color in new code, and a mix has no
26
+ * single correct answer. An expression whose every value position is a
27
+ * literal on the same side of the split _is_ decidable and gets renamed:
28
+ * `color={flag ? "blue" : "green"}`.
29
+ * - An element that already carries `backgroundColor`. Moving `color` there would
30
+ * silently overwrite the explicit value.
31
+ *
32
+ * Both keep their `color` prop and need a look by hand.
33
+ */
34
+ const accentBoxColorToBackgroundColorTransform = (fileInfo, { j }) => {
35
+ const flowPackages = [
36
+ "@mittwald/flow-react-components",
37
+ "@mittwald/flow-remote-react-components",
38
+ ];
39
+ const affectedComponents = new Set(["AccentBox"]);
40
+ /** Values `color` still accepts — everything else was a background color. */
41
+ const contentColors = new Set([
42
+ "default",
43
+ "dark",
44
+ "light",
45
+ "dark-static",
46
+ "light-static",
47
+ ]);
48
+ const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
49
+ /** The value of a string literal, or `undefined` for anything dynamic. */
50
+ const literalValue = (node) => {
51
+ if (!node || typeof node !== "object" || !("type" in node)) {
52
+ return undefined;
53
+ }
54
+ const { type } = node;
55
+ if (type !== "StringLiteral" && type !== "Literal") {
56
+ return undefined;
57
+ }
58
+ const { value } = node;
59
+ return typeof value === "string" ? value : undefined;
60
+ };
61
+ /**
62
+ * Every literal that could become this attribute's value, or `undefined` when
63
+ * any of those positions is something we cannot read. The positions are the
64
+ * expression itself, both ternary branches, and the operands of `??`/`||` —
65
+ * plus only the right operand of `&&`, since `&&` yields its left operand
66
+ * only when that operand is falsy and a non-empty string never is.
67
+ *
68
+ * A list, not a single value, because this transform decides per attribute,
69
+ * not per literal: `color={flag ? "blue" : "dark"}` mixes a background with a
70
+ * content colour, and no single rename is right for both.
71
+ */
72
+ const valueLiterals = (node) => {
73
+ const literal = literalValue(node);
74
+ if (literal !== undefined) {
75
+ return [literal];
76
+ }
77
+ if (!node || typeof node !== "object" || !("type" in node)) {
78
+ return undefined;
79
+ }
80
+ const typed = node;
81
+ if (typed.type === "ConditionalExpression") {
82
+ const consequent = valueLiterals(typed.consequent);
83
+ const alternate = valueLiterals(typed.alternate);
84
+ return consequent && alternate
85
+ ? [...consequent, ...alternate]
86
+ : undefined;
87
+ }
88
+ if (typed.type === "LogicalExpression") {
89
+ const right = valueLiterals(typed.right);
90
+ if (right === undefined) {
91
+ return undefined;
92
+ }
93
+ if (typed.operator === "&&") {
94
+ return right;
95
+ }
96
+ const left = valueLiterals(typed.left);
97
+ return left ? [...left, ...right] : undefined;
98
+ }
99
+ return undefined;
100
+ };
101
+ const root = j(fileInfo.source, { parser: "tsx" });
102
+ // Local JSX identifier -> canonical component name (resolves `as` aliases).
103
+ const localToComponent = new Map();
104
+ // Local names of `import * as Flow` namespace imports from a Flow package.
105
+ const flowNamespaces = new Set();
106
+ root
107
+ .find(j.ImportDeclaration)
108
+ .filter((path) => isFlowImport(String(path.node.source.value)))
109
+ .forEach((path) => {
110
+ for (const specifier of path.node.specifiers ?? []) {
111
+ if (specifier.type === "ImportSpecifier" &&
112
+ specifier.imported.type === "Identifier" &&
113
+ affectedComponents.has(specifier.imported.name)) {
114
+ localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
115
+ }
116
+ else if (specifier.type === "ImportNamespaceSpecifier" &&
117
+ specifier.local) {
118
+ flowNamespaces.add(String(specifier.local.name));
119
+ }
120
+ }
121
+ });
122
+ if (localToComponent.size === 0 && flowNamespaces.size === 0) {
123
+ return fileInfo.source;
124
+ }
125
+ root.find(j.JSXOpeningElement).forEach((path) => {
126
+ const name = path.node.name;
127
+ let isAffected = false;
128
+ if (name.type === "JSXIdentifier") {
129
+ isAffected = localToComponent.has(name.name);
130
+ }
131
+ else if (name.type === "JSXMemberExpression" &&
132
+ name.object.type === "JSXIdentifier" &&
133
+ name.property.type === "JSXIdentifier") {
134
+ isAffected =
135
+ flowNamespaces.has(name.object.name) &&
136
+ affectedComponents.has(name.property.name);
137
+ }
138
+ if (!isAffected) {
139
+ return;
140
+ }
141
+ const attributes = path.node.attributes ?? [];
142
+ const isNamed = (attribute, key) => !!attribute &&
143
+ typeof attribute === "object" &&
144
+ attribute.type === "JSXAttribute" &&
145
+ attribute.name?.type ===
146
+ "JSXIdentifier" &&
147
+ attribute.name?.name === key;
148
+ // An explicit `backgroundColor` is the new API already — never overwrite it.
149
+ if (attributes.some((attribute) => isNamed(attribute, "backgroundColor"))) {
150
+ return;
151
+ }
152
+ for (const attribute of attributes) {
153
+ if (!isNamed(attribute, "color") || attribute.type !== "JSXAttribute") {
154
+ continue;
155
+ }
156
+ const value = attribute.value;
157
+ const literals = value?.type === "JSXExpressionContainer"
158
+ ? valueLiterals(value.expression)
159
+ : valueLiterals(value);
160
+ if (literals === undefined || literals.length === 0) {
161
+ continue;
162
+ }
163
+ // All-or-nothing. Every value has to be a background colour for the
164
+ // rename to be right; all content colours means the element already uses
165
+ // the new meaning, and a mix has no single correct answer.
166
+ const backgrounds = literals.filter((literal) => !contentColors.has(literal));
167
+ if (backgrounds.length !== literals.length) {
168
+ continue;
169
+ }
170
+ attribute.name = j.jsxIdentifier("backgroundColor");
171
+ }
172
+ });
173
+ return root.toSource();
174
+ };
175
+ exports.default = accentBoxColorToBackgroundColorTransform;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Renames the `action` prop to `onAction` on `Action`.
5
+ *
6
+ * The scope is deliberately narrow. Only JSX elements that resolve to `Action`
7
+ * — imported (named or as a namespace) from `@mittwald/flow-react-components`
8
+ * or `@mittwald/flow-remote-react-components`, including their subpath entries
9
+ * — are touched. `Action` is not one of the generated remote components, but
10
+ * the remote package re-exports the `flr-universal` surface, which carries it.
11
+ * Same-named components from other packages are left untouched, and so is the
12
+ * `action` attribute of a plain `<form>`.
13
+ *
14
+ * An element that already carries `onAction` keeps it and only loses the stale
15
+ * `action` prop, which mirrors what the runtime fallback does: an explicit
16
+ * `onAction` wins.
17
+ */
18
+ const actionPropToOnActionTransform = (fileInfo, { j }) => {
19
+ const flowPackages = [
20
+ "@mittwald/flow-react-components",
21
+ "@mittwald/flow-remote-react-components",
22
+ ];
23
+ const affectedComponents = new Set(["Action"]);
24
+ const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
25
+ const root = j(fileInfo.source, { parser: "tsx" });
26
+ // Local JSX identifier -> canonical component name (resolves `as` aliases).
27
+ const localToComponent = new Map();
28
+ // Local names of `import * as Flow` namespace imports from a Flow package.
29
+ const flowNamespaces = new Set();
30
+ root
31
+ .find(j.ImportDeclaration)
32
+ .filter((path) => isFlowImport(String(path.node.source.value)))
33
+ .forEach((path) => {
34
+ for (const specifier of path.node.specifiers ?? []) {
35
+ if (specifier.type === "ImportSpecifier" &&
36
+ specifier.imported.type === "Identifier" &&
37
+ affectedComponents.has(specifier.imported.name)) {
38
+ localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
39
+ }
40
+ else if (specifier.type === "ImportNamespaceSpecifier" &&
41
+ specifier.local) {
42
+ flowNamespaces.add(String(specifier.local.name));
43
+ }
44
+ }
45
+ });
46
+ if (localToComponent.size === 0 && flowNamespaces.size === 0) {
47
+ return fileInfo.source;
48
+ }
49
+ root.find(j.JSXOpeningElement).forEach((path) => {
50
+ const name = path.node.name;
51
+ let isAffected = false;
52
+ if (name.type === "JSXIdentifier") {
53
+ isAffected = localToComponent.has(name.name);
54
+ }
55
+ else if (name.type === "JSXMemberExpression" &&
56
+ name.object.type === "JSXIdentifier" &&
57
+ name.property.type === "JSXIdentifier") {
58
+ isAffected =
59
+ flowNamespaces.has(name.object.name) &&
60
+ affectedComponents.has(name.property.name);
61
+ }
62
+ if (!isAffected) {
63
+ return;
64
+ }
65
+ const attributes = path.node.attributes ?? [];
66
+ const isNamed = (attribute, key) => attribute.type === "JSXAttribute" &&
67
+ attribute.name.type === "JSXIdentifier" &&
68
+ attribute.name.name === key;
69
+ const hasOnAction = attributes.some((attribute) => isNamed(attribute, "onAction"));
70
+ if (hasOnAction) {
71
+ path.node.attributes = attributes.filter((attribute) => !isNamed(attribute, "action"));
72
+ return;
73
+ }
74
+ for (const attribute of attributes) {
75
+ if (isNamed(attribute, "action") && attribute.type === "JSXAttribute") {
76
+ attribute.name.name = "onAction";
77
+ }
78
+ }
79
+ });
80
+ return root.toSource();
81
+ };
82
+ exports.default = actionPropToOnActionTransform;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Renames the `Align` component to `Combine`, and `AlignProps` to
5
+ * `CombineProps`.
6
+ *
7
+ * Only names imported from `@mittwald/flow-react-components` or
8
+ * `@mittwald/flow-remote-react-components` (including their subpath entries)
9
+ * are touched, so a same-named import from another package is left alone. An
10
+ * `Align` imported under a local alias (`import { Align as Row }`) keeps its
11
+ * alias — only the imported name changes. Namespace usages (`<Flow.Align />`,
12
+ * `Flow.AlignProps`) are rewritten as well.
13
+ */
14
+ const alignToCombineTransform = (fileInfo, { j }) => {
15
+ const flowPackages = [
16
+ "@mittwald/flow-react-components",
17
+ "@mittwald/flow-remote-react-components",
18
+ ];
19
+ const renames = new Map([
20
+ ["Align", "Combine"],
21
+ ["AlignProps", "CombineProps"],
22
+ ]);
23
+ const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
24
+ // ast-types models `importKind` on the declaration only, while babel also
25
+ // puts it on the specifier — which is where a per-specifier `type X` lives.
26
+ const isTypeOnly = (specifier) => specifier.importKind === "type";
27
+ const makeValueImport = (specifier) => {
28
+ specifier.importKind = "value";
29
+ };
30
+ const root = j(fileInfo.source, { parser: "tsx" });
31
+ /** Local identifiers that refer to a renamed export, mapped to the new name. */
32
+ const localRenames = new Map();
33
+ /**
34
+ * Whether any import specifier was rewritten. An aliased `Align as Row` adds
35
+ * nothing to `localRenames` — its local name stays `Row` — so counting those
36
+ * alone would treat an alias-only file as untouched and discard the rewrite.
37
+ */
38
+ let renamedAnImport = false;
39
+ /** Local names of `import * as Flow` namespace imports from a Flow package. */
40
+ const flowNamespaces = new Set();
41
+ const flowImports = root
42
+ .find(j.ImportDeclaration)
43
+ .filter((path) => isFlowImport(String(path.node.source.value)));
44
+ /**
45
+ * `imported:local` pairs the file already binds without a rename. A rename
46
+ * that lands on one of these must not add a second specifier for it — the
47
+ * name is available from the import that already carries it, which is also
48
+ * the entry that really exports it.
49
+ */
50
+ const claimed = new Set();
51
+ flowImports.forEach((path) => {
52
+ for (const specifier of path.node.specifiers ?? []) {
53
+ if (specifier.type === "ImportNamespaceSpecifier" &&
54
+ specifier.local?.name) {
55
+ flowNamespaces.add(String(specifier.local.name));
56
+ continue;
57
+ }
58
+ if (specifier.type !== "ImportSpecifier" ||
59
+ specifier.imported.type !== "Identifier" ||
60
+ renames.has(specifier.imported.name)) {
61
+ continue;
62
+ }
63
+ claimed.add(`${specifier.imported.name}:${String(specifier.local?.name ?? specifier.imported.name)}`);
64
+ }
65
+ });
66
+ flowImports.forEach((path) => {
67
+ const specifiers = path.node.specifiers ?? [];
68
+ const survivors = [];
69
+ for (const specifier of specifiers) {
70
+ if (specifier.type !== "ImportSpecifier" ||
71
+ specifier.imported.type !== "Identifier") {
72
+ survivors.push(specifier);
73
+ continue;
74
+ }
75
+ const imported = specifier.imported.name;
76
+ const renamed = renames.get(imported);
77
+ if (!renamed) {
78
+ survivors.push(specifier);
79
+ continue;
80
+ }
81
+ const local = String(specifier.local?.name ?? imported);
82
+ const isAlias = local !== imported;
83
+ const newLocal = isAlias ? local : renamed;
84
+ const key = `${renamed}:${newLocal}`;
85
+ renamedAnImport = true;
86
+ if (!isAlias) {
87
+ // An aliased import keeps its local name and needs no further change.
88
+ localRenames.set(local, renamed);
89
+ }
90
+ // Renaming can collide with a name the file already binds — directly, or
91
+ // because a second old name maps onto the same new one. Either way the
92
+ // binding already exists, so this specifier goes away instead of
93
+ // producing a duplicate declaration, which would not parse.
94
+ const collision = claimed.has(key);
95
+ claimed.add(key);
96
+ if (collision) {
97
+ // A value import must not lose out to a type-only one.
98
+ if (!isTypeOnly(specifier)) {
99
+ for (const kept of survivors) {
100
+ if (kept.type === "ImportSpecifier" &&
101
+ kept.imported.type === "Identifier" &&
102
+ `${kept.imported.name}:${String(kept.local?.name ?? kept.imported.name)}` === key) {
103
+ makeValueImport(kept);
104
+ }
105
+ }
106
+ }
107
+ continue;
108
+ }
109
+ // Mutate in place so an `import type` / `type X` modifier survives.
110
+ specifier.imported.name = renamed;
111
+ if (!isAlias && specifier.local) {
112
+ specifier.local.name = renamed;
113
+ }
114
+ survivors.push(specifier);
115
+ }
116
+ if (survivors.length === specifiers.length) {
117
+ return;
118
+ }
119
+ // Every specifier moved to an import that already binds the name. Leaving
120
+ // the declaration behind would turn it into a side-effect import of an
121
+ // entry the file no longer uses.
122
+ if (survivors.length === 0) {
123
+ j(path).remove();
124
+ return;
125
+ }
126
+ path.node.specifiers = survivors;
127
+ });
128
+ if (!renamedAnImport && flowNamespaces.size === 0) {
129
+ return fileInfo.source;
130
+ }
131
+ /**
132
+ * `JSXIdentifier` extends `Identifier`, so this one pass covers JSX tags,
133
+ * value references and type references alike. Positions where the name is not
134
+ * a reference to the import — an object key, a member's property, a JSX
135
+ * attribute name — are skipped.
136
+ */
137
+ root.find(j.Identifier).forEach((path) => {
138
+ const parent = path.parent.node;
139
+ const isNamespaceMember = (parent.type === "MemberExpression" ||
140
+ parent.type === "JSXMemberExpression") &&
141
+ parent.property === path.node;
142
+ if (isNamespaceMember) {
143
+ const object = parent.object;
144
+ if (object.type === "Identifier" || object.type === "JSXIdentifier") {
145
+ const renamed = renames.get(path.node.name);
146
+ if (renamed && flowNamespaces.has(String(object.name))) {
147
+ path.node.name = renamed;
148
+ }
149
+ }
150
+ return;
151
+ }
152
+ if (parent.type === "ImportSpecifier" ||
153
+ parent.type === "JSXAttribute" ||
154
+ ((parent.type === "ObjectProperty" || parent.type === "Property") &&
155
+ parent.key === path.node &&
156
+ !parent.computed)) {
157
+ return;
158
+ }
159
+ const renamed = localRenames.get(path.node.name);
160
+ if (renamed) {
161
+ path.node.name = renamed;
162
+ }
163
+ });
164
+ return root.toSource();
165
+ };
166
+ exports.default = alignToCombineTransform;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Renames the `color="accent"` prop value to `color="success"` on `Button` and
5
+ * `SubmitButton`.
6
+ *
7
+ * The scope is deliberately narrow. Only JSX elements that resolve to one of
8
+ * those components — imported (named or as a namespace) from
9
+ * `@mittwald/flow-react-components` or
10
+ * `@mittwald/flow-remote-react-components`, including their subpath entries —
11
+ * are touched. Same-named components from other packages are left untouched.
12
+ *
13
+ * Only literal `"accent"` values are rewritten — either as the whole value
14
+ * (`color="accent"`, `color={"accent"}`) or in a value position inside a
15
+ * dynamic one (`color={cond ? "secondary" : "accent"}`, `color={override ??
16
+ * "accent"}`). A value the transform cannot see into (`color={someVariable}`)
17
+ * is left alone.
18
+ */
19
+ const buttonColorAccentToSuccessTransform = (fileInfo, { j }) => {
20
+ const flowPackages = [
21
+ "@mittwald/flow-react-components",
22
+ "@mittwald/flow-remote-react-components",
23
+ ];
24
+ const affectedComponents = new Set(["Button", "SubmitButton"]);
25
+ const isFlowImport = (source) => flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
26
+ const isAccentLiteral = (node) => {
27
+ if (!node || typeof node !== "object" || !("type" in node)) {
28
+ return false;
29
+ }
30
+ const { type } = node;
31
+ return ((type === "StringLiteral" || type === "Literal") &&
32
+ node.value === "accent");
33
+ };
34
+ /**
35
+ * Rewrites every `"accent"` sitting in a position whose value can reach the
36
+ * prop: the expression itself, both branches of a ternary, and the operands
37
+ * of `??`/`||` — plus the right operand of `&&`, since `&&` yields its left
38
+ * operand only when that operand is falsy and a non-empty string never is. A
39
+ * `"accent"` anywhere else is not a value that reaches the prop (an object
40
+ * key, an index into a lookup table) and stays as it is.
41
+ *
42
+ * This cannot move into a shared module: the CLI loads this file straight out
43
+ * of the published package, which ships only
44
+ * `src/migrations/**\/transform.ts` — see this package's AGENTS.md.
45
+ */
46
+ const rewriteValuePositions = (node) => {
47
+ if (isAccentLiteral(node)) {
48
+ return j.stringLiteral("success");
49
+ }
50
+ if (node?.type === "ConditionalExpression") {
51
+ node.consequent = rewriteValuePositions(node.consequent);
52
+ node.alternate = rewriteValuePositions(node.alternate);
53
+ return node;
54
+ }
55
+ if (node?.type === "LogicalExpression") {
56
+ if (node.operator !== "&&") {
57
+ node.left = rewriteValuePositions(node.left);
58
+ }
59
+ node.right = rewriteValuePositions(node.right);
60
+ return node;
61
+ }
62
+ return node;
63
+ };
64
+ const root = j(fileInfo.source, { parser: "tsx" });
65
+ // Local JSX identifier -> canonical component name (resolves `as` aliases).
66
+ const localToComponent = new Map();
67
+ // Local names of `import * as Flow` namespace imports from a Flow package.
68
+ const flowNamespaces = new Set();
69
+ root
70
+ .find(j.ImportDeclaration)
71
+ .filter((path) => isFlowImport(String(path.node.source.value)))
72
+ .forEach((path) => {
73
+ for (const specifier of path.node.specifiers ?? []) {
74
+ if (specifier.type === "ImportSpecifier" &&
75
+ specifier.imported.type === "Identifier" &&
76
+ affectedComponents.has(specifier.imported.name)) {
77
+ localToComponent.set(String(specifier.local?.name ?? specifier.imported.name), String(specifier.imported.name));
78
+ }
79
+ else if (specifier.type === "ImportNamespaceSpecifier" &&
80
+ specifier.local) {
81
+ flowNamespaces.add(String(specifier.local.name));
82
+ }
83
+ }
84
+ });
85
+ if (localToComponent.size === 0 && flowNamespaces.size === 0) {
86
+ return fileInfo.source;
87
+ }
88
+ root.find(j.JSXOpeningElement).forEach((path) => {
89
+ const name = path.node.name;
90
+ let isAffected = false;
91
+ if (name.type === "JSXIdentifier") {
92
+ isAffected = localToComponent.has(name.name);
93
+ }
94
+ else if (name.type === "JSXMemberExpression" &&
95
+ name.object.type === "JSXIdentifier" &&
96
+ name.property.type === "JSXIdentifier") {
97
+ isAffected =
98
+ flowNamespaces.has(name.object.name) &&
99
+ affectedComponents.has(name.property.name);
100
+ }
101
+ if (!isAffected) {
102
+ return;
103
+ }
104
+ for (const attribute of path.node.attributes ?? []) {
105
+ if (attribute.type !== "JSXAttribute" ||
106
+ attribute.name.type !== "JSXIdentifier" ||
107
+ attribute.name.name !== "color") {
108
+ continue;
109
+ }
110
+ const value = attribute.value;
111
+ // color="accent"
112
+ if (isAccentLiteral(value)) {
113
+ attribute.value = j.stringLiteral("success");
114
+ continue;
115
+ }
116
+ // color={"accent"}, and every value position inside a dynamic value:
117
+ // color={cond ? "accent" : "x"}, color={fallback ?? "accent"}.
118
+ if (value?.type === "JSXExpressionContainer" &&
119
+ value.expression.type !== "JSXEmptyExpression") {
120
+ value.expression = rewriteValuePositions(value.expression);
121
+ }
122
+ }
123
+ });
124
+ return root.toSource();
125
+ };
126
+ exports.default = buttonColorAccentToSuccessTransform;