Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type {FileInfo, API, Options} from 'jscodeshift';
import postcss, {Plugin} from 'postcss';
import valueParser from 'postcss-value-parser';
import {toPx} from '@shopify/polaris-tokens';

import {POLARIS_MIGRATOR_COMMENT} from '../../constants';
import {hasNumericOperator} from '../../utilities/sass';
import {
hasNumericOperator,
hasTransformableLength,
toPx,
} from '../../utilities/sass';
import {isKeyOf, isValueOf} from '../../utilities/type-guards';

// List of the props we want to run this migration on
const targetProps = [
Expand Down Expand Up @@ -32,11 +36,6 @@ const targetProps = [
'margin-block-end',
] as const;

type TargetProp = typeof targetProps[number];

const isTargetProp = (propName: unknown): propName is TargetProp =>
targetProps.includes(propName as TargetProp);

// Mapping of spacing tokens and their corresponding px values
const spacingTokensMap = {
'1px': '--p-space-025',
Expand All @@ -57,11 +56,6 @@ const spacingTokensMap = {
'128px': '--p-space-32',
} as const;

type SpacingToken = keyof typeof spacingTokensMap;

const isSpacingTokenValue = (value: unknown): value is SpacingToken =>
Object.keys(spacingTokensMap).includes(value as SpacingToken);

const processed = Symbol('processed');

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

const prop = decl.prop;

if (!isTargetProp(prop)) return;
if (!isValueOf(targetProps, prop)) return;

const parsedValue = valueParser(decl.value);

Expand All @@ -91,15 +85,11 @@ const plugin = (_options: PluginOptions = {}): Plugin => {
if (node.type === 'function') {
return StopWalkingFunctionNodes;
} else if (node.type === 'word') {
const dimension = valueParser.unit(node.value);
const valueInPx = toPx(node.value);

if (isTransformableLength(dimension)) {
const dimensionInPx = toPx(`${dimension.number}${dimension.unit}`);
if (!isKeyOf(spacingTokensMap, valueInPx)) return;

if (!isSpacingTokenValue(dimensionInPx)) return;

node.value = `var(${spacingTokensMap[dimensionInPx]})`;
}
node.value = `var(${spacingTokensMap[valueInPx]})`;
}
});

Expand Down Expand Up @@ -128,35 +118,3 @@ export default function replaceSassLengths(
syntax: require('postcss-scss'),
}).css;
}

/**
* All transformable dimension units. These values are used to determine
* if a decl.value can be converted to pixels and mapped to a Polaris custom property.
*/
const transformableLengthUnits = ['px', 'rem'];

function isTransformableLength(
dimension: false | valueParser.Dimension,
): dimension is valueParser.Dimension {
if (!dimension) return false;

// Zero is the only unitless length we can transform
if (dimension.unit === '' && dimension.number === '0') return true;

return transformableLengthUnits.includes(dimension.unit);
}

function hasTransformableLength(parsedValue: valueParser.ParsedValue): boolean {
let transformableLength = false;

parsedValue.walk((node) => {
if (
node.type === 'word' &&
isTransformableLength(valueParser.unit(node.value))
) {
transformableLength = true;
}
});

return transformableLength;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
hasSassFunction,
hasNumericOperator,
} from '../../utilities/sass';
import {isKeyOf} from '../../utilities/type-guards';

