Skip to content

Commit f711b74

Browse files
committed
createSassMigrator method to wrap common functionality
1 parent 77d75f9 commit f711b74

File tree

5 files changed

+176
-135
lines changed

5 files changed

+176
-135
lines changed

.changeset/silent-spiders-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris-migrator': minor
3+
---
4+
5+
Add `createSassMigrator` utility to stash common logic, starting with only parsing each event once.

polaris-migrator/README.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,18 @@ Be aware that this may also create additional code changes in your codebase, we
235235
npx @shopify/polaris-migrator replace-sass-spacing <path>
236236
```
237237

238-
## Creating a migration
238+
## Creating Migrations
239239

240-
### Setup
240+
Sometimes referred to as "codemods", migrations are JavaScript functions which modify some code from one form to another (eg; to move between breaking versions of `@shopify/polaris`). ASTs (Abstract Syntax Trees) are used to "walk" through the code in discreet, strongly typed steps, called "nodes". All changes made to nodes (and thus the AST) are then written out as the new/"migrated" version of the code.
241241

242-
Run `yarn new-migration` to generate a new migration from a template.
242+
`polaris-migrator` supports two types of migrations:
243+
244+
- SASS Migrations
245+
- Typescript Migrations
246+
247+
### Creating a SASS migration
248+
249+
Run `yarn new-migration` to generate a new migration from the `sass-migration` template:
243250

244251
```sh
245252
❯ yarn new-migration
@@ -250,7 +257,7 @@ $ plop
250257
typescript-migration
251258
```
252259

253-
We will use the `sass-migration` and call our migration `replace-sass-function` for this example. Provide the name of your migration:
260+
Next, provide the name of your migration. For example; `replace-sass-function`:
254261

255262
```sh
256263
? [PLOP] Please choose a generator. sass-migration
@@ -269,11 +276,11 @@ migrations
269276
└── replace-sass-function.test.ts
270277
```
271278

272-
### Writing migration function
279+
#### The SASS migration function
273280

274-
A migration is simply a javascript function which serves as the entry-point for your codemod. The `replace-sass-function.ts` file defines a "migration" function. This function is named the same as the provided migration name, `replace-sass-function`, and is the default export of the file.
281+
Each migrator has a default export adhering to the [PostCSS Plugin API](https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md) with one main difference: events are only executed once.
275282

276-
Some example code has been provided for each template. You can make any migration code adjustments in the migration function. For Sass migrations, a [PostCSS plugin](https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md) is used to parse and transform the source code provided by the [jscodeshift](https://github.com/facebook/jscodeshift).
283+
Continuing the example, here is what the migration may look like if our goal is to replace the Sass function `hello()` with `world()`.
277284

278285
```ts
279286
// polaris-migrator/src/migrations/replace-sass-function/replace-sass-function.ts
@@ -305,9 +312,9 @@ export default function replaceSassFunction(fileInfo: FileInfo) {
305312
}
306313
```
307314

308-
This example migration will replace the Sass function `hello()` with `world()`.
315+
A more complete example can be seen in [`replace-spacing-lengths.ts`](https://github.com/Shopify/polaris/blob/main/polaris-migrator/src/migrations/replace-spacing-lengths/replace-spacing-lengths.ts).
309316

310-
### Testing
317+
#### Testing
311318

312319
The template will also generate starting test files you can use to test your migration. In your migrations `tests` folder, you can see 3 files:
313320

@@ -317,6 +324,8 @@ The template will also generate starting test files you can use to test your mig
317324

318325
The main test file will load the input/output fixtures to test your migration against. You can configure additional fixtures and test migration options (see the `replace-sass-spacing.test.ts` as an example).
319326

327+
## Running Migrations
328+
320329
Run tests locally from workspace root by filtering to the migrations package:
321330

322331
```sh

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

Lines changed: 79 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type {FileInfo, API, Options} from 'jscodeshift';
2-
import postcss, {Plugin} from 'postcss';
31
import valueParser from 'postcss-value-parser';
42

53
import {POLARIS_MIGRATOR_COMMENT} from '../../constants';
@@ -10,122 +8,107 @@ import {
108
isSassFunction,
119
isTransformableLength,
1210
namespace,
13-
NamespaceOptions,
1411
toTransformablePx,
1512
StopWalkingFunctionNodes,
13+
createSassMigrator,
1614
} from '../../utilities/sass';
1715
import {isKeyOf} from '../../utilities/type-guards';
1816

