Skip to content

Commit 8194e17

Browse files
authored
Create Sass color() function migration (#7375)
### WHY are these changes introduced? Helps migrate away the legacy Sass `color()` function with the appropriate CSS custom property color token. Related #7215 ### WHAT is this pull request doing? Adds the `replace-sass-color` migration: ```diff - color: color('ink'); - background: color('white'); + color: var(--p-text); + background: var(--p-surface); ```
1 parent 7a78e07 commit 8194e17

File tree

9 files changed

+466
-0
lines changed

9 files changed

+466
-0
lines changed

.changeset/twenty-mayflies-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris-migrator': minor
3+
---
4+
5+
Add Sass color function migration

polaris-migrator/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ For projects that use the [`@use` rule](https://sass-lang.com/documentation/at-r
7676
npx @shopify/polaris-migrator <sass-migration> <path> --namespace="legacy-polaris-v8"
7777
```
7878

79+
### `replace-sass-color`
80+
81+
Replace the legacy Sass `color()` function with the supported CSS custom property token equivalent (ex: `var(--p-surface)`). This will only replace a limited subset of mapped values. See the [color-maps.ts](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/migrations/replace-sass-color/color-maps.ts) for a full list of color mappings based on the CSS property.
82+
83+
```diff
84+
- color: color('ink');
85+
- background: color('white');
86+
+ color: var(--p-text);
87+
+ background: var(--p-surface);
88+
```
89+
90+
```sh
91+
npx @shopify/polaris-migrator replace-sass-color <path>
92+
```
93+
7994
### `replace-sass-spacing`
8095

8196
Replace the legacy Sass `spacing()` function with the supported CSS custom property token equivalent (ex: `var(--p-space-4)`).
@@ -354,3 +369,4 @@ git reset $(grep -r -l "polaris-migrator:")
354369
- Common utilities:
355370
- [`jsx.ts`](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/utilities/jsx.ts)
356371
- [`imports.ts`](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/utilities/imports.ts)
372+
0
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import type {FileInfo, API, Options} from 'jscodeshift';
2+
import postcss, {Plugin} from 'postcss';
3+
import valueParser from 'postcss-value-parser';
4+
import {colors as tokenColors, createVar} from '@shopify/polaris-tokens';
5+
6+
import {
7+
NamespaceOptions,
8+
namespace,
9+
getFunctionArgs,
10+
stripQuotes,
11+
StopWalkingFunctionNodes,
12+
} from '../../utilities/sass';
13+
import {isKeyOf} from '../../utilities/type-guards';
14+
15+
export default function replaceSassColors(
16+
file: FileInfo,
17+
_: API,
18+
options: Options,
19+
) {
20+
return postcss(plugin(options)).process(file.source, {
21+
syntax: require('postcss-scss'),
22+
}).css;
23+
}
24+
25+
const processed = Symbol('processed');
26+
27+
interface PluginOptions extends Options, NamespaceOptions {}
28+
29+
const plugin = (options: PluginOptions = {}): Plugin => {
30+
const namespacedColor = namespace('color', options);
31+
32+
return {
33+
postcssPlugin: 'replace-sass-color',
34+
Declaration(decl) {
35+
// @ts-expect-error - Skip if processed so we don't process it again
36+
if (decl[processed]) return;
37+
38+
if (!isKeyOf(propertyMaps, decl.prop)) return;
39+
const replacementMap = propertyMaps[decl.prop];
40+
const parsed = valueParser(decl.value);
41+
42+
parsed.walk((node) => {
43+
if (node.type !== 'function') return;
44+
45+
if (node.value === 'rgba') {
46+
return StopWalkingFunctionNodes;
47+
}
48+
49+
// 1. Remove color() fallbacks
50+
if (node.value === 'var') {
51+
const args = getFunctionArgs(node);
52+
const polarisCustomPropertyIndex = args.findIndex((arg) =>
53+
polarisCustomPropertyRegEx.test(arg),
54+
);
55+
const colorFnFallbackIndex = args.findIndex((arg) =>
56+
arg.startsWith(namespacedColor),
57+
);
58+
59+
if (polarisCustomPropertyIndex < colorFnFallbackIndex) {
60+
node.nodes = [node.nodes[0]];
61+
}
62+
63+
return StopWalkingFunctionNodes;
64+
}
65+
66+
// 2. Replace `color()` with variable
67+
if (node.value === namespacedColor) {
68+
const colorFnArgs = getFunctionArgs(node).map(stripQuotes);
69+
const hue = colorFnArgs[0] ?? '';
70+
const value = colorFnArgs[1] ?? 'base';
71+
const forBackground = colorFnArgs[2];
72+
73+
// Skip color() with for-background argument
74+
if (forBackground) return;
75+
76+
// Skip if we don't have a color for the hue and value
77+
if (
78+
!(
79+
isKeyOf(replacementMap, hue) &&
80+
isKeyOf(replacementMap[hue], value)
81+
)
82+
)
83+
return;
84+
85+
const colorCustomProperty: string = replacementMap[hue][value];
86+
87+
node.value = 'var';
88+
node.nodes = [
89+
{
90+
type: 'word',
91+
value: colorCustomProperty,
92+
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
93+
sourceEndIndex: colorCustomProperty.length,
94+
},
95+
];
96+
}
97+
});
98+
99+
decl.value = parsed.toString();
100+
101+
// @ts-expect-error - Mark the declaration as processed
102+
decl[processed] = true;
103+
},
104+
};
105+
};
106+
107+
/*
108+
* See the legacy Sass API file for the original color palette
109+
* documentation/guides/legacy-polaris-v8-public-api.scss
110+
*/
111+
112+
const colorMap = {
113+
blue: {
114+
dark: '--p-interactive-hovered',
115+
base: '--p-interactive',
116+
},
117+
green: {
118+
dark: '--p-text-success',
119+
base: '--p-text-success',
120+
},
121+
yellow: {
122+
dark: '--p-text-warning',
123+
base: '--p-text-warning',
124+
},
125+
red: {
126+
dark: '--p-text-critical',
127+
base: '--p-text-critical',
128+
},
129+
ink: {
130+
base: '--p-text',
131+
light: '--p-text-subdued',
132+
lighter: '--p-text-subdued',
133+
lightest: '--p-text-subdued',
134+
},
135+
sky: {
136+
dark: '--p-text-subdued-on-dark',
137+
base: '--p-text-on-dark',
138+
light: '--p-text-on-dark',
139+
lighter: '--p-text-on-dark',
140+
},
141+
black: {
142+
base: '--p-text',
143+
},
144+
white: {
145+
base: '--p-text-on-dark',
146+
},
147+
};
148+
149+
const backgroundColorMap = {
150+
green: {
151+
light: '--p-surface-success',
152+
lighter: '--p-surface-success-subdued',
153+
},
154+
yellow: {
155+
light: '--p-surface-warning',
156+
lighter: '--p-surface-warning-subdued',
157+
},
158+
red: {
159+
light: '--p-surface-critical',
160+
lighter: '--p-surface-critical-subdued',
161+
},
162+
ink: {
163+
dark: '--p-surface-dark',
164+
base: '--p-surface-neutral-subdued-dark',
165+
},
166+
sky: {
167+
base: '--p-surface-neutral',
168+
light: '--p-surface-neutral-subdued',
169+
lighter: '--p-surface-subdued',
170+
},
171+
black: {
172+
base: '--p-surface-dark',
173+
},
174+
white: {
175+
base: '--p-surface',
176+
},
177+
};
178+
179+
const borderColorMap = {
180+
green: {
181+
dark: '--p-border-success',
182+
base: '--p-border-success',
183+
light: '--p-border-success-subdued',
184+
lighter: '--p-border-success-subdued',
185+
},
186+
yellow: {
187+
dark: '--p-border-warning',
188+
base: '--p-border-warning',
189+
light: '--p-border-warning-disabled',
190+
lighter: '--p-border-warning-subdued',
191+
},
192+
red: {
193+
dark: '--p-border-critical',
194+
base: '--p-border-critical',
195+
light: '--p-border-critical-subdued',
196+
lighter: '--p-border-critical-subdued',
197+
},
198+
ink: {
199+
lightest: '--p-border',
200+
},
201+
sky: {
202+
light: '--p-border-subdued',
203+
},
204+
};
205+
206+
const fillColorMap = {
207+
green: {
208+
dark: '--p-icon-success',
209+
base: '--p-icon-success',
210+
},
211+
yellow: {
212+
dark: '--p-icon-warning',
213+
base: '--p-icon-warning',
214+
},
215+
red: {
216+
dark: '--p-icon-critical',
217+
base: '--p-icon-critical',
218+
},
219+
ink: {
220+
base: '--p-icon',
221+
light: '--p-icon',
222+
lighter: '--p-icon-subdued',
223+
lightest: '--p-icon-disabled',
224+
},
225+
black: {
226+
base: '--p-icon',
227+
},
228+
white: {
229+
base: '--p-icon-on-dark',
230+
},
231+
};
232+
233+
const propertyMaps = {
234+
color: colorMap,
235+
background: backgroundColorMap,
236+
'background-color': backgroundColorMap,
237+
border: borderColorMap,
238+
'border-color': borderColorMap,
239+
fill: fillColorMap,
240+
};
241+
242+
const polarisCustomPropertyRegEx = new RegExp(
243+
Object.keys(tokenColors).map(createVar).join('|'),
244+
);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
.my-class {
2+
// color
3+
color: color('blue');
4+
color: color('blue', 'dark');
5+
color: color('green');
6+
color: color('green', 'dark');
7+
color: color('yellow');
8+
color: color('yellow', 'dark');
9+
color: color('red');
10+
color: color('red', 'dark');
11+
color: color('ink');
12+
color: color('ink', 'light');
13+
color: color('ink', 'lighter');
14+
color: color('ink', 'lightest');
15+
color: color('sky');
16+
color: color('sky', 'dark');
17+
color: color('sky', 'light');
18+
color: color('sky', 'lighter');
19+
color: color('black');
20+
color: color('white');
21+
22+
// background
23+
background-color: color('green', 'light');
24+
background-color: color('green', 'lighter');
25+
background-color: color('yellow', 'light');
26+
background-color: color('yellow', 'lighter');
27+
background-color: color('red', 'light');
28+
background-color: color('red', 'lighter');
29+
background-color: color('ink');
30+
background-color: color('ink', 'dark');
31+
background-color: color('sky');
32+
background-color: color('sky', 'light');
33+
background-color: color('sky', 'lighter');
34+
background-color: color('black');
35+
background-color: color('white');
36+
37+
// border
38+
border-color: color('green', 'dark');
39+
border-color: color('green');
40+
border-color: color('green', 'light');
41+
border-color: color('green', 'lighter');
42+
border-color: color('yellow', 'dark');
43+
border-color: color('yellow');
44+
border-color: color('yellow', 'light');
45+
border-color: color('yellow', 'lighter');
46+
border-color: color('red', 'dark');
47+
border-color: color('red');
48+
border-color: color('red', 'light');
49+
border-color: color('red', 'lighter');
50+
border-color: color('ink', 'lightest');
51+
border-color: color('sky', 'light');
52+
53+
// fill
54+
fill: color('green', 'dark');
55+
fill: color('green');
56+
fill: color('yellow', 'dark');
57+
fill: color('yellow');
58+
fill: color('red', 'dark');
59+
fill: color('red');
60+
fill: color('ink');
61+
fill: color('ink', 'light');
62+
fill: color('ink', 'lighter');
63+
fill: color('ink', 'lightest');
64+
fill: color('black');
65+
fill: color('white');
66+
67+
// Remove color() fallbacks
68+
color: var(--p-text, color('white'));
69+
70+
// Handle declarations with multiple values
71+
border: var(--p-border-width-1) solid color('ink', 'lightest');
72+
background: border-box color('sky', 'light');
73+
74+
// Skip color() with a for-background argument
75+
color: color('ink', 'lighter', #f2ece4);
76+
77+
// Skip replacing color() within a function
78+
background: rgba(color('black'), 0.5);
79+
}

0 commit comments

Comments
 (0)