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
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import type {FileInfo, API, Options} from 'jscodeshift';
import postcss, {Plugin} from 'postcss';
import valueParser from 'postcss-value-parser';
import {colors as tokenColors, createVar} from '@shopify/polaris-tokens';

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

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');

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(propertyMaps, decl.prop)) return;
const replacementMap = propertyMaps[decl.prop];
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 args = getFunctionArgs(node);
const polarisCustomPropertyIndex = args.findIndex((arg) =>
polarisCustomPropertyRegEx.test(arg),
);
const colorFnFallbackIndex = args.findIndex((arg) =>
arg.startsWith(namespacedColor),
);

if (polarisCustomPropertyIndex < colorFnFallbackIndex) {
node.nodes = [node.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;
},
};
};

/*
* See the legacy Sass API file for the original color palette
* documentation/guides/legacy-polaris-v8-public-api.scss
*/

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',
},
};

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',
},
};

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',
},
};

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',
},
};

const propertyMaps = {
color: colorMap,
background: backgroundColorMap,
'background-color': backgroundColorMap,
border: borderColorMap,
Copy link
Contributor

Choose a reason for hiding this comment

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

After the border() migration work merges in it might be worth also adding the following properties to this script as well:

  • border-top
  • border-right
  • border-bottom
  • border-left

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yes, great idea! 👍

'border-color': borderColorMap,
fill: fillColorMap,
};

const polarisCustomPropertyRegEx = new RegExp(
Object.keys(tokenColors).map(createVar).join('|'),
);
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