const spacingMap = {
none: '--p-space-0',
Expand All @@ -22,9 +23,6 @@ const spacingMap = {
'extra-loose': '--p-space-8',
};

const isSpacing = (spacing: unknown): spacing is keyof typeof spacingMap =>
Object.keys(spacingMap).includes(spacing as string);

const processed = Symbol('processed');

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

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

if (!isSpacing(spacing)) return;
if (!isKeyOf(spacingMap, spacing)) return;
const spacingCustomProperty = spacingMap[spacing];

node.value = 'var';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {FileInfo} from 'jscodeshift';
import postcss, {Plugin} from 'postcss';

import {isKeyOf} from '../../utilities/type-guards';

/** Mapping of static breakpoint mixins from old to new */
const staticBreakpointMixins = {
'page-content-when-partially-condensed': '#{$p-breakpoints-lg-down}',
Expand All @@ -20,11 +22,6 @@ const staticBreakpointMixins = {
'after-topbar-sheet': '#{$p-breakpoints-sm-up}',
};

const isStaticBreakpointMixin = (
mixinName: unknown,
): mixinName is keyof typeof staticBreakpointMixins =>
Object.keys(staticBreakpointMixins).includes(mixinName as string);

const plugin = (): Plugin => ({
postcssPlugin: 'ReplaceStaticBreakpointMixins',
AtRule(atRule) {
Expand All @@ -33,7 +30,7 @@ const plugin = (): Plugin => ({
// Extract mixin name e.g. name from `@include name;` or `@include name();`
const mixinName = atRule.params.match(/^([a-zA-Z0-9_-]+)/)?.[1];

if (!isStaticBreakpointMixin(mixinName)) return;
if (!isKeyOf(staticBreakpointMixins, mixinName)) return;

atRule.assign({
name: 'media',
Expand Down
109 changes: 108 additions & 1 deletion polaris-migrator/src/utilities/sass.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import type {Node, ParsedValue, FunctionNode} from 'postcss-value-parser';
import type {Declaration} from 'postcss';
import valueParser, {
Node,
ParsedValue,
FunctionNode,
Dimension,
} from 'postcss-value-parser';
import {toPx as polarisTokensToPx} from '@shopify/polaris-tokens';

import {isKeyOf} from './type-guards';

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

return containsSassFunction;
}

/**
* All transformable dimension units. These values are used to determine
* if a decl.value can be converted to pixels and mapped to a Polaris custom property.
*/
export const transformableLengthUnits = ['px', 'rem'];

function isUnitlessZero(dimension: false | Dimension) {
return dimension && dimension.unit === '' && dimension.number === '0';
}

export function isTransformableLength(
dimension: false | Dimension,
): dimension is Dimension {
if (!dimension) return false;

// Zero is the only unitless dimension our length transforms support
if (isUnitlessZero(dimension)) return true;

return transformableLengthUnits.includes(dimension.unit);
}

export function hasTransformableLength(parsedValue: ParsedValue): boolean {
let transformableLength = false;

parsedValue.walk((node) => {
if (
node.type === 'word' &&
isTransformableLength(valueParser.unit(node.value))
) {
transformableLength = true;
}
});

return transformableLength;
}

export function toPx(value: string) {
const dimension = valueParser.unit(value);

if (!isTransformableLength(dimension)) return;

return isUnitlessZero(dimension)
? dimension.number
: polarisTokensToPx(`${dimension.number}${dimension.unit}`);
}

/**
* A mapping of evaluated `rem` values (in pixels) and their replacement `decl.value`
*/
interface ReplaceRemFunctionMap {
[remValueInPx: string]: string;
}

/**
* Replaces a basic `rem` function with a value from the provided map.
*
* Note: If a `map` value starts with `--`, it is assumed to be a CSS
* custom property and wrapped in `var()`.
*
* @example
* const decl = { value: 'rem(4px)' };
* const namespacedDecl = { value: 'my-namespace.rem(4px)' };
* const map = { '4px': '--p-size-1' };
*
* replaceRemFunction(decl, map)
* //=> decl === { value: 'var(--p-size-1)' }
*
* replaceRemFunction(namespacedDecl, map, 'my-namespace')
* //=> namespaceDecl === { value: 'var(--p-size-1)' }
*/
export function replaceRemFunction(
decl: Declaration,
map: ReplaceRemFunctionMap,
options?: NamespaceOptions,
): void {
const namespacedRemPattern = namespace('rem', options).replace('.', '\\.');

const namespacedRemFunctionRegExp = new RegExp(
String.raw`^${namespacedRemPattern}\(\s*([\d.]+)(px)?\s*\)\s*$`,
'g',
);

decl.value = decl.value.replace(
namespacedRemFunctionRegExp,
(match, number, unit) => {
if (!unit && number !== '0') return match;

const remValueInPx = `${number}${unit ?? 'px'}`;

if (!isKeyOf(map, remValueInPx)) return match;

const newValue = map[remValueInPx];

return newValue.startsWith('--') ? `var(${newValue})` : newValue;
},
);
}
13 changes: 13 additions & 0 deletions polaris-migrator/src/utilities/type-guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function isKeyOf<T extends {[key: string]: any}>(
obj: T,
key: PropertyKey | undefined,
): key is keyof T {
return Object.keys(obj).includes(key as string);
}

export function isValueOf<T extends readonly string[]>(
arr: T,
value: string | undefined,
): value is T[number] {
return arr.includes(value as string);
}