Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/dirty-mice-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris-migrator': minor
---

Add sass padding and margin migration
1 change: 1 addition & 0 deletions polaris-migrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"generate": "plop"
},
"dependencies": {
"@shopify/polaris-tokens": "^6.0.0",
"chalk": "^4.1.0",
"globby": "11.0.1",
"is-git-clean": "^1.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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';

// List of the props we want to run this migration on
const targetProps = [
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'padding-inline',
'padding-inline-start',
'padding-inline-end',
'padding-block',
'padding-block-start',
'padding-block-end',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'margin-inline',
'margin-inline-start',
'margin-inline-end',
'margin-block',
'margin-block-start',
'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 = {
'0': '--p-space-0',
'0px': '--p-space-0',
'1px': '--p-space-025',
'2px': '--p-space-05',
'4px': '--p-space-1',
'8px': '--p-space-2',
'12px': '--p-space-3',
'16px': '--p-space-4',
'20px': '--p-space-5',
'24px': '--p-space-6',
'32px': '--p-space-8',
'40px': '--p-space-10',
'48px': '--p-space-12',
'64px': '--p-space-16',
'80px': '--p-space-20',
'96px': '--p-space-24',
'112px': '--p-space-28',
'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');

/**
* Exit early and stop traversing descendant nodes:
* https://www.npmjs.com/package/postcss-value-parser:~:text=Returning%20false%20in%20the%20callback%20will%20prevent%20traversal%20of%20descendent%20nodes
*/
const StopWalkingFunctionNodes = false;

interface PluginOptions extends Options {}

const plugin = (_options: PluginOptions = {}): Plugin => {
return {
postcssPlugin: 'replace-sass-lengths',
Declaration(decl) {
// @ts-expect-error - Skip if processed so we don't process it again
if (decl[processed]) return;

const prop = decl.prop;

if (!isTargetProp(prop)) return;

const parsedValue = valueParser(decl.value);

if (!hasTransformableLength(parsedValue)) return;

parsedValue.walk((node) => {
if (node.type === 'function') {
return StopWalkingFunctionNodes;
} else if (node.type === 'word') {
const dimension = valueParser.unit(node.value);

if (isTransformableLength(dimension)) {
const dimensionInPx = toPx(`${dimension.number}${dimension.unit}`);

if (!isSpacingTokenValue(dimensionInPx)) return;

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

if (hasNumericOperator(parsedValue)) {
// Insert comment if the declaration value contains calculations
decl.before(postcss.comment({text: POLARIS_MIGRATOR_COMMENT}));
decl.before(
postcss.comment({text: `${decl.prop}: ${parsedValue.toString()};`}),
);
} else {
decl.value = parsedValue.toString();
}

// @ts-expect-error - Mark the declaration as processed
decl[processed] = true;
},
};
};

export default function replaceSassLengths(
fileInfo: FileInfo,
_: API,
options: Options,
) {
return postcss(plugin(options)).process(fileInfo.source, {
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
@@ -0,0 +1,64 @@
/* stylelint-disable shorthand-property-no-redundant-values */

.my-class {
color: lightsteelblue;

// Unitless
padding: 0;
padding: 1;
padding: 2;

// Single shorthand pixels
padding: 16px;
padding: 10px;

// Single shorthand rems
padding: 1rem;
padding: 10rem;

// Double shorthand pixels
padding: 16px 16px;
padding: 10px 10px;
padding: 16px 10px;

// Double shorthand rems
padding: 1rem 1rem;
padding: 10rem 10rem;
padding: 1rem 10rem;

// Double shorthand mixed (px, rem, unitless)
padding: 16px 1rem;
padding: 10px 10rem;
padding: 16px 10rem;
padding: 10px 1rem;
padding: 0 16px;
padding: 1rem 0;

// Double shorthand mixed edge cases
padding: 16px 4in;
padding: 16px 1em;
padding: 16px -16px;

// Unsupported transforms with comments
padding: 16px + 16px;
padding: 16px + 16px 16px;
padding: (16px + 16px) 16px;
padding: $var + 16px;
padding: rem(16px) + 16px;
padding: rem($var * 16px);
padding: calc(16px + 16px);
padding: 16px + #{16px + 16px};

// Unsupported transforms without comments
padding: rem(16px);
padding: #{16px + 16px};
padding: layout-width(nav);
padding: 10em;
padding: var(--var);
padding: var(--p-space-4);
padding: var(--p-space-4, 16px);
// Skip negative lengths. Need to discuss replacement strategy e.g.
// calc(-1 * var(--p-space-*)) vs var(--p-space--*)
padding: -16px;
padding: -10px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* stylelint-disable shorthand-property-no-redundant-values */

.my-class {
color: lightsteelblue;

// Unitless
padding: var(--p-space-0);
padding: 1;
padding: 2;

// Single shorthand pixels
padding: var(--p-space-4);
padding: 10px;

// Single shorthand rems
padding: var(--p-space-4);
padding: 10rem;

// Double shorthand pixels
padding: var(--p-space-4) var(--p-space-4);
padding: 10px 10px;
padding: var(--p-space-4) 10px;

// Double shorthand rems
padding: var(--p-space-4) var(--p-space-4);
padding: 10rem 10rem;
padding: var(--p-space-4) 10rem;

// Double shorthand mixed (px, rem, unitless)
padding: var(--p-space-4) var(--p-space-4);
padding: 10px 10rem;
padding: var(--p-space-4) 10rem;
padding: 10px var(--p-space-4);
padding: var(--p-space-0) var(--p-space-4);
padding: var(--p-space-4) var(--p-space-0);

// Double shorthand mixed edge cases
padding: var(--p-space-4) 4in;
padding: var(--p-space-4) 1em;
padding: var(--p-space-4) -16px;

// Unsupported transforms with comments
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: var(--p-space-4) + var(--p-space-4); */
padding: 16px + 16px;
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: var(--p-space-4) + var(--p-space-4) var(--p-space-4); */
padding: 16px + 16px 16px;
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: (16px + 16px) var(--p-space-4); */
padding: (16px + 16px) 16px;
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: $var + var(--p-space-4); */
padding: $var + 16px;
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: rem(16px) + var(--p-space-4); */
padding: rem(16px) + 16px;
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: rem($var * 16px); */
padding: rem($var * 16px);
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: calc(16px + 16px); */
padding: calc(16px + 16px);
/* polaris-migrator: This is a complex expression that we can't automatically convert. Please check this manually. */
/* padding: var(--p-space-4) + #{16px + 16px}; */
padding: 16px + #{16px + 16px};

// Unsupported transforms without comments
padding: rem(16px);
padding: #{16px + 16px};
padding: layout-width(nav);
padding: 10em;
padding: var(--var);
padding: var(--p-space-4);
padding: var(--p-space-4, 16px);
// Skip negative lengths. Need to discuss replacement strategy e.g.
// calc(-1 * var(--p-space-*)) vs var(--p-space--*)
padding: -16px;
padding: -10px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {check} from '../../../utilities/testUtils';

const migration = 'replace-sass-lengths';
const fixtures = ['replace-sass-lengths', 'with-namespace'];

for (const fixture of fixtures) {
check(__dirname, {
fixture,
migration,
extension: 'scss',
options: {
namespace: fixture.includes('with-namespace')
? 'legacy-polaris-v8'
: undefined,
},
});
}
Loading