Skip to content
Closed
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/happy-turtles-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris-migrator': patch
---

Initial release
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ module.exports = {
'@shopify/jsx-no-hardcoded-content': 'off',
},
},
{
files: ['polaris-migrator/**/tests/**/*.{ts,tsx}'],
rules: {
'import/no-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'prefer-object-spread': 'off',
'@shopify/jsx-no-hardcoded-content': 'off',
},
},
{
files: ['polaris-react/src/**/*.{ts,tsx}'],
extends: ['plugin:@shopify/typescript-type-checking'],
Expand Down
1 change: 1 addition & 0 deletions .stylelintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
'**/node_modules/**/*.{css,scss}',
'**/dist/**/*.{css,scss}',
'documentation/guides/legacy-polaris-v8-public-api.scss',
'polaris-migrations/**/tests/**/*.{css,scss}',
'polaris-react/build/**/*.{css,scss}',
'polaris-react/build-internal/**/*.{css,scss}',
'stylelint-polaris/tests/**/*.{css,scss}',
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"polaris-for-figma",
"polaris-for-vscode",
"polaris-icons",
"polaris-migrator",
"polaris-react",
"stylelint-polaris",
"polaris.shopify.com"
Expand All @@ -36,12 +37,13 @@
"changeset": "changeset",
"preversion-packages": "turbo run preversion",
"version-packages": "yarn preversion-packages && changeset version",
"release": "turbo run build --filter=polaris.shopify.com^... && changeset publish",
"release": "turbo run build --filter='!polaris.shopify.com' && changeset publish",
"preversion": "echo \"Error: use @changsets/cli to version packages\" && exit 1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/node": "^7.14.9",
"@babel/plugin-transform-runtime": "^7.18.9",
"@changesets/changelog-github": "^0.4.4",
"@changesets/cli": "^2.23.0",
"@next/eslint-plugin-next": "^12.1.4",
Expand Down
4 changes: 4 additions & 0 deletions polaris-migrator/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": [["@shopify/babel-preset", {"typescript": true, "react": true}]],
"plugins": ["@babel/transform-runtime"]
}
56 changes: 56 additions & 0 deletions polaris-migrator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# [Polaris Migrator](https://polaris.shopify.com/docs/advanced-features)

[![npm version](https://img.shields.io/npm/v/@shopify/polaris-migrator.svg?style=flat)](https://www.npmjs.com/package/@shopify/polaris-migrator)

Codemod transformations to help upgrade your Polaris codebase.

## Usage

```sh
npx @shopify/polaris-migrator <transform> <path>
```

- `transform` - name of transform, see available transforms below.
- `path` - files or directory to transform
- `--dry` Do a dry-run, no code will be edited
- `--print` Prints the changed output for comparison

## Documentation

Visit [polaris.shopify.com/docs/advanced-features/migrations](https://polaris.shopify.com/docs/advanced-features/migrations) to view available migrations.

## Development

Start the `dev` npm script to run the build process in watch mode.

```sh
yarn workspace @shopify/polaris-migrator dev
```

Then, use the `start` script to execute the migrator in a separate terminal.

```sh
# Run the CLI script
yarn workspace @shopify/polaris-migrator start template-babel "**/template-babel.input.ts"
```

You can also install the script and test the package globally on your local machine.

```sh
cd polaris-migrator
npm i -g
```

Once that is done, the package can now run using `polaris-migrator`.

```sh
# Usage
polaris-migrator <migration> <path>

# Example
polaris-migrator template-babel "./src/**/template-babel.input.ts" --dry --print --force
```

## Writing a migration

Create a new migration by copying one of the template examples (ex: `template-babel` or `template-sass`). Make the desired migration adjustments to the copied template and update the tests to validate your migration.
8 changes: 8 additions & 0 deletions polaris-migrator/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
transform: {
'\\.[jt]sx?$': [
'babel-jest',
{targets: 'current node', envName: 'test', rootMode: 'upward'},
],
},
};
46 changes: 46 additions & 0 deletions polaris-migrator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@shopify/polaris-migrator",
"version": "0.0.0",
"description": "Codemod transformations to help upgrade your Polaris codebase",
"license": "SEE LICENSE IN LICENSE.md",
"author": "Shopify <[email protected]>",
"homepage": "https://polaris.shopify.com",
"repository": "https://github.com/Shopify/polaris",
"bugs": {
"url": "https://github.com/Shopify/polaris/issues"
},
"publishConfig": {
"access": "public",
"@shopify:registry": "https://registry.npmjs.org"
},
"bin": "dist/index.js",
"main": "dist/index.js",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "node ./dist/index.js",
"lint": "TIMING=1 eslint --cache .",
"test": "jest",
"clean": "rm -rf .turbo node_modules dist *.tsbuildinfo"
},
"dependencies": {
"@babel/core": "^7.18.9",
"@babel/plugin-transform-runtime": "^7.18.9",
"@babel/runtime": "^7.18.9",
"@babel/helper-plugin-utils": "^7.18.9",
"@shopify/babel-preset": "^24.1.5",
"chalk": "^4.1.0",
"globby": "11.0.1",
"is-git-clean": "^1.1.0",
"jscodeshift": "^0.13.1",
"meow": "^9.0.0",
"p-map": "^4.0.0",
"postcss-scss": "^4.0.4",
"postcss": "^8.4.14"
},
"devDependencies": {
"@types/babel__helper-plugin-utils": "^7.10.0",
"@types/is-git-clean": "^1.1.0",
"rollup-plugin-preserve-shebang": "^1.0.1"
}
}
52 changes: 52 additions & 0 deletions polaris-migrator/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as url from 'node:url';