19-
export default function replaceSpacingLengths(
20-
fileInfo: FileInfo,
21-
_: API,
22-
options: Options,
23-
) {
24-
return postcss(plugin(options)).process(fileInfo.source, {
25-
syntax: require('postcss-scss'),
26-
}).css;
27-
}
28-
29-
const processed = Symbol('processed');
30-
31-
interface PluginOptions extends Options, NamespaceOptions {}
32-
33-
const plugin = (options: PluginOptions = {}): Plugin => {
34-
const namespacedRem = namespace('rem', options);
35-
36-
return {
37-
postcssPlugin: 'replace-sass-space',
38-
Declaration(decl) {
39-
// @ts-expect-error - Skip if processed so we don't process it again
40-
if (decl[processed]) return;
41-
42-
if (!spaceProps.has(decl.prop)) return;
43-
44-
/**
45-
* A collection of transformable values to migrate (e.g. decl lengths, functions, etc.)
46-
*
47-
* Note: This is evaluated at the end of each visitor execution to determine whether
48-
* or not to replace the declaration or insert a comment.
49-
*/
50-
const targets: {replaced: boolean}[] = [];
51-
let hasNumericOperator = false;
52-
const parsedValue = valueParser(decl.value);
53-
54-
handleSpaceProps();
55-
56-
if (targets.some(({replaced}) => !replaced || hasNumericOperator)) {
57-
decl.before(
58-
createInlineComment(POLARIS_MIGRATOR_COMMENT, {prose: true}),
59-
);
60-
decl.before(
61-
createInlineComment(`${decl.prop}: ${parsedValue.toString()};`),
62-
);
63-
} else {
64-
decl.value = parsedValue.toString();
65-
}
66-
67-
//
68-
// Handlers
69-
//
17+
export default createSassMigrator(
18+
'replace-sass-space',
19+
(_, options, context) => {
20+
const namespacedRem = namespace('rem', options);
21+
22+
return (root) => {
23+
root.walkDecls((decl) => {
24+
if (!spaceProps.has(decl.prop)) return;
25+
26+
/**
27+
* A collection of transformable values to migrate (e.g. decl lengths, functions, etc.)
28+
*
29+
* Note: This is evaluated at the end of each visitor execution to determine whether
30+
* or not to replace the declaration or insert a comment.
31+
*/
32+
const targets: {replaced: boolean}[] = [];
33+
let hasNumericOperator = false;
34+
const parsedValue = valueParser(decl.value);
35+
36+
handleSpaceProps();
37+
38+
if (targets.some(({replaced}) => !replaced || hasNumericOperator)) {
39+
decl.before(
40+
createInlineComment(POLARIS_MIGRATOR_COMMENT, {prose: true}),
41+
);
42+
decl.before(
43+
createInlineComment(`${decl.prop}: ${parsedValue.toString()};`),
44+
);
45+
} else if (context.fix) {
46+
decl.value = parsedValue.toString();
47+
}
48+
49+
//
50+
// Handlers
51+
//
52+
53+
function handleSpaceProps() {
54+
parsedValue.walk((node) => {
55+
if (isNumericOperator(node)) {
56+
hasNumericOperator = true;
57+
return;
58+
}
7059

71-
function handleSpaceProps() {
72-
parsedValue.walk((node) => {
73-
if (isNumericOperator(node)) {
74-
hasNumericOperator = true;
75-
return;
76-
}
60+
if (node.type === 'word') {
61+
if (globalValues.has(node.value)) return;
7762

78-
if (node.type === 'word') {
79-
if (globalValues.has(node.value)) return;
63+
const dimension = valueParser.unit(node.value);
8064

81-
const dimension = valueParser.unit(node.value);
65+
if (!isTransformableLength(dimension)) return;
8266

83-
if (!isTransformableLength(dimension)) return;
67+
targets.push({replaced: false});
8468

85-
targets.push({replaced: false});
69+
const valueInPx = toTransformablePx(node.value);
8670

87-
const valueInPx = toTransformablePx(node.value);
71+
if (!isKeyOf(spaceMap, valueInPx)) return;
8872

89-
if (!isKeyOf(spaceMap, valueInPx)) return;
73+
targets[targets.length - 1]!.replaced = true;
9074

91-
targets[targets.length - 1]!.replaced = true;
75+
node.value = `var(${spaceMap[valueInPx]})`;
76+
return;
77+
}
9278

93-
node.value = `var(${spaceMap[valueInPx]})`;
94-
return;
95-
}
79+
if (node.type === 'function') {
80+
if (isSassFunction(namespacedRem, node)) {
81+
targets.push({replaced: false});
9682

97-
if (node.type === 'function') {
98-
if (isSassFunction(namespacedRem, node)) {
99-
targets.push({replaced: false});
83+
const args = getFunctionArgs(node);
10084

101-
const args = getFunctionArgs(node);
85+
if (args.length !== 1) return;
10286

103-
if (args.length !== 1) return;
87+
const valueInPx = toTransformablePx(args[0]);
10488

105-
const valueInPx = toTransformablePx(args[0]);
89+
if (!isKeyOf(spaceMap, valueInPx)) return;
10690

107-
if (!isKeyOf(spaceMap, valueInPx)) return;
91+
targets[targets.length - 1]!.replaced = true;
10892

109-
targets[targets.length - 1]!.replaced = true;
93+
node.value = 'var';
94+
node.nodes = [
95+
{
96+
type: 'word',
97+
value: spaceMap[valueInPx],
98+
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
99+
sourceEndIndex: spaceMap[valueInPx].length,
100+
},
101+
];
102+
}
110103

111-
node.value = 'var';
112-
node.nodes = [
113-
{
114-
type: 'word',
115-
value: spaceMap[valueInPx],
116-
sourceIndex: node.nodes[0]?.sourceIndex ?? 0,
117-
sourceEndIndex: spaceMap[valueInPx].length,
118-
},
119-
];
104+
return StopWalkingFunctionNodes;
120105
}
121-
122-
return StopWalkingFunctionNodes;
123-
}
124-
});
125-
}
126-
},
127-
};
128-
};
106+
});
107+
}
108+
});
109+
};
110+
},
111+
);
129112

130113
const globalValues = new Set(['inherit', 'initial', 'unset']);
131114

polaris-migrator/src/utilities/sass.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import postcss from 'postcss';
1+
import type {FileInfo, API, Options} from 'jscodeshift';
2+
import postcss, {Root, Result, Plugin} from 'postcss';
23
import valueParser, {
34
Node,
45
ParsedValue,
@@ -251,3 +252,52 @@ export function createInlineComment(text: string, options?: {prose?: boolean}) {
251252

252253
return comment;
253254
}
255+
256+
interface PluginOptions extends Options, NamespaceOptions {}
257+
258+
interface PluginContext {
259+
fix: boolean;
260+
}
261+
export type PolarisMigrator = (
262+
primaryOption: true,
263+
secondaryOptions: PluginOptions,
264+
context: PluginContext,
265+
) => (root: Root, result: Result) => void;
266+
267+
export function createSassMigrator(name: string, ruleFn: PolarisMigrator) {
268+
return (fileInfo: FileInfo, _: API, options: Options) => {
269+
const plugin: Plugin = {
270+
postcssPlugin: name,
271+
// PostCSS will rewalk the AST every time a declaration/rule/etc is
272+
// mutated by a plugin. This can be useful in some cases, but in ours we
273+
// only want single-pass behaviour.
274+
//
275+
// This can be avoided in 2 ways:
276+
//
277+
// 1) Flagging each declaration as we pass it, then skipping it on
278+
// subsequent passes.
279+
// 2) Using postcss's Once() plugin callback.
280+
//
281+
// We're going with the Once() callback as it's idomatic PostCSS.
282+
Once(root, {result}) {
283+
// NOTE: For fullest compatibility with stylelint, we initialise the
284+
// rule here _inside_ the postcss Once function so multiple passes can
285+
// be performed without rules accidentally retaining scoped variables,
286+
// etc.
287+
ruleFn(
288+
// Normally, this comes from stylelint config, but for this shim we
289+
// just hard-code it, and instead rely on the "seconary" options
290+
// object for passing through the jscodeshift options.
291+
true,
292+
options,
293+
// Also normally comes from styelint via the cli `--fix` flag.
294+
{fix: true},
295+
)(root, result);
296+
},
297+
};
298+
299+
return postcss(plugin).process(fileInfo.source, {
300+
syntax: require('postcss-scss'),
301+
}).css;
302+
};
303+
}
Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
1-
import type {FileInfo} from 'jscodeshift';
2-
import postcss, {Plugin} from 'postcss';
31
import valueParser from 'postcss-value-parser';
42

5-
const processed = Symbol('processed');
6-
7-
const plugin = (): Plugin => ({
8-
postcssPlugin: '{{kebabCase migrationName}}',
9-
Declaration(decl) {
10-
// @ts-expect-error - Skip if processed so we don't process it again
11-
if (decl[processed]) return;
12-
13-
// const prop = decl.prop;
14-
const parsedValue = valueParser(decl.value);
15-
16-
parsedValue.walk((node) => {
17-
if (!(node.type === 'function' && node.value === 'hello')) return;
18-
19-
node.value = 'world';
3+
import {
4+
isSassFunction,
5+
StopWalkingFunctionNodes,
6+
createSassMigrator,
7+
} from '../../utilities/sass';
8+
9+
// options can be passed in from cli / config.
10+
export default createSassMigrator('{{kebabCase migrationName}}', (_, options, context) => {
11+
return (root) => {
12+
root.walkDecls((decl) => {
13+
const parsedValue = valueParser(decl.value);
14+
15+
parsedValue.walk((node) => {
16+
if (isSassFunction('hello', node)) {
17+
node.value = 'world';
18+
return StopWalkingFunctionNodes;
19+
}
20+
});
21+
22+
if (context.fix) {
23+
decl.value = parsedValue.toString();
24+
}
2025
});
21-
22-
decl.value = parsedValue.toString();
23-
24-
// @ts-expect-error - Mark the declaration as processed
25-
decl[processed] = true;
26-
},
26+
};
2727
});
28-
29-
export default function {{camelCase migrationName}}(fileInfo: FileInfo) {
30-
return postcss(plugin()).process(fileInfo.source, {
31-
syntax: require('postcss-scss'),
32-
}).css;
33-
}

0 commit comments

Comments
 (0)