Skip to content

Commit 33584a9

Browse files
Add Polaris migrator utilities (#7350)
1 parent 82285bc commit 33584a9

File tree

5 files changed

+136
-63
lines changed

5 files changed

+136
-63
lines changed

polaris-migrator/src/migrations/replace-sass-lengths/replace-sass-lengths.ts

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import type {FileInfo, API, Options} from 'jscodeshift';
22
import postcss, {Plugin} from 'postcss';
33
import valueParser from 'postcss-value-parser';
4-
import {toPx} from '@shopify/polaris-tokens';
54

65
import {POLARIS_MIGRATOR_COMMENT} from '../../constants';
7-
import {hasNumericOperator} from '../../utilities/sass';
6+
import {
7+
hasNumericOperator,
8+
hasTransformableLength,
9+
toTransformablePx,
10+
} from '../../utilities/sass';
11+
import {isKeyOf, isValueOf} from '../../utilities/type-guards';
812

913
// List of the props we want to run this migration on
1014
const targetProps = [
@@ -32,11 +36,6 @@ const targetProps = [
3236
'margin-block-end',
3337
] as const;
3438

35-
type TargetProp = typeof targetProps[number];
36-
37-
const isTargetProp = (propName: unknown): propName is TargetProp =>
38-
targetProps.includes(propName as TargetProp);
39-
4039
// Mapping of spacing tokens and their corresponding px values
4140
const spacingTokensMap = {
4241
'1px': '--p-space-025',
@@ -57,11 +56,6 @@ const spacingTokensMap = {
5756
'128px': '--p-space-32',
5857
} as const;
5958

60-
type SpacingToken = keyof typeof spacingTokensMap;
61-
62-
const isSpacingTokenValue = (value: unknown): value is SpacingToken =>
63-
Object.keys(spacingTokensMap).includes(value as SpacingToken);
64-
6559
const processed = Symbol('processed');
6660

6761
/**
@@ -81,7 +75,7 @@ const plugin = (_options: PluginOptions = {}): Plugin => {
8175

8276
const prop = decl.prop;
8377

84-
if (!isTargetProp(prop)) return;
78+
if (!isValueOf(targetProps, prop)) return;
8579

8680
const parsedValue = valueParser(decl.value);
8781

@@ -91,15 +85,11 @@ const plugin = (_options: PluginOptions = {}): Plugin => {
9185
if (node.type === 'function') {
9286
return StopWalkingFunctionNodes;
9387
} else if (node.type === 'word') {
94-
const dimension = valueParser.unit(node.value);
88+
const valueInPx = toTransformablePx(node.value);
9589

96-
if (isTransformableLength(dimension)) {
97-
const dimensionInPx = toPx(`${dimension.number}${dimension.unit}`);
90+
if (!isKeyOf(spacingTokensMap, valueInPx)) return;
9891

99-
if (!isSpacingTokenValue(dimensionInPx)) return;
100-
101-
node.value = `var(${spacingTokensMap[dimensionInPx]})`;
102-
}
92+
node.value = `var(${spacingTokensMap[valueInPx]})`;
10393
}
10494
});
10595

@@ -128,35 +118,3 @@ export default function replaceSassLengths(
128118
syntax: require('postcss-scss'),
129119
}).css;
130120
}
131-
132-
/**
133-
* All transformable dimension units. These values are used to determine
134-
* if a decl.value can be converted to pixels and mapped to a Polaris custom property.
135-
*/
136-
const transformableLengthUnits = ['px', 'rem'];
137-
138-
function isTransformableLength(
139-
dimension: false | valueParser.Dimension,
140-
): dimension is valueParser.Dimension {
141-
if (!dimension) return false;
142-
143-
// Zero is the only unitless length we can transform
144-
if (dimension.unit === '' && dimension.number === '0') return true;
145-
146-
return transformableLengthUnits.includes(dimension.unit);
147-
}
148-
149-
function hasTransformableLength(parsedValue: valueParser.ParsedValue): boolean {
150-
let transformableLength = false;
151-
152-
parsedValue.walk((node) => {
153-
if (
154-
node.type === 'word' &&
155-
isTransformableLength(valueParser.unit(node.value))
156-
) {
157-
transformableLength = true;
158-
}
159-
});
160-
161-
return transformableLength;
162-
}

polaris-migrator/src/migrations/replace-sass-spacing/replace-sass-spacing.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
hasSassFunction,
1111
hasNumericOperator,
1212
} from '../../utilities/sass';
13+
import {isKeyOf} from '../../utilities/type-guards';
1314

1415
const spacingMap = {
1516
none: '--p-space-0',
@@ -22,9 +23,6 @@ const spacingMap = {
2223
'extra-loose': '--p-space-8',
2324
};
2425

25-
const isSpacing = (spacing: unknown): spacing is keyof typeof spacingMap =>
26-
Object.keys(spacingMap).includes(spacing as string);
27-
2826
const processed = Symbol('processed');
2927

3028
interface PluginOptions extends Options, NamespaceOptions {}
@@ -47,7 +45,7 @@ const plugin = (options: PluginOptions = {}): Plugin => {
4745

4846
const spacing = node.nodes[0]?.value ?? '';
4947

50-
if (!isSpacing(spacing)) return;
48+
if (!isKeyOf(spacingMap, spacing)) return;
5149
const spacingCustomProperty = spacingMap[spacing];
5250

5351
node.value = 'var';

polaris-migrator/src/migrations/replace-static-breakpoint-mixins/replace-static-breakpoint-mixins.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {FileInfo} from 'jscodeshift';
22
import postcss, {Plugin} from 'postcss';
33

4+
import {isKeyOf} from '../../utilities/type-guards';
5+
46
/** Mapping of static breakpoint mixins from old to new */
57
const staticBreakpointMixins = {
68
'page-content-when-partially-condensed': '#{$p-breakpoints-lg-down}',
@@ -20,11 +22,6 @@ const staticBreakpointMixins = {
2022
'after-topbar-sheet': '#{$p-breakpoints-sm-up}',
2123
};
2224

23-
const isStaticBreakpointMixin = (
24-
mixinName: unknown,
25-
): mixinName is keyof typeof staticBreakpointMixins =>
26-
Object.keys(staticBreakpointMixins).includes(mixinName as string);
27-
2825
const plugin = (): Plugin => ({
2926
postcssPlugin: 'ReplaceStaticBreakpointMixins',
3027
AtRule(atRule) {
@@ -33,7 +30,7 @@ const plugin = (): Plugin => ({
3330
// Extract mixin name e.g. name from `@include name;` or `@include name();`
3431
const mixinName = atRule.params.match(/^([a-zA-Z0-9_-]+)/)?.[1];
3532

36-
if (!isStaticBreakpointMixin(mixinName)) return;
33+
if (!isKeyOf(staticBreakpointMixins, mixinName)) return;
3734

3835
atRule.assign({
3936
name: 'media',

polaris-migrator/src/utilities/sass.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import type {Node, ParsedValue, FunctionNode} from 'postcss-value-parser';
1+
import type {Declaration} from 'postcss';
2+
import valueParser, {
3+
Node,
4+
ParsedValue,
5+
FunctionNode,
6+
Dimension,
7+
} from 'postcss-value-parser';
8+
import {toPx} from '@shopify/polaris-tokens';
9+
10+
import {isKeyOf} from './type-guards';
211

312
function getNamespace(options?: NamespaceOptions) {
413
return options?.namespace || '';
@@ -71,3 +80,101 @@ export function hasSassFunction(
7180

7281
return containsSassFunction;
7382
}
83+
84+
/**
85+
* All transformable dimension units. These values are used to determine
86+
* if a decl.value can be converted to pixels and mapped to a Polaris custom property.
87+
*/
88+
export const transformableLengthUnits = ['px', 'rem'];
89+
90+
function isUnitlessZero(dimension: false | Dimension) {
91+
return dimension && dimension.unit === '' && dimension.number === '0';
92+
}
93+
94+
export function isTransformableLength(
95+
dimension: false | Dimension,
96+
): dimension is Dimension {
97+
if (!dimension) return false;
98+
99+
// Zero is the only unitless dimension our length transforms support
100+
if (isUnitlessZero(dimension)) return true;
101+
102+
return transformableLengthUnits.includes(dimension.unit);
103+
}
104+
105+
export function hasTransformableLength(parsedValue: ParsedValue): boolean {
106+
let transformableLength = false;
107+
108+
parsedValue.walk((node) => {
109+
if (
110+
node.type === 'word' &&
111+
isTransformableLength(valueParser.unit(node.value))
112+
) {
113+
transformableLength = true;
114+
}
115+
});
116+
117+
return transformableLength;
118+
}
119+
120+
export function toTransformablePx(value: string) {
121+
const dimension = valueParser.unit(value);
122+
123+
if (!isTransformableLength(dimension)) return;
124+
125+
return isUnitlessZero(dimension)
126+
? `${dimension.number}px`
127+
: toPx(`${dimension.number}${dimension.unit}`);
128+
}
129+
130+
/**
131+
* A mapping of evaluated `rem` values (in pixels) and their replacement `decl.value`
132+
*/
133+
interface ReplaceRemFunctionMap {
134+
[remValueInPx: string]: string;
135+
}
136+
137+
/**
138+
* Replaces a basic `rem` function with a value from the provided map.
139+
*
140+
* Note: If a `map` value starts with `--`, it is assumed to be a CSS
141+
* custom property and wrapped in `var()`.
142+
*
143+
* @example
144+
* const decl = { value: 'rem(4px)' };
145+
* const namespacedDecl = { value: 'my-namespace.rem(4px)' };
146+
* const map = { '4px': '--p-size-1' };
147+
*
148+
* replaceRemFunction(decl, map)
149+
* //=> decl === { value: 'var(--p-size-1)' }
150+
*
151+
* replaceRemFunction(namespacedDecl, map, 'my-namespace')
152+
* //=> namespaceDecl === { value: 'var(--p-size-1)' }
153+
*/
154+
export function replaceRemFunction(
155+
decl: Declaration,
156+
map: ReplaceRemFunctionMap,
157+
options?: NamespaceOptions,
158+
): void {
159+
const namespacedRemPattern = namespace('rem', options).replace('.', '\\.');
160+
161+
const namespacedRemFunctionRegExp = new RegExp(
162+
String.raw`^${namespacedRemPattern}\(\s*([\d.]+)(px)?\s*\)\s*$`,
163+
'g',
164+
);
165+
166+
decl.value = decl.value.replace(
167+
namespacedRemFunctionRegExp,
168+
(match, number, unit) => {
169+
if (!unit && number !== '0') return match;
170+
171+
const remValueInPx = `${number}${unit ?? 'px'}`;
172+
173+
if (!isKeyOf(map, remValueInPx)) return match;
174+
175+
const newValue = map[remValueInPx];
176+
177+
return newValue.startsWith('--') ? `var(${newValue})` : newValue;
178+
},
179+
);
180+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function isKeyOf<T extends {[key: string]: any}>(
2+
obj: T,
3+
key: PropertyKey | undefined,
4+
): key is keyof T {
5+
return Object.keys(obj).includes(key as string);
6+
}
7+
8+
export function isValueOf<T extends readonly string[]>(
9+
arr: T,
10+
value: string | undefined,
11+
): value is T[number] {
12+
return arr.includes(value as string);
13+
}

0 commit comments

Comments
 (0)