import {babel} from '@rollup/plugin-babel';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import shebang from 'rollup-plugin-preserve-shebang';
import globby from 'globby';

const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json')));

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

const migrationPaths = globby.sync(
path.join(__dirname, './src/migrations/*/index.ts'),
);

/** @type {import('rollup').RollupOptions} */
export default {
input: ['src/index.ts', ...migrationPaths],
output: [
{
format: /** @type {const} */ ('cjs'),
entryFileNames: '[name][assetExtname].js',
dir: path.dirname(pkg.main),
preserveModules: true,
},
],
plugins: [
shebang(),
// Allows node_modules resolution
nodeResolve({extensions, preferBuiltins: true}),
// Allow bundling cjs modules. Rollup doesn't understand cjs
commonjs(),
// Compile TypeScript/JavaScript files
babel({
extensions,
rootMode: 'upward',
include: ['src/**/*'],
babelHelpers: 'runtime',
}),
],
external: [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
// https://www.npmjs.com/package/@rollup/plugin-babel#user-content-babelhelpers
/@babel\/runtime/,
/node_modules/,
],
};
111 changes: 111 additions & 0 deletions polaris-migrator/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

import chalk from 'chalk';
import globby from 'globby';
import meow from 'meow';
import pMap from 'p-map';

import type {MigrationFn} from './types';
import {checkGitStatus} from './utilities/checkGitStatus';

const cli = meow({
description: 'Code migrations for updating Polaris apps.',
help: `
Usage
$ npx @shopify/polaris-migrator <migration> <path> <...options>
migration One of the choices from https://polaris.shopify.com/docs/advanced-features/migrations
path Files or directory to transform. Can be a glob like src/**.scss
Options
--force Bypass Git safety checks and forcibly run migrations
--dry Dry run (no changes are made to files)
--print Print transformed files to your terminal
`,
flags: {
force: {
alias: 'f',
type: 'boolean',
},
dry: {
type: 'boolean',
},
print: {
type: 'boolean',
},
},
});

export async function run() {
const [migration, pathGlob] = cli.input;

if (!cli.flags.dry) {
checkGitStatus(cli.flags.force);
}

if (!migration) {
throw new Error(
`Missing migration argument. Ex. @shopify/polaris-migrator <migration>`,
);
}

if (!pathGlob) {
throw new Error(
`Missing path argument. Ex. @shopify/polaris-migrator <migration> <path>`,
);
}

const migrationNames = await fs.promises.readdir(
path.join(__dirname, './migrations'),
);

if (!migrationNames.includes(migration)) {
throw new Error(`No migration found for ${migration}`);
}

const filePaths = await globby(pathGlob, {
absolute: true,
});

if (filePaths.length === 0) {
throw new Error(`No files found for ${pathGlob}`);
}

const {migration: migrationFn}: {migration: MigrationFn} = await import(
`./migrations/${migration}/index.js`
);

process.stdout.write(`${chalk.green('Running migration:')} ${migration}\n`);

await pMap(
filePaths,
async (filePath) => {
const fileName = path.basename(filePath);
const extName = path.extname(fileName);

if (!migrationFn.extensions.includes(extName)) {
return;
}

const content = await fs.promises.readFile(filePath, 'utf-8');
const newContent = migrationFn(content);

if (typeof newContent !== 'string') {
throw new Error(`Unable to run migration on ${filePath}`);
}

if (cli.flags.print) {
process.stdout.write(`${chalk.blue('File:')} ${fileName}\n`);
process.stdout.write(`${newContent}\n`);
}

if (!cli.flags.dry) {
process.stdout.write(
`Writing content: ${fileName}\n${chalk.green('Done.')}\n`,
);
// await fs.promises.writeFile(filePath, newContent);
}
},
{concurrency: os.cpus.length || Infinity},
);
}
6 changes: 6 additions & 0 deletions polaris-migrator/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node

/* eslint-disable-next-line node/shebang */
import {run} from './cli';

run().catch(console.error);
30 changes: 30 additions & 0 deletions polaris-migrator/src/migrations/replace-sass-spacing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {postcss, Plugin} from '../../runners/postcss';
import {createRegexFromMap} from '../../utilities/regex';
import type {MigrationFn} from '../../types';

const spacingMap = {
'spacing(none)': 'var(--p-space-0)',
'spacing(extra-tight)': 'var(--p-space-1)',
'spacing(tight)': 'var(--p-space-2)',
'spacing(base-tight)': 'var(--p-space-3)',
'spacing()': 'var(--p-space-4)',
'spacing(base)': 'var(--p-space-4)',
'spacing(loose)': 'var(--p-space-5)',
'spacing(extra-loose)': 'var(--p-space-8)',
};

const plugin = (): Plugin => ({
postcssPlugin: 'ReplaceSassSpacing',
Declaration(decl) {
decl.value = decl.value.replace(
createRegexFromMap(spacingMap),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of the usage of regex here. It looks like it can match false positives like my-spacing(), and miss variations in whitespace/quoting like spacing( none ), spacing('none') and spacing( "none" ) (admittedly as we use prettier our whitespace should be pretty conventional so this shouldn't be a huuuge issue for our codebases).

I'd expect us to have a better AST here. There should be AST nodes for stuff like "this is a function with a name and these specific arguments" - you should be able to be able to say "only act when you find a function name, whose name is "spacing" and has these specific values as the first argument". We should leverage a tokeniser, rather than trying to fuzzy match ourselves. It looks like this behaviour of transforming values into "this is a function call" etc is provided by postcss-value-parser

stylelint has a demo of using postcss-value-parser https://github.com/stylelint/stylelint/blob/main/lib/rules/function-allowed-list/index.js#L36

(value) => spacingMap[value as keyof typeof spacingMap],
);
},
});

export const migration: MigrationFn = (fileContent: string) => {
return postcss(plugin).process(fileContent);
};

migration.extensions = ['.scss'];
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.Card {
padding: spacing(loose) spacing(extra-loose);
border-radius: var(--p-border-radius-2, border-radius());
box-shadow: var(--p-shadow-card, shadow());
}

.ButtonContainer {
right: spacing();
top: spacing(loose);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.Card {
padding: var(--p-space-5) var(--p-space-8);
border-radius: var(--p-border-radius-2, border-radius());
box-shadow: var(--p-shadow-card, shadow());
}

.ButtonContainer {
right: var(--p-space-4);
top: var(--p-space-5);
}
Loading