Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
158b6e6
Initial replace-typography-declarations migration
aaronccasanova Oct 4, 2022
dcbe2b8
docs: Add changeset entry
aaronccasanova Oct 4, 2022
3bf8eaf
Add support for migrating basic rem function calls
aaronccasanova Oct 4, 2022
8106a92
Remove unused imports
aaronccasanova Oct 4, 2022
2577a9f
Merge branch 'main' of https://github.com/Shopify/polaris into replac…
aaronccasanova Oct 6, 2022
77c5efe
Fix merge conflict errors
aaronccasanova Oct 6, 2022
99d424e
Add with-namespace tests
aaronccasanova Oct 6, 2022
91f1c4c
Update changeset entry
aaronccasanova Oct 6, 2022
c4cd95d
Add replaceDecl utility to reduce duplicate handler logic
aaronccasanova Oct 6, 2022
3763cf8
Make Polaris migrator comment generic
aaronccasanova Oct 7, 2022
40f3881
Add utility to getFunctionArgs
aaronccasanova Oct 7, 2022
abd06d4
Add support for the legacy font-size() function
aaronccasanova Oct 7, 2022
88c4a26
Rearchitect migration to reduce duplicate comment logic
aaronccasanova Oct 8, 2022
56c2329
Add support for the legacy line-height() function
aaronccasanova Oct 8, 2022
fdc6e12
Merge branch 'main' of https://github.com/Shopify/polaris into replac…
aaronccasanova Oct 8, 2022
dfa9668
Remove replaceDecl utility
aaronccasanova Oct 11, 2022
58a4512
Merge branch 'main' of https://github.com/Shopify/polaris into replac…
aaronccasanova Oct 11, 2022
b895e2c
Restructure migration to only parse values of target declarations
aaronccasanova Oct 11, 2022
0cac336
Ignore global values
aaronccasanova Oct 11, 2022
9bfdb6b
Update polaris-migrator/src/migrations/replace-typography-declaration…
aaronccasanova Oct 12, 2022
8322f0c
Update polaris-migrator/src/migrations/replace-typography-declaration…
aaronccasanova Oct 12, 2022
debb469
Add rem() support to the line-height handler
aaronccasanova Oct 14, 2022
46b1cce
Update line-height test lengths
aaronccasanova Oct 14, 2022
1487243
Add replace-typography-declarations docs
aaronccasanova Oct 14, 2022
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/stupid-suns-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris-migrator': patch
---

Add migration to `replace-typography-declarations`
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ 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,
isTransformableLength,
} from '../../utilities/sass';

