@pantheon-systems/pds-toolkit-react 2.0.0-alpha.59 → 2.0.0-alpha.60

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 (31) hide show
  1. package/codemods/v1-to-v2/css-token-renames.js +186 -0
  2. package/codemods/v1-to-v2/mappings.json +85 -7
  3. package/codemods/v1-to-v2/transforms/__testfixtures__/css-class-removals/basic.input.tsx +24 -0
  4. package/codemods/v1-to-v2/transforms/__testfixtures__/css-class-removals/basic.output.tsx +24 -0
  5. package/codemods/v1-to-v2/transforms/__testfixtures__/css-class-renames/basic.input.tsx +29 -0
  6. package/codemods/v1-to-v2/transforms/__testfixtures__/css-class-renames/basic.output.tsx +29 -0
  7. package/codemods/v1-to-v2/transforms/css-class-removals.js +97 -27
  8. package/codemods/v1-to-v2/transforms/css-class-renames.js +80 -16
  9. package/dist/components/SiteFooter/SiteFooter.d.ts +7 -3
  10. package/dist/components/buttons/MenuButton/MenuButton.d.ts +4 -1
  11. package/dist/components/icons/Icon/generated-icon-data.d.ts +1 -1
  12. package/dist/css/component-css/pds-button.css +1 -3
  13. package/dist/css/component-css/pds-checkbox.css +1 -1
  14. package/dist/css/component-css/pds-cta-link.css +1 -1
  15. package/dist/css/component-css/pds-expansion-panel-group.css +1 -1
  16. package/dist/css/component-css/pds-expansion-panel.css +1 -1
  17. package/dist/css/component-css/pds-index.css +11 -13
  18. package/dist/css/component-css/pds-menu-button.css +1 -1
  19. package/dist/css/component-css/pds-side-nav.css +1 -1
  20. package/dist/css/component-css/pds-tab-menu.css +2 -2
  21. package/dist/css/component-css/pds-utility-button.css +1 -1
  22. package/dist/css/component-css/pds-workspace-selector.css +1 -1
  23. package/dist/css/design-tokens/variables.dark.css +6 -13
  24. package/dist/css/design-tokens/variables.global.css +1 -0
  25. package/dist/css/design-tokens/variables.light.css +7 -14
  26. package/dist/css/pds-components.css +11 -13
  27. package/dist/css/pds-core.css +1 -1
  28. package/dist/index.css +1 -1
  29. package/dist/index.js +148 -141
  30. package/dist/index.js.map +1 -1
  31. package/package.json +7 -6
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * css-token-renames
4
+ *
5
+ * Find-and-replace script for PDS design token renames in CSS and SCSS files.
6
+ * Driven entirely by tokens.renames and tokens.patterns in the published
7
+ * @pantheon-systems/pds-design-tokens codemods mappings.
8
+ *
9
+ * Handles:
10
+ * - Exact token renames: --pds-old-name → --pds-new-name
11
+ * - Wildcard patterns: --pds-badge-*-secondary → --pds-badge-*-muted
12
+ * - CSS utility classes: pds-color-bg-default → pds-color-surface-default
13
+ * (in CSS selectors; JSX className usage is handled by css-class-renames)
14
+ *
15
+ * Usage (from a consuming app, after installing @pantheon-systems/pds-toolkit-react):
16
+ * node node_modules/@pantheon-systems/pds-toolkit-react/codemods/v1-to-v2/css-token-renames.js <path>
17
+ * node node_modules/@pantheon-systems/pds-toolkit-react/codemods/v1-to-v2/css-token-renames.js src/apps/workspace/
18
+ *
19
+ * Usage (from a local checkout of this repo):
20
+ * node codemods/v1-to-v2/css-token-renames.js <path>
21
+ *
22
+ * Options:
23
+ * --dry Preview changes without writing files
24
+ * --only=<category> Restrict to a token category (e.g. animation, color, spacing)
25
+ * --ext=css,scss File extensions to scan (default: css,scss)
26
+ */
27
+
28
+ import fs from 'fs';
29
+ import path from 'path';
30
+ import { createRequire } from 'module';
31
+ import { globSync } from 'glob';
32
+
33
+ // JSON imports via createRequire — works in both CJS and ESM contexts
34
+ const require = createRequire(import.meta.url);
35
+ const mappings = require('@pantheon-systems/pds-design-tokens/codemods/v1-to-v2/mappings.json');
36
+
37
+ // --- Argument parsing ---
38
+
39
+ const args = process.argv.slice(2);
40
+ const dry = args.includes('--dry');
41
+ const onlyArg = args.find((a) => a.startsWith('--only='));
42
+ const extArg = args.find((a) => a.startsWith('--ext='));
43
+ const targetPaths = args.filter((a) => !a.startsWith('--'));
44
+
45
+ const onlyCategory = onlyArg ? onlyArg.replace('--only=', '').trim() : null;
46
+ const extensions = extArg
47
+ ? extArg
48
+ .replace('--ext=', '')
49
+ .split(',')
50
+ .map((e) => e.trim())
51
+ : ['css', 'scss'];
52
+
53
+ if (targetPaths.length === 0) {
54
+ console.error(
55
+ 'Usage: node codemods/v1-to-v2/css-token-renames.js <path> [--dry] [--only=category] [--ext=css,scss]',
56
+ );
57
+ process.exit(1);
58
+ }
59
+
60
+ // --- Build rename and pattern lists ---
61
+
62
+ function buildRenames() {
63
+ const exact = []; // { from, to }
64
+ const patterns = []; // { regex, replacement, from }
65
+
66
+ for (const entry of mappings.tokens.renames) {
67
+ if (!entry.from || !entry.to || entry.from === entry.to) continue;
68
+ if (onlyCategory && entry.category !== onlyCategory) continue;
69
+ exact.push({ from: entry.from, to: entry.to });
70
+ }
71
+
72
+ for (const entry of mappings.tokens.patterns) {
73
+ if (!entry.from || !entry.to || entry.from === entry.to) continue;
74
+ if (onlyCategory && entry.category !== onlyCategory) continue;
75
+
76
+ // Convert wildcard pattern to regex.
77
+ // Escape special regex chars except *, then replace * with a capture group.
78
+ const escaped = entry.from
79
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
80
+ .replace(/\*/g, '([\\w-]+)');
81
+ const regex = new RegExp(escaped, 'g');
82
+
83
+ // Build replacement — each * in `to` becomes $1, $2, etc.
84
+ let captureIndex = 0;
85
+ const replacement = entry.to.replace(/\*/g, () => `$${++captureIndex}`);
86
+
87
+ patterns.push({ regex, replacement, from: entry.from });
88
+ }
89
+
90
+ return { exact, patterns };
91
+ }
92
+
93
+ const { exact: EXACT_RENAMES, patterns: PATTERNS } = buildRenames();
94
+
95
+ // --- File discovery ---
96
+
97
+ function findFiles(targetPath) {
98
+ const extPattern =
99
+ extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
100
+
101
+ if (fs.existsSync(targetPath) && fs.statSync(targetPath).isFile()) {
102
+ return [targetPath];
103
+ }
104
+
105
+ if (fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
106
+ return globSync(`**/*.${extPattern}`, {
107
+ cwd: targetPath,
108
+ absolute: true,
109
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
110
+ });
111
+ }
112
+
113
+ return globSync(targetPath, {
114
+ absolute: true,
115
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
116
+ });
117
+ }
118
+
119
+ // --- Apply renames to file content ---
120
+
121
+ function applyRenames(content) {
122
+ let result = content;
123
+ let changed = false;
124
+ const appliedRenames = [];
125
+
126
+ // Exact renames
127
+ for (const { from, to } of EXACT_RENAMES) {
128
+ if (!result.includes(from)) continue;
129
+ const regex = new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
130
+ const next = result.replace(regex, to);
131
+ if (next !== result) {
132
+ appliedRenames.push(` ${from} → ${to}`);
133
+ result = next;
134
+ changed = true;
135
+ }
136
+ }
137
+
138
+ // Wildcard patterns
139
+ for (const { regex, replacement, from } of PATTERNS) {
140
+ regex.lastIndex = 0;
141
+ const next = result.replace(regex, replacement);
142
+ if (next !== result) {
143
+ appliedRenames.push(` ${from} → ${replacement} (pattern)`);
144
+ result = next;
145
+ changed = true;
146
+ }
147
+ }
148
+
149
+ return { result, changed, appliedRenames };
150
+ }
151
+
152
+ // --- Main ---
153
+
154
+ let totalFiles = 0;
155
+ let changedFiles = 0;
156
+ let totalRenames = 0;
157
+
158
+ const allFiles = targetPaths.flatMap(findFiles);
159
+
160
+ if (allFiles.length === 0) {
161
+ console.log('No CSS/SCSS files found matching the given path(s).');
162
+ process.exit(0);
163
+ }
164
+
165
+ for (const filePath of allFiles) {
166
+ const content = fs.readFileSync(filePath, 'utf8');
167
+ const { result, changed, appliedRenames } = applyRenames(content);
168
+
169
+ totalFiles++;
170
+ if (!changed) continue;
171
+
172
+ changedFiles++;
173
+ totalRenames += appliedRenames.length;
174
+
175
+ console.log(
176
+ `${dry ? '[dry] ' : ''}${path.relative(process.cwd(), filePath)}`,
177
+ );
178
+ appliedRenames.forEach((r) => console.log(r));
179
+
180
+ if (!dry) fs.writeFileSync(filePath, result, 'utf8');
181
+ }
182
+
183
+ console.log(
184
+ `\n${dry ? '[dry run] ' : ''}${changedFiles}/${totalFiles} files changed, ${totalRenames} renames applied.`,
185
+ );
186
+ if (dry && changedFiles > 0) console.log('Run without --dry to apply changes.');
@@ -696,13 +696,6 @@
696
696
  "replacement": "Use variant=\"link\"",
697
697
  "reason": "The 'inline' button variant has been renamed to 'link' to describe its appearance (a text link) rather than its placement, and is no longer treated as legacy."
698
698
  },
699
- {
700
- "component": "MenuButton",
701
- "prop": "variant",
702
- "oldValue": "navbar",
703
- "replacement": "Use variant=\"subtle\"",
704
- "reason": "The navbar variant has been removed in v2. Use subtle instead. The expanded-state transparency and icon-color behavior previously specific to the navbar variant now applies to all subtle MenuButton triggers automatically."
705
- },
706
699
  {
707
700
  "component": "IconButton",
708
701
  "prop": "variant",
@@ -1350,6 +1343,91 @@
1350
1343
  "old": "pds-overline-text--lg",
1351
1344
  "new": "pds-overline-l",
1352
1345
  "reason": "Overline text size variants replaced with standalone size-based classes."
1346
+ },
1347
+ {
1348
+ "old": "pds-color-bg-default",
1349
+ "new": "pds-color-surface-default",
1350
+ "reason": "Background utility classes renamed to the surface naming convention. Mirrors the CSS custom property rename of the same name."
1351
+ },
1352
+ {
1353
+ "old": "pds-color-bg-default-secondary",
1354
+ "new": "pds-color-surface-default-secondary",
1355
+ "reason": "Background utility classes renamed to the surface naming convention. Mirrors the CSS custom property rename of the same name."
1356
+ },
1357
+ {
1358
+ "old": "pds-color-bg-reverse",
1359
+ "new": "pds-color-surface-reverse",
1360
+ "reason": "Background utility classes renamed to the surface naming convention. Mirrors the CSS custom property rename of the same name."
1361
+ },
1362
+ {
1363
+ "old": "pds-color-bg-transparent",
1364
+ "new": "pds-color-surface-transparent",
1365
+ "reason": "Background utility classes renamed to the surface naming convention."
1366
+ },
1367
+ {
1368
+ "old": "pds-color-bg-info",
1369
+ "new": "pds-color-surface-info",
1370
+ "reason": "Status background utility classes renamed to the surface naming convention."
1371
+ },
1372
+ {
1373
+ "old": "pds-color-bg-success",
1374
+ "new": "pds-color-surface-success",
1375
+ "reason": "Status background utility classes renamed to the surface naming convention."
1376
+ },
1377
+ {
1378
+ "old": "pds-color-bg-warning",
1379
+ "new": "pds-color-surface-warning",
1380
+ "reason": "Status background utility classes renamed to the surface naming convention."
1381
+ },
1382
+ {
1383
+ "old": "pds-color-bg-critical",
1384
+ "new": "pds-color-surface-critical",
1385
+ "reason": "Status background utility classes renamed to the surface naming convention."
1386
+ },
1387
+ {
1388
+ "old": "pds-color-bg-discovery",
1389
+ "new": "pds-color-surface-discovery",
1390
+ "reason": "Status background utility classes renamed to the surface naming convention."
1391
+ },
1392
+ {
1393
+ "old": "pds-color-fg-default",
1394
+ "new": "pds-color-foreground-default",
1395
+ "reason": "Foreground utility classes renamed to the foreground naming convention. Mirrors the CSS custom property rename of the same name."
1396
+ },
1397
+ {
1398
+ "old": "pds-color-fg-default-secondary",
1399
+ "new": "pds-color-foreground-default-secondary",
1400
+ "reason": "Foreground utility classes renamed to the foreground naming convention. Mirrors the CSS custom property rename of the same name."
1401
+ },
1402
+ {
1403
+ "old": "pds-color-fg-reverse",
1404
+ "new": "pds-color-foreground-reverse",
1405
+ "reason": "Foreground utility classes renamed to the foreground naming convention. Mirrors the CSS custom property rename of the same name."
1406
+ },
1407
+ {
1408
+ "old": "pds-color-fg-info",
1409
+ "new": "pds-color-foreground-info",
1410
+ "reason": "Status foreground utility classes renamed to the foreground naming convention."
1411
+ },
1412
+ {
1413
+ "old": "pds-color-fg-success",
1414
+ "new": "pds-color-foreground-success",
1415
+ "reason": "Status foreground utility classes renamed to the foreground naming convention."
1416
+ },
1417
+ {
1418
+ "old": "pds-color-fg-warning",
1419
+ "new": "pds-color-foreground-warning",
1420
+ "reason": "Status foreground utility classes renamed to the foreground naming convention."
1421
+ },
1422
+ {
1423
+ "old": "pds-color-fg-critical",
1424
+ "new": "pds-color-foreground-critical",
1425
+ "reason": "Status foreground utility classes renamed to the foreground naming convention."
1426
+ },
1427
+ {
1428
+ "old": "pds-color-fg-discovery",
1429
+ "new": "pds-color-foreground-discovery",
1430
+ "reason": "Status foreground utility classes renamed to the foreground naming convention."
1353
1431
  }
1354
1432
  ],
1355
1433
  "removals": [
@@ -29,3 +29,27 @@ const J = () => <div className='pds-button--subtle' />;
29
29
  // Dynamic class — cannot transform, leave as-is
30
30
  const cls = 'pds-button--navbar';
31
31
  const K = () => <div className={cls} />;
32
+
33
+ // Bare ternary — direct rename in one branch, safe removal in the other
34
+ const L = ({ active }: { active: boolean }) => (
35
+ <div
36
+ className={active ? 'pds-button--navbar' : 'pds-typography--product'}
37
+ />
38
+ );
39
+
40
+ // Bare logical expression — right-hand side is a flagged class
41
+ const M = ({ isOpen }: { isOpen: boolean }) => (
42
+ <div className={isOpen && 'pds-color-bg-brand'} />
43
+ );
44
+
45
+ // Ternary nested inside a template literal interpolation
46
+ const N = ({ isEmployee }: { isEmployee: boolean }) => (
47
+ <div
48
+ className={`chat-bubble ${
49
+ isEmployee ? 'pds-button--navbar' : 'pds-ts-9xl'
50
+ }`}
51
+ />
52
+ );
53
+
54
+ // Non-className prop that still ends in "ClassName"
55
+ const O = () => <Icon iconClassName='pds-button--navbar' />;
@@ -29,3 +29,27 @@ const J = () => <div className='pds-button--subtle' />;
29
29
  // Dynamic class — cannot transform, leave as-is
30
30
  const cls = 'pds-button--navbar';
31
31
  const K = () => <div className={cls} />;
32
+
33
+ // Bare ternary — direct rename in one branch, safe removal in the other
34
+ const L = ({ active }: { active: boolean }) => (
35
+ <div
36
+ className={active ? "pds-button--subtle" : ""}
37
+ />
38
+ );
39
+
40
+ // Bare logical expression — right-hand side is a flagged class
41
+ const M = ({ isOpen }: { isOpen: boolean }) => (
42
+ <div className={isOpen && 'pds-color-bg-brand'} />
43
+ );
44
+
45
+ // Ternary nested inside a template literal interpolation
46
+ const N = ({ isEmployee }: { isEmployee: boolean }) => (
47
+ <div
48
+ className={`chat-bubble ${
49
+ isEmployee ? "pds-button--subtle" : "pds-ts-8xl"
50
+ }`}
51
+ />
52
+ );
53
+
54
+ // Non-className prop that still ends in "ClassName"
55
+ const O = () => <Icon iconClassName="pds-button--subtle" />;
@@ -27,5 +27,34 @@ const G = () => <div className='pds-overline-m' />;
27
27
  const cls = 'pds-overline-text';
28
28
  const H = () => <div className={cls} />;
29
29
 
30
+ // Bare ternary — both branches are string literals
31
+ const J = ({ active }: { active: boolean }) => (
32
+ <div className={active ? 'pds-overline-text' : 'pds-overline-text--sm'} />
33
+ );
34
+
35
+ // Bare logical expression — right-hand side is a string literal
36
+ const K = ({ isOpen }: { isOpen: boolean }) => (
37
+ <div className={isOpen && 'pds-overline-text'} />
38
+ );
39
+
40
+ // Ternary nested inside a template literal interpolation
41
+ const L = ({ isEmployee }: { isEmployee: boolean }) => (
42
+ <div
43
+ className={`chat-bubble ${
44
+ isEmployee ? 'pds-overline-text--sm' : 'pds-overline-text--lg'
45
+ }`}
46
+ />
47
+ );
48
+
49
+ // Ternary with one literal branch and one dynamic branch — only the literal
50
+ // branch can be renamed; the dynamic branch is left untouched
51
+ const M = ({ active, fallbackClass }: { active: boolean; fallbackClass: string }) => (
52
+ <div className={active ? 'pds-overline-text' : fallbackClass} />
53
+ );
54
+
55
+ // Non-className prop that still ends in "ClassName" — common for components
56
+ // with multiple styleable slots
57
+ const N = () => <Icon iconClassName='pds-overline-text' />;
58
+
30
59
  // Non-className attribute — should be left alone
31
60
  const I = () => <div id='pds-overline-text' />;
@@ -27,5 +27,34 @@ const G = () => <div className='pds-overline-m' />;
27
27
  const cls = 'pds-overline-text';
28
28
  const H = () => <div className={cls} />;
29
29
 
30
+ // Bare ternary — both branches are string literals
31
+ const J = ({ active }: { active: boolean }) => (
32
+ <div className={active ? "pds-overline-m" : "pds-overline-s"} />
33
+ );
34
+
35
+ // Bare logical expression — right-hand side is a string literal
36
+ const K = ({ isOpen }: { isOpen: boolean }) => (
37
+ <div className={isOpen && "pds-overline-m"} />
38
+ );
39
+
40
+ // Ternary nested inside a template literal interpolation
41
+ const L = ({ isEmployee }: { isEmployee: boolean }) => (
42
+ <div
43
+ className={`chat-bubble ${
44
+ isEmployee ? "pds-overline-s" : "pds-overline-l"
45
+ }`}
46
+ />
47
+ );
48
+
49
+ // Ternary with one literal branch and one dynamic branch — only the literal
50
+ // branch can be renamed; the dynamic branch is left untouched
51
+ const M = ({ active, fallbackClass }: { active: boolean; fallbackClass: string }) => (
52
+ <div className={active ? "pds-overline-m" : fallbackClass} />
53
+ );
54
+
55
+ // Non-className prop that still ends in "ClassName" — common for components
56
+ // with multiple styleable slots
57
+ const N = () => <Icon iconClassName="pds-overline-m" />;
58
+
30
59
  // Non-className attribute — should be left alone
31
60
  const I = () => <div id='pds-overline-text' />;
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Codemod: css-class-removals
3
3
  *
4
- * Handles removed PDS CSS utility classes in JSX className attributes.
4
+ * Handles removed PDS CSS utility classes in JSX className attributes — and
5
+ * any other prop ending in "ClassName" (e.g. iconClassName, labelClassName),
6
+ * a common pattern for components with multiple styleable slots.
5
7
  * Driven entirely by cssClasses.removals in mappings.json.
6
8
  *
7
9
  * Three kinds of change:
@@ -21,6 +23,17 @@
21
23
  * these manually. Removing them silently would break visible styles with
22
24
  * no clear path forward.
23
25
  *
26
+ * Handles ternaries / logical expressions, as a bare className expression or
27
+ * nested inside a template literal's ${...} slot, as long as every branch
28
+ * reached is itself a plain string literal:
29
+ * className={isActive ? 'pds-button--navbar' : 'other-class'}
30
+ * className={`base ${isEmployee ? 'pds-lead-text' : 'pds-ts-9xl'}`}
31
+ * className={isOpen && 'pds-button--navbar'}
32
+ *
33
+ * Does not handle truly dynamic class names (a variable, function call, or
34
+ * any branch of a ternary/logical expression that isn't itself a string
35
+ * literal).
36
+ *
24
37
  * Usage:
25
38
  * npx jscodeshift -t codemods/v1-to-v2/transforms/css-class-removals.js \
26
39
  * --extensions=tsx,ts,jsx,js \
@@ -30,6 +43,7 @@
30
43
  */
31
44
 
32
45
  const path = require('path');
46
+
33
47
  const mappings = require(path.join(__dirname, '../mappings.json'));
34
48
 
35
49
  // Extract the first pds-* class name from a replacement string, if present.
@@ -71,11 +85,11 @@ function buildLookups() {
71
85
  return { renames, removals, flags };
72
86
  }
73
87
 
74
- const { renames: RENAMES, removals: REMOVALS, flags: FLAGS } = buildLookups();
88
+ const { flags: FLAGS, removals: REMOVALS, renames: RENAMES } = buildLookups();
75
89
 
76
90
  // Process a space-separated class string.
77
91
  // Returns { result, changed, flagged[] }
78
- function processClasses(classString, filePath) {
92
+ function processClasses(classString) {
79
93
  let changed = false;
80
94
  const flagged = [];
81
95
 
@@ -107,6 +121,52 @@ function processClasses(classString, filePath) {
107
121
  return { result: normalised, changed: didChange, flagged };
108
122
  }
109
123
 
124
+ // Recursively process any string-literal branch reachable from `node`
125
+ // without crossing into a non-literal value — i.e. it descends into
126
+ // ternaries (ConditionalExpression) and logical expressions (&&/||),
127
+ // processing each branch that is itself a string literal, but leaves
128
+ // anything else (a variable, a function call, a member expression, ...)
129
+ // untouched since its value isn't knowable statically. Returns the
130
+ // (possibly replaced) node, whether anything changed, and any flagged
131
+ // classes found along the way. Does not mutate in place, so callers must
132
+ // reassign the returned node back onto its parent.
133
+ function processStringLiteralBranches(node, j) {
134
+ if (!node) return { node, changed: false, flagged: [] };
135
+
136
+ if (node.type === 'StringLiteral' || node.type === 'Literal') {
137
+ const { changed, flagged, result } = processClasses(node.value);
138
+ return changed
139
+ ? { node: j.stringLiteral(result), changed: true, flagged }
140
+ : { node, changed: false, flagged };
141
+ }
142
+
143
+ if (node.type === 'ConditionalExpression') {
144
+ const consequent = processStringLiteralBranches(node.consequent, j);
145
+ const alternate = processStringLiteralBranches(node.alternate, j);
146
+ if (consequent.changed) node.consequent = consequent.node;
147
+ if (alternate.changed) node.alternate = alternate.node;
148
+ return {
149
+ node,
150
+ changed: consequent.changed || alternate.changed,
151
+ flagged: [...consequent.flagged, ...alternate.flagged],
152
+ };
153
+ }
154
+
155
+ if (node.type === 'LogicalExpression') {
156
+ const left = processStringLiteralBranches(node.left, j);
157
+ const right = processStringLiteralBranches(node.right, j);
158
+ if (left.changed) node.left = left.node;
159
+ if (right.changed) node.right = right.node;
160
+ return {
161
+ node,
162
+ changed: left.changed || right.changed,
163
+ flagged: [...left.flagged, ...right.flagged],
164
+ };
165
+ }
166
+
167
+ return { node, changed: false, flagged: [] };
168
+ }
169
+
110
170
  module.exports = function transform(file, api) {
111
171
  const j = api.jscodeshift;
112
172
  const root = j(file.source);
@@ -115,18 +175,15 @@ module.exports = function transform(file, api) {
115
175
  let changed = false;
116
176
 
117
177
  root
118
- .find(j.JSXAttribute, { name: { name: 'className' } })
178
+ .find(j.JSXAttribute)
179
+ .filter((nodePath) => /className$/i.test(nodePath.node.name.name))
119
180
  .forEach((nodePath) => {
120
181
  const { value } = nodePath.node;
121
182
  if (!value) return;
122
183
 
123
184
  // className="pds-button--navbar foo"
124
185
  if (value.type === 'StringLiteral') {
125
- const {
126
- result,
127
- changed: c,
128
- flagged,
129
- } = processClasses(value.value, file.path);
186
+ const { changed: c, flagged, result } = processClasses(value.value);
130
187
  if (c) {
131
188
  nodePath.node.value = j.stringLiteral(result);
132
189
  changed = true;
@@ -138,29 +195,14 @@ module.exports = function transform(file, api) {
138
195
  if (value.type !== 'JSXExpressionContainer') return;
139
196
  const { expression } = value;
140
197
 
141
- // className={'pds-button--navbar'}
142
- if (expression.type === 'StringLiteral') {
143
- const {
144
- result,
145
- changed: c,
146
- flagged,
147
- } = processClasses(expression.value, file.path);
148
- if (c) {
149
- value.expression = j.stringLiteral(result);
150
- changed = true;
151
- }
152
- flagged.forEach((cls) => allFlagged.push(cls));
153
- return;
154
- }
155
-
156
- // className={`pds-button--navbar ${x}`}
198
+ // className={`pds-button--navbar ${isActive ? 'a' : 'b'}`}
157
199
  if (expression.type === 'TemplateLiteral') {
158
200
  expression.quasis.forEach((quasi) => {
159
201
  const {
160
- result,
161
202
  changed: c,
162
203
  flagged,
163
- } = processClasses(quasi.value.raw, file.path);
204
+ result,
205
+ } = processClasses(quasi.value.raw);
164
206
  if (c) {
165
207
  quasi.value.raw = result;
166
208
  quasi.value.cooked = result;
@@ -168,7 +210,35 @@ module.exports = function transform(file, api) {
168
210
  }
169
211
  flagged.forEach((cls) => allFlagged.push(cls));
170
212
  });
213
+
214
+ expression.expressions.forEach((expr, index) => {
215
+ const {
216
+ changed: c,
217
+ flagged,
218
+ node: processed,
219
+ } = processStringLiteralBranches(expr, j);
220
+ if (c) {
221
+ expression.expressions[index] = processed;
222
+ changed = true;
223
+ }
224
+ flagged.forEach((cls) => allFlagged.push(cls));
225
+ });
226
+ return;
227
+ }
228
+
229
+ // className={'pds-button--navbar'}
230
+ // className={isActive ? 'pds-button--navbar' : 'other-class'}
231
+ // className={isOpen && 'pds-button--navbar'}
232
+ const {
233
+ changed: c,
234
+ flagged,
235
+ node: processed,
236
+ } = processStringLiteralBranches(expression, j);
237
+ if (c) {
238
+ value.expression = processed;
239
+ changed = true;
171
240
  }
241
+ flagged.forEach((cls) => allFlagged.push(cls));
172
242
  });
173
243
 
174
244
  // Emit warnings for classes that need manual attention