Skip to content

Commit 996e4fa

Browse files
authored
Create @shopify/polaris-migrator package (Shopify#6852)
Closes Shopify#6943 Adds the beginnings of the `@shopify/polaris-migrator` package. This package applies code transformations to help update Polaris apps and the Polaris codebase. ## Usage ```sh npx @shopify/polaris-migrator <migration> <path> ``` Run tests for `@shopify/polaris-migrator` ```sh yarn test --filter=@shopify/polaris-migrator ``` Original PR branch: Shopify#6701
1 parent 4418738 commit 996e4fa

29 files changed

+1467
-581
lines changed

.eslintrc.js

+15
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,21 @@ module.exports = {
8787
'@shopify/jsx-no-hardcoded-content': 'off',
8888
},
8989
},
90+
{
91+
files: ['polaris-migrator/src/**/*.{ts,tsx}'],
92+
rules: {
93+
'import/no-default-export': 'off',
94+
},
95+
},
96+
{
97+
files: ['polaris-migrator/src/**/tests/*.{ts,tsx}'],
98+
rules: {
99+
'import/no-default-export': 'off',
100+
'import/no-extraneous-dependencies': 'off',
101+
'@shopify/jsx-no-hardcoded-content': 'off',
102+
'@typescript-eslint/ban-ts-comment': 'off',
103+
},
104+
},
90105
{
91106
files: ['polaris-react/src/**/*.{ts,tsx}'],
92107
extends: ['plugin:@shopify/typescript-type-checking'],

.stylelintrc.js

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ module.exports = {
1616
'./stylelint-polaris/configs/internal',
1717
],
1818
},
19+
{
20+
files: ['polaris-migrator/**/tests/*.{css,scss}'],
21+
rules: {
22+
'declaration-property-value-disallowed-list': 'off',
23+
},
24+
},
1925
],
2026
ignoreFiles: [
2127
'**/.next/**/*.{css,scss}',

babel.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module.exports = {
77
'.',
88
// Note: The following projects use rootMode: 'upward' to inherit
99
// and merge with this root level config.
10+
'./polaris-migrator',
1011
'./polaris-tokens',
1112
'./polaris-icons',
1213
'./polaris-react',

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"polaris-for-figma",
1212
"polaris-for-vscode",
1313
"polaris-icons",
14+
"polaris-migrator",
1415
"polaris-react",
1516
"stylelint-polaris",
1617
"polaris.shopify.com"
@@ -37,7 +38,7 @@
3738
"gen-assets": "turbo run gen-assets --filter=polaris.shopify.com",
3839
"preversion-packages": "turbo run preversion",
3940
"version-packages": "yarn preversion-packages && changeset version",
40-
"release": "turbo run build --filter=polaris.shopify.com^... && changeset publish",
41+
"release": "turbo run build --filter='!polaris.shopify.com' && changeset publish",
4142
"preversion": "echo \"Error: use @changsets/cli to version packages\" && exit 1"
4243
},
4344
"devDependencies": {

polaris-migrator/README.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# [Polaris Migrator](https://polaris.shopify.com/docs/advanced-features)
2+
3+
[![npm version](https://img.shields.io/npm/v/@shopify/polaris-migrator.svg?style=flat)](https://www.npmjs.com/package/@shopify/polaris-migrator)
4+
5+
Codemod transformations to help upgrade your Polaris codebase.
6+
7+
## Usage
8+
9+
```sh
10+
npx @shopify/polaris-migrator <migration> <path>
11+
```
12+
13+
- `migration` - name of migration, see available migrations on the docs site below.
14+
- `path` - files or directory to perform migration
15+
- `--dry` Do a dry-run, no code will be edited
16+
- `--print` Prints the changed output for comparison
17+
18+
## Documentation
19+
20+
Visit [polaris.shopify.com/docs/advanced-features/migrations](https://polaris.shopify.com/docs/advanced-features/migrations) to view available migrations.

polaris-migrator/bin/migrator-cli.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env node
2+
3+
const {run} = require('./../dist/cjs');
4+
5+
run();

polaris-migrator/jest.config.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
transform: {
3+
'\\.(js|tsx?)$': [
4+
'babel-jest',
5+
{targets: 'current node', envName: 'test', rootMode: 'upward'},
6+
],
7+
},
8+
};

polaris-migrator/package.json

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@shopify/polaris-migrator",
3+
"version": "0.0.0",
4+
"description": "Codemod transformations to help upgrade your Polaris codebase",
5+
"license": "SEE LICENSE IN LICENSE.md",
6+
"author": "Shopify <[email protected]>",
7+
"homepage": "https://polaris.shopify.com",
8+
"repository": "https://github.com/Shopify/polaris",
9+
"bugs": {
10+
"url": "https://github.com/Shopify/polaris/issues"
11+
},
12+
"publishConfig": {
13+
"access": "public",
14+
"@shopify:registry": "https://registry.npmjs.org"
15+
},
16+
"main": "dist/cjs/index.js",
17+
"module": "dist/esm/index.mjs",
18+
"types": "dist/types/index.d.ts",
19+
"bin": "bin/migrator-cli.js",
20+
"files": [
21+
"bin",
22+
"dist"
23+
],
24+
"scripts": {
25+
"build": "run-s build:*",
26+
"build:js": "rollup -c",
27+
"build:types": "tsc -b",
28+
"dev": "rollup -c -w",
29+
"start": "./bin/migrator-cli.js",
30+
"lint": "TIMING=1 eslint --cache .",
31+
"test": "jest",
32+
"clean": "rm -rf .turbo node_modules dist *.tsbuildinfo"
33+
},
34+
"dependencies": {
35+
"chalk": "^4.1.0",
36+
"globby": "11.0.1",
37+
"is-git-clean": "^1.1.0",
38+
"jscodeshift": "^0.13.1",
39+
"meow": "^9.0.0",
40+
"postcss": "^8.4.14",
41+
"postcss-scss": "^4.0.4",
42+
"postcss-value-parser": "^4.2.0"
43+
},
44+
"devDependencies": {
45+
"@types/is-git-clean": "^1.1.0",
46+
"@types/jscodeshift": "^0.11.5",
47+
"@rollup/plugin-babel": "^5.3.1",
48+
"@rollup/plugin-commonjs": "^22.0.2",
49+
"@rollup/plugin-json": "^4.1.0",
50+
"@rollup/plugin-node-resolve": "^13.3.0",
51+
"@shopify/polaris": "^10.0.0",
52+
"prettier": "^2.7.1"
53+
}
54+
}

polaris-migrator/rollup.config.mjs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import * as url from 'node:url';
4+
5+
import {babel} from '@rollup/plugin-babel';
6+
import {nodeResolve} from '@rollup/plugin-node-resolve';
7+
import commonjs from '@rollup/plugin-commonjs';
8+
import json from '@rollup/plugin-json';
9+
import globby from 'globby';
10+
11+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
12+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')));
13+
14+
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
15+
16+
const migrationPaths = globby.sync('./src/migrations/*!(tests)/*.ts');
17+
18+
/** @type {import('rollup').RollupOptions} */
19+
export default {
20+
input: ['src/index.ts', ...migrationPaths],
21+
output: [
22+
{
23+
format: /** @type {const} */ ('cjs'),
24+
entryFileNames: '[name][assetExtname].js',
25+
dir: path.dirname(pkg.main),
26+
preserveModules: true,
27+
exports: 'auto',
28+
},
29+
{
30+
format: /** @type {const} */ ('esm'),
31+
entryFileNames: '[name][assetExtname].mjs',
32+
dir: path.dirname(pkg.module),
33+
preserveModules: true,
34+
},
35+
],
36+
plugins: [
37+
// Allows node_modules resolution
38+
nodeResolve({extensions, preferBuiltins: true}),
39+
// Allow bundling cjs modules. Rollup doesn't understand cjs
40+
commonjs(),
41+
// Compile TypeScript/JavaScript files
42+
babel({
43+
extensions,
44+
rootMode: 'upward',
45+
include: ['src/**/*'],
46+
babelHelpers: 'bundled',
47+
envName: 'production',
48+
targets: 'node 14.13',
49+
}),
50+
json({compact: true}),
51+
],
52+
external: [
53+
...Object.keys(pkg.dependencies ?? {}),
54+
...Object.keys(pkg.peerDependencies ?? {}),
55+
'jscodeshift/src/Runner',
56+
],
57+
};

polaris-migrator/src/cli.ts

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
// @ts-expect-error TS can't resolve the type of this import
5+
import * as jscodeshift from 'jscodeshift/src/Runner';
6+
import chalk from 'chalk';
7+
import isGitClean from 'is-git-clean';
8+
import globby from 'globby';
9+
import meow from 'meow';
10+
11+
const cli = meow({
12+
description: 'Code migrations for updating Polaris apps.',
13+
help: `
14+
Usage
15+
$ npx @shopify/polaris-migrator <migration> <path> <...options>
16+
migration One of the choices from https://polaris.shopify.com/docs/advanced-features/migrations
17+
path Files or directory to transform. Can be a glob like src/**.scss
18+
Options
19+
--force Bypass Git safety checks and forcibly run migrations
20+
--dry Dry run (no changes are made to files)
21+
--print Print transformed files to your terminal
22+
`,
23+
flags: {
24+
force: {
25+
type: 'boolean',
26+
},
27+
dry: {
28+
type: 'boolean',
29+
},
30+
print: {
31+
type: 'boolean',
32+
},
33+
},
34+
});
35+
36+
export function checkGitStatus(force?: boolean) {
37+
let clean = false;
38+
let errorMessage = 'Unable to determine if git directory is clean';
39+
try {
40+
clean = isGitClean.sync(process.cwd());
41+
errorMessage = 'Git directory is not clean';
42+
} catch (err: any) {
43+
if (err && err.stderr && err.stderr.indexOf('Not a git repository') >= 0) {
44+
clean = true;
45+
}
46+
}
47+
48+
if (!clean) {
49+
/* eslint-disable no-console */
50+
if (force) {
51+
console.log(`WARNING: ${errorMessage}. Forcibly continuing.`);
52+
} else {
53+
console.log('Thank you for using @shopify/polaris-migrator!');
54+
console.log(
55+
chalk.yellow(
56+
'\nBut before we continue, please stash or commit your git changes.',
57+
),
58+
);
59+
console.log(
60+
'\nYou may use the --force flag to override this safety check.',
61+
);
62+
process.exit(1);
63+
}
64+
/* eslint-enable no-console */
65+
}
66+
}
67+
68+
type Args = [
69+
migration: string,
70+
path: string,
71+
flags?: {force?: boolean; dry?: boolean; print?: boolean},
72+
];
73+
74+
export async function run(...args: Args) {
75+
const [migration, files] = args.length ? args : cli.input;
76+
const flags = args && args[2] ? args[2] : cli.flags;
77+
78+
const migrationFile = path.join(
79+
__dirname,
80+
`./migrations/${migration}/${migration}.js`,
81+
);
82+
83+
try {
84+
if (!fs.existsSync(migrationFile)) {
85+
throw new Error(`No migration found for ${migration}`);
86+
}
87+
88+
if (!files) throw new Error(`No path provided for migration`);
89+
90+
if (!flags.dry) {
91+
checkGitStatus(cli.flags.force);
92+
}
93+
94+
const filepaths = globby.sync(files, {cwd: process.cwd()});
95+
if (filepaths.length === 0) {
96+
throw new Error(`No files found for ${files}`);
97+
}
98+
99+
// eslint-disable-next-line no-console
100+
console.log(chalk.green('Running migration:'), migration);
101+
102+
await jscodeshift.run(migrationFile, filepaths, {
103+
dry: flags.dry,
104+
print: flags.print,
105+
babel: true,
106+
ignorePattern: ['**/node_modules/**', '**/.next/**', '**/build/**'],
107+
extensions: 'tsx,ts,jsx,js',
108+
parser: 'tsx',
109+
verbose: 2,
110+
runInBand: true,
111+
silent: false,
112+
stdin: false,
113+
});
114+
} catch (error) {
115+
// eslint-disable-next-line no-console
116+
console.error(error);
117+
process.exit(1);
118+
}
119+
}

polaris-migrator/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {run} from './cli';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {FileInfo} from 'jscodeshift';
2+
import postcss, {Plugin} from 'postcss';
3+
import valueParser, {Node, WordNode, FunctionNode} from 'postcss-value-parser';
4+
5+
const spacingMap = {
6+
none: '--p-space-0',
7+
'extra-tight': '--p-space-1',
8+
tight: '--p-space-2',
9+
'base-tight': '--p-space-3',
10+
'': '--p-space-4',
11+
base: '--p-space-4',
12+
loose: '--p-space-5',
13+
'extra-loose': '--p-space-8',
14+
};
15+
16+
type Spacing = keyof typeof spacingMap;
17+
18+
function isSpacingFn(node: Node): node is FunctionNode {
19+
return node.type === 'function' && node.value === 'spacing';
20+
}
21+
22+
function isOperator(node: Node): boolean {
23+
return (
24+
node.value === '+' ||
25+
node.value === '-' ||
26+
node.value === '*' ||
27+
node.value === '/'
28+
);
29+
}
30+
31+
const plugin = (): Plugin => ({
32+
postcssPlugin: 'ReplaceSassSpacing',
33+
Declaration(decl) {
34+
const parsed = valueParser(decl.value);
35+
36+
// Skip if the value contains calculations
37+
const containsCalculation = parsed.nodes.some(isOperator);
38+
if (containsCalculation) return;
39+
40+
parsed.walk((node) => {
41+
if (!isSpacingFn(node)) return;
42+
const hasNodes = Boolean(node.nodes && node.nodes.length);
43+
const spacing = hasNodes ? node.nodes[0].value : '';
44+
const newSpacing = spacingMap[spacing as Spacing];
45+
46+
node.value = 'var';
47+
node.nodes = hasNodes ? node.nodes : [{type: 'word'} as WordNode];
48+
node.nodes[0].value = newSpacing;
49+
});
50+
51+
decl.value = parsed.toString();
52+
},
53+
});
54+
55+
export default function replaceSassSpacing(file: FileInfo) {
56+
return postcss(plugin()).process(file.source, {
57+
syntax: require('postcss-scss'),
58+
}).css;
59+
}

0 commit comments

Comments
 (0)