-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Create @shopify/polaris-migrator package
#6701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@shopify/polaris-migrator': patch | ||
| --- | ||
|
|
||
| Initial release |
| 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"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # [Polaris Migrator](https://polaris.shopify.com/docs/advanced-features) | ||
|
|
||
| [](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. |
| 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'}, | ||
| ], | ||
| }, | ||
| }; |
| 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" | ||
| } | ||
| } | ||
| 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', | ||
sam-b-rose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }), | ||
| ], | ||
| external: [ | ||
| ...Object.keys(pkg.dependencies ?? {}), | ||
| ...Object.keys(pkg.peerDependencies ?? {}), | ||
| // https://www.npmjs.com/package/@rollup/plugin-babel#user-content-babelhelpers | ||
| /@babel\/runtime/, | ||
| /node_modules/, | ||
sam-b-rose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import * as fs from 'node:fs'; | ||
sam-b-rose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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}, | ||
| ); | ||
| } | ||
| 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); |
| 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), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 stylelint has a demo of using |
||
| (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); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.