Skip to content

Commit 1ea0194

Browse files
committed
createSassMigrator method to wrap common functionality
1 parent 60191f3 commit 1ea0194

File tree

6 files changed

+131
-86
lines changed

6 files changed

+131
-86
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: 40 additions & 32 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,45 +276,44 @@ 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
280-
281-
import type {FileInfo} from 'jscodeshift';
282-
import postcss, {Plugin} from 'postcss';
283287
import valueParser from 'postcss-value-parser';
284288

285-
const plugin = (): Plugin => ({
286-
postcssPlugin: 'replace-sass-function',
287-
Declaration(decl) {
288-
// const prop = decl.prop;
289-
const parsedValue = valueParser(decl.value);
290-
291-
parsedValue.walk((node) => {
292-
if (!(node.type === 'function' && node.value === 'hello')) return;
293-
294-
node.value = 'world';
295-
});
296-
297-
decl.value = parsedValue.toString();
298-
},
289+
import {
290+
isSassFunction,
291+
StopWalkingFunctionNodes,
292+
createSassMigrator,
293+
} from '../../utilities/sass';
294+
295+
// options can be passed in from cli / config.
296+
export default createSassMigrator('replace-sass-function', (/* options */) => {
297+
return {
298+
Declaration(decl) {
299+
const parsedValue = valueParser(decl.value);
300+
301+
parsedValue.walk((node) => {
302+
if (isSassFunction('hello', node)) {
303+
node.value = 'world';
304+
return StopWalkingFunctionNodes;
305+
}
306+
});
307+
308+
decl.value = parsedValue.toString();
309+
},
310+
};
299311
});
300-
301-
export default function replaceSassFunction(fileInfo: FileInfo) {
302-
return postcss(plugin()).process(fileInfo.source, {
303-
syntax: require('postcss-scss'),
304-
}).css;
305-
}
306312
```
307313

308-
This example migration will replace the Sass function `hello()` with `world()`.
314+
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).
309315

310-
### Testing
316+
#### Testing
311317

312318
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:
313319

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

318324
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).
319325

326+
## Running Migrations
327+
320328
Run tests locally from workspace root by filtering to the migrations package:
321329

322330
```sh

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

Lines changed: 3 additions & 23 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,35 +8,17 @@ 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 => {
17+
export default createSassMigrator('replace-sass-space', (options) => {
3418
const namespacedRem = namespace('rem', options);
3519

3620
return {
37-
postcssPlugin: 'replace-sass-space',
3821
Declaration(decl) {
39-
// @ts-expect-error - Skip if processed so we don't process it again
40-
if (decl[processed]) return;
41-
4222
if (!spaceProps.has(decl.prop)) return;
4323

4424
/**
@@ -125,7 +105,7 @@ const plugin = (options: PluginOptions = {}): Plugin => {
125105
}
126106
},
127107
};
128-
};
108+
});
129109

130110
const globalValues = new Set(['inherit', 'initial', 'unset']);
131111

polaris-migrator/src/utilities/sass.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import postcss from 'postcss';
1+
import type {FileInfo, API, Options} from 'jscodeshift';
2+
import postcss, {
3+
Root,
4+
Plugin,
5+
Declaration,
6+
Rule as PostCSSRule,
7+
Comment as PostCSSComment,
8+
AtRule,
9+
} from 'postcss';
210
import valueParser, {
311
Node,
412
ParsedValue,
@@ -251,3 +259,50 @@ export function createInlineComment(text: string, options?: {prose?: boolean}) {
251259

252260
return comment;
253261
}
262+
263+
interface PluginOptions extends Options, NamespaceOptions {}
264+
265+
interface PostCSSWalkers {
266+
AtRule?: (node: AtRule) => void;
267+
Comment?: (node: PostCSSComment) => void;
268+
Declaration?: (node: Declaration) => void;
269+
Rule?: (node: PostCSSRule) => void;
270+
}
271+
272+
export function createSassMigrator(
273+
name: string,
274+
ruleFn: (options: PluginOptions) => PostCSSWalkers,
275+
) {
276+
return (fileInfo: FileInfo, _: API, options: Options) => {
277+
const plugin: Plugin = {
278+
postcssPlugin: name,
279+
// PostCSS will rewalk the AST every time a declaration/rule/etc is
280+
// mutated by a plugin. This can be useful in some cases, but in ours we
281+
// only want single-pass behaviour.
282+
//
283+
// This can be avoided in 2 ways:
284+
//
285+
// 1) Flagging each declaration as we pass it, then skipping it on
286+
// subsequent passes.
287+
// 2) Using postcss's Once() plugin callback.
288+
//
289+
// We're going with the Once() callback as it's idomatic PostCSS.
290+
Once(root: Root) {
291+
// NOTE: We initialise the rule here _inside_ the postcss Once function
292+
// so multiple passes can be performed without rules accidentally
293+
// retaining scoped variables, etc.
294+
const rules = ruleFn(options);
295+
296+
if (rules.AtRule) root.walkAtRules((node) => rules.AtRule(node));
297+
if (rules.Comment) root.walkComments((node) => rules.Comment(node));
298+
if (rules.Declaration)
299+
root.walkDecls((node) => rules.Declaration(node));
300+
if (rules.Rule) root.walkRules((node) => rules.Rule(node));
301+
},
302+
};
303+
304+
return postcss(plugin).process(fileInfo.source, {
305+
syntax: require('postcss-scss'),
306+
}).css;
307+
};
308+
}
Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
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';
20-
});
21-
22-
decl.value = parsedValue.toString();
23-
24-
// @ts-expect-error - Mark the declaration as processed
25-
decl[processed] = true;
26-
},
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 */) => {
11+
return {
12+
Declaration(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+
decl.value = parsedValue.toString();
23+
},
24+
};
2725
});
28-
29-
export default function {{camelCase migrationName}}(fileInfo: FileInfo) {
30-
return postcss(plugin()).process(fileInfo.source, {
31-
syntax: require('postcss-scss'),
32-
}).css;
33-
}

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16568,6 +16568,11 @@ obuf@^1.0.0, obuf@^1.1.2:
1656816568
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
1656916569
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
1657016570

16571+
on-change@^4.0.1:
16572+
version "4.0.1"
16573+
resolved "https://registry.yarnpkg.com/on-change/-/on-change-4.0.1.tgz#a3fea9971748efab3560351c8278ea1a692b7c16"
16574+
integrity sha512-Gxmr/8NsLTigoUUEPvnAIUGl7uxwfC3Mm1G+qTmODEZcT4ZGfeaFyQgfGCdbKVS2pICIFYB82RH6MUHToPcS4Q==
16575+
1657116576
on-exit-leak-free@^2.1.0:
1657216577
version "2.1.0"
1657316578
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"

0 commit comments

Comments
 (0)