// List of the props we want to run this migration on
const targetProps = [
Expand Down Expand Up @@ -130,35 +134,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
@@ -0,0 +1,192 @@
import type {FileInfo, API, Options} from 'jscodeshift';
import postcss, {Declaration, Plugin} from 'postcss';
import valueParser from 'postcss-value-parser';
import {toPx} from '@shopify/polaris-tokens';

import {
NamespaceOptions,
namespace,
isTransformableLength,
hasSassFunction,
isSassFunction,
} from '../../utilities/sass';
import {isKeyof} from '../../utilities/type-guards';

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

const processed = Symbol('processed');

interface PluginOptions extends Options, NamespaceOptions {}

const plugin = (options: PluginOptions = {}): Plugin => {
const namespacedFontFamily = namespace('font-family', options);

return {
postcssPlugin: 'replace-typography-declarations',
Declaration: {
'font-family': createHandleFontFamily(namespacedFontFamily),
'font-size': handleFontSize,
'font-weight': handleFontWeight,
'line-height': handleFontLineHeight,
},
};
};

const fontFamilyMap = {
'': '--p-font-family-sans',
base: '--p-font-family-sans',
monospace: '--p-font-family-mono',
};

const fontSizeMap = {
'12px': '--p-font-size-75',
'14px': '--p-font-size-100',
'16px': '--p-font-size-200',
'20px': '--p-font-size-300',
'24px': '--p-font-size-400',
'28px': '--p-font-size-500',
'32px': '--p-font-size-600',
'40px': '--p-font-size-700',
};

const fontLineHeightMap = {
'16px': '--p-font-line-height-1',
'20px': '--p-font-line-height-2',
'24px': '--p-font-line-height-3',
'28px': '--p-font-line-height-4',
'32px': '--p-font-line-height-5',
'40px': '--p-font-line-height-6',
'48px': '--p-font-line-height-7',
};

const fontWeightMap = {
400: '--p-font-weight-regular',
500: '--p-font-weight-medium',
600: '--p-font-weight-semibold',
700: '--p-font-weight-bold',
// https://drafts.csswg.org/css-fonts-3/#propdef-font-weight
// 100 - Thin
// 200 - Extra Light (Ultra Light)
// 300 - Light
// 400 - Normal
normal: '--p-font-weight-regular',
// 500 - Medium
// 600 - Semi Bold (Demi Bold)
// 700 - Bold
bold: '--p-font-weight-bold',
// 800 - Extra Bold (Ultra Bold)
// 900 - Black (Heavy)
};

function createHandleFontFamily(namespacedFontFamily: string) {
return (decl: Declaration): void => {
// @ts-expect-error - Skip if processed so we don't process it again
if (decl[processed]) return;

const parsedValue = valueParser(decl.value);

if (!hasSassFunction(namespacedFontFamily, parsedValue)) return;

parsedValue.walk((node) => {
if (!isSassFunction(namespacedFontFamily, node)) return;

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

if (!isKeyof(fontFamilyMap, fontFamily)) return;

const fontFamilyCustomProperty = fontFamilyMap[fontFamily];

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

decl.value = parsedValue.toString();

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

function handleFontSize(decl: Declaration): void {
// @ts-expect-error - Skip if processed so we don't process it again
if (decl[processed]) return;

const parsedValue = valueParser(decl.value);
const fontSize = parsedValue.nodes[0];

if (parsedValue.nodes.length !== 1 || fontSize.type !== 'word') {
return;
}

const dimension = valueParser.unit(fontSize.value);

if (!isTransformableLength(dimension)) return;

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

if (!isKeyof(fontSizeMap, dimensionInPx)) return;

decl.value = `var(${fontSizeMap[dimensionInPx]})`;

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

function handleFontWeight(decl: Declaration): void {
// @ts-expect-error - Skip if processed so we don't process it again
if (decl[processed]) return;

const parsedValue = valueParser(decl.value);
const fontWeight = parsedValue.nodes[0];

if (parsedValue.nodes.length !== 1 || fontWeight.type !== 'word') {
return;
}

if (!isKeyof(fontWeightMap, fontWeight.value)) return;

decl.value = `var(${fontWeightMap[fontWeight.value]})`;

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

function handleFontLineHeight(decl: Declaration): void {
// @ts-expect-error - Skip if processed so we don't process it again
if (decl[processed]) return;

const parsedValue = valueParser(decl.value);
const lineHeight = parsedValue.nodes[0];

if (parsedValue.nodes.length !== 1 || lineHeight.type !== 'word') {
return;
}

const dimension = valueParser.unit(lineHeight.value);

if (!isTransformableLength(dimension)) return;

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

if (!isKeyof(fontLineHeightMap, dimensionInPx)) return;

decl.value = `var(${fontLineHeightMap[dimensionInPx]})`;

// @ts-expect-error - Mark the declaration as processed
decl[processed] = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.font-family {
font-family: sans-serif;
font-family: font-family();
font-family: font-family(base);
font-family: font-family(monospace);
}

.font-size {
font-size: 10px;
font-size: 12px;
font-size: 1rem;
font-size: 1em;
}

.font-weight {
font-weight: 300;
font-weight: 400;
font-weight: 700;
font-weight: normal;
font-weight: bold;
font-weight: 400 !important;
font-weight: 400 + $var;
}

.font-line-height {
line-height: 10px;
line-height: 16px;
line-height: 1rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.font-family {
font-family: sans-serif;
font-family: var(--p-font-family-sans);
font-family: var(--p-font-family-sans);
font-family: var(--p-font-family-mono);
}

.font-size {
font-size: 10px;
font-size: var(--p-font-size-75);
font-size: var(--p-font-size-200);
font-size: 1em;
}

.font-weight {
font-weight: 300;
font-weight: var(--p-font-weight-regular);
font-weight: var(--p-font-weight-bold);
font-weight: var(--p-font-weight-regular);
font-weight: var(--p-font-weight-bold);
font-weight: var(--p-font-weight-regular) !important;
font-weight: 400 + $var;
}

.font-line-height {
line-height: 10px;
line-height: var(--p-font-line-height-1);
line-height: var(--p-font-line-height-1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {check} from '../../../utilities/testUtils';

const migration = 'replace-typography-declarations';
const fixtures = ['replace-typography-declarations'];

for (const fixture of fixtures) {
check(__dirname, {
fixture,
migration,
extension: 'scss',
});
}
39 changes: 38 additions & 1 deletion polaris-migrator/src/utilities/sass.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type {Node, ParsedValue, FunctionNode} from 'postcss-value-parser';
import valueParser, {
Node,
ParsedValue,
FunctionNode,
Dimension,
} from 'postcss-value-parser';

function getNamespace(options?: NamespaceOptions) {
return options?.namespace || '';
Expand Down Expand Up @@ -71,3 +76,35 @@ 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'];

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

// Zero is the only unitless dimension our length transforms support
if (dimension.unit === '' && dimension.number === '0') 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;
}
6 changes: 6 additions & 0 deletions polaris-migrator/src/utilities/type-guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
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);
}