Skip to content
5 changes: 5 additions & 0 deletions .changeset/twenty-mayflies-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris-migrator': minor
---

Add Sass color function migration
16 changes: 16 additions & 0 deletions polaris-migrator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ For projects that use the [`@use` rule](https://sass-lang.com/documentation/at-r
npx @shopify/polaris-migrator <sass-migration> <path> --namespace="legacy-polaris-v8"
```

### `replace-sass-color`

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.

```diff
- color: color('ink');
- background: color('white');
+ color: var(--p-text);
+ background: var(--p-surface);
```

```sh
npx @shopify/polaris-migrator replace-sass-color <path>
```

### `replace-sass-spacing`

Replace the legacy Sass `spacing()` function with the supported CSS custom property token equivalent (ex: `var(--p-space-4)`).
Expand Down Expand Up @@ -354,3 +369,4 @@ git reset $(grep -r -l "polaris-migrator:")
- Common utilities:
- [`jsx.ts`](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/utilities/jsx.ts)
- [`imports.ts`](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/utilities/imports.ts)
0
133 changes: 133 additions & 0 deletions polaris-migrator/src/migrations/replace-sass-color/color-maps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* See the legacy Sass API file for the original color palette
* documentation/guides/legacy-polaris-v8-public-api.scss
*/

export interface ColorHue {
[hue: string]: ColorValue;
}

export interface ColorValue {
[value: string]: string;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: These aren't used anymore


export const colorMap = {
blue: {
dark: '--p-interactive-hovered',
base: '--p-interactive',
},
green: {
dark: '--p-text-success',
base: '--p-text-success',
},
yellow: {
dark: '--p-text-warning',
base: '--p-text-warning',
},
red: {
dark: '--p-text-critical',
base: '--p-text-critical',
},
ink: {
base: '--p-text',
light: '--p-text-subdued',
lighter: '--p-text-subdued',
lightest: '--p-text-subdued',
},
sky: {
dark: '--p-text-subdued-on-dark',
base: '--p-text-on-dark',
light: '--p-text-on-dark',
lighter: '--p-text-on-dark',
},
black: {
base: '--p-text',
},
white: {
base: '--p-text-on-dark',
},
};

export const backgroundColorMap = {
green: {
light: '--p-surface-success',
lighter: '--p-surface-success-subdued',
},
yellow: {
light: '--p-surface-warning',
lighter: '--p-surface-warning-subdued',
},
red: {
light: '--p-surface-critical',
lighter: '--p-surface-critical-subdued',
},
ink: {
dark: '--p-surface-dark',
base: '--p-surface-neutral-subdued-dark',
},
sky: {
base: '--p-surface-neutral',
light: '--p-surface-neutral-subdued',
lighter: '--p-surface-subdued',
},
black: {
base: '--p-surface-dark',
},
white: {
base: '--p-surface',
},
};

export const borderColorMap = {
green: {
dark: '--p-border-success',
base: '--p-border-success',
light: '--p-border-success-subdued',
lighter: '--p-border-success-subdued',
},
yellow: {
dark: '--p-border-warning',
base: '--p-border-warning',
light: '--p-border-warning-disabled',
lighter: '--p-border-warning-subdued',
},
red: {
dark: '--p-border-critical',
base: '--p-border-critical',
light: '--p-border-critical-subdued',
lighter: '--p-border-critical-subdued',
},
ink: {
lightest: '--p-border',
},
sky: {
light: '--p-border-subdued',
},
};

export const fillColorMap = {
green: {
dark: '--p-icon-success',
base: '--p-icon-success',
},
yellow: {
dark: '--p-icon-warning',
base: '--p-icon-warning',
},
red: {
dark: '--p-icon-critical',
base: '--p-icon-critical',
},
ink: {
base: '--p-icon',
light: '--p-icon',
lighter: '--p-icon-subdued',
lightest: '--p-icon-disabled',
},
black: {
base: '--p-icon',
},
white: {
base: '--p-icon-on-dark',
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type {FileInfo, API, Options} from 'jscodeshift';
import postcss, {Plugin} from 'postcss';
import valueParser from 'postcss-value-parser';
import {colors as tokenColors} from '@shopify/polaris-tokens';

import {
NamespaceOptions,
namespace,
isSassFunction,
getFunctionArgs,
stripQuotes,
StopWalkingFunctionNodes,
} from '../../utilities/sass';
import {isKeyOf} from '../../utilities/type-guards';

import {
backgroundColorMap,
borderColorMap,
colorMap,
fillColorMap,
} from './color-maps';

const tokenColorsKeys = Object.keys(tokenColors);
const maps = {
colorMap,
backgroundColorMap,
borderColorMap,
fillColorMap,
};
const propertyMap: {[key: string]: keyof typeof maps} = {
color: 'colorMap',
background: 'backgroundColorMap',
'background-color': 'backgroundColorMap',
border: 'borderColorMap',
'border-color': 'borderColorMap',
fill: 'fillColorMap',
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const propertyMap: {[key: string]: keyof typeof maps} = {
color: 'colorMap',
background: 'backgroundColorMap',
'background-color': 'backgroundColorMap',
border: 'borderColorMap',
'border-color': 'borderColorMap',
fill: 'fillColorMap',
};
const propertyMaps = {
color: colorMap,
background: backgroundColorMap,
'background-color': backgroundColorMap,
border: borderColorMap,
'border-color': borderColorMap,
fill: fillColorMap,
};

question: Is it possible to add the maps here and remove the intermediary propertyMapKey access?


export default function replaceSassColors(
file: FileInfo,
_: API,
options: Options,
) {
return postcss(plugin(options)).process(file.source, {
syntax: require('postcss-scss'),
}).css;
}

const processed = Symbol('processed');
const polarisCustomPropertyRegEx = new RegExp(
String.raw`--p-(${tokenColorsKeys.join('|')})`,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light suggestion to use the createVar util from Polaris tokens:

Object.keys(colorsTokenGroup).map(createVar).join('|')

e.g.
image


interface PluginOptions extends Options, NamespaceOptions {}

const plugin = (options: PluginOptions = {}): Plugin => {
const namespacedColor = namespace('color', options);

return {
postcssPlugin: 'replace-sass-color',
Declaration(decl) {
// @ts-expect-error - Skip if processed so we don't process it again
if (decl[processed]) return;

if (!isKeyOf(propertyMap, decl.prop)) return;
const propertyMapKey = propertyMap[decl.prop];
const replacementMap = maps[propertyMapKey];
const parsed = valueParser(decl.value);

parsed.walk((node) => {
if (node.type !== 'function') return;

if (node.value === 'rgba') {
return StopWalkingFunctionNodes;
}

// 1. Remove color() fallbacks
if (node.value === 'var') {
const {nodes} = node;
const polarisCustomPropertyIndex = nodes.findIndex(
(node) =>
node.type === 'word' &&
polarisCustomPropertyRegEx.test(node.value),
);
const colorFnFallbackIndex = nodes.findIndex((node) =>
isSassFunction(namespacedColor, node),
);

if (polarisCustomPropertyIndex < colorFnFallbackIndex) {
node.nodes = [nodes[0]];
}

return StopWalkingFunctionNodes;
}

// 2. Replace `color()` with variable
if (node.value === namespacedColor) {
const colorFnArgs = getFunctionArgs(node).map(stripQuotes);
const hue = colorFnArgs[0] ?? '';
const value = colorFnArgs[1] ?? 'base';
const forBackground = colorFnArgs[2];

// Skip color() with for-background argument
if (forBackground) return;

// Skip if we don't have a color for the hue and value
if (
!(
isKeyOf(replacementMap, hue) &&
isKeyOf(replacementMap[hue], value)
)
)
return;

const colorCustomProperty: string = replacementMap[hue][value];

node.value = 'var';
node.nodes = [
{
type: 'word',
value: colorCustomProperty,
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
sourceEndIndex: colorCustomProperty.length,
},
];
}
});

decl.value = parsed.toString();

// @ts-expect-error - Mark the declaration as processed
decl[processed] = true;
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
.my-class {
// color
color: color('blue');
color: color('blue', 'dark');
color: color('green');
color: color('green', 'dark');
color: color('yellow');
color: color('yellow', 'dark');
color: color('red');
color: color('red', 'dark');
color: color('ink');
color: color('ink', 'light');
color: color('ink', 'lighter');
color: color('ink', 'lightest');
color: color('sky');
color: color('sky', 'dark');
color: color('sky', 'light');
color: color('sky', 'lighter');
color: color('black');
color: color('white');

// background
background-color: color('green', 'light');
background-color: color('green', 'lighter');
background-color: color('yellow', 'light');
background-color: color('yellow', 'lighter');
background-color: color('red', 'light');
background-color: color('red', 'lighter');
background-color: color('ink');
background-color: color('ink', 'dark');
background-color: color('sky');
background-color: color('sky', 'light');
background-color: color('sky', 'lighter');
background-color: color('black');
background-color: color('white');

// border
border-color: color('green', 'dark');
border-color: color('green');
border-color: color('green', 'light');
border-color: color('green', 'lighter');
border-color: color('yellow', 'dark');
border-color: color('yellow');
border-color: color('yellow', 'light');
border-color: color('yellow', 'lighter');
border-color: color('red', 'dark');
border-color: color('red');
border-color: color('red', 'light');
border-color: color('red', 'lighter');
border-color: color('ink', 'lightest');
border-color: color('sky', 'light');

// fill
fill: color('green', 'dark');
fill: color('green');
fill: color('yellow', 'dark');
fill: color('yellow');
fill: color('red', 'dark');
fill: color('red');
fill: color('ink');
fill: color('ink', 'light');
fill: color('ink', 'lighter');
fill: color('ink', 'lightest');
fill: color('black');
fill: color('white');

// Remove color() fallbacks
color: var(--p-text, color('white'));

// Handle declarations with multiple values
border: var(--p-border-width-1) solid color('ink', 'lightest');
background: border-box color('sky', 'light');

// Skip color() with a for-background argument
color: color('ink', 'lighter', #f2ece4);

// Skip replacing color() within a function
background: rgba(color('black'), 0.5);
}
Loading