diff --git a/.changeset/great-cougars-explode.md b/.changeset/great-cougars-explode.md new file mode 100644 index 0000000000..1deddebb3d --- /dev/null +++ b/.changeset/great-cougars-explode.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/cli': patch +--- + +Adds pre-release of codemods command diff --git a/.changeset/short-pandas-obey.md b/.changeset/short-pandas-obey.md new file mode 100644 index 0000000000..4f4ac73272 --- /dev/null +++ b/.changeset/short-pandas-obey.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/codemods': patch +--- + +Pre-release of codemods. \ No newline at end of file diff --git a/.github/workflows/sizeDiff.yml b/.github/workflows/sizeDiff.yml index f19e76e6c0..73e9bff29e 100644 --- a/.github/workflows/sizeDiff.yml +++ b/.github/workflows/sizeDiff.yml @@ -19,5 +19,5 @@ jobs: - uses: preactjs/compressed-size-action@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - build-script: 'build' + build-script: 'build --force' clean-script: 'clean' diff --git a/tools/cli/package.json b/tools/cli/package.json index a5da447245..a2aef71ba7 100644 --- a/tools/cli/package.json +++ b/tools/cli/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@lg-tools/build": "0.5.1", + "@lg-tools/codemods": "0.0.1", "@lg-tools/create": "0.2.7", "@lg-tools/install": "0.1.8", "@lg-tools/link": "0.2.3", diff --git a/tools/cli/src/index.ts b/tools/cli/src/index.ts index e46addb3db..f58f805f74 100644 --- a/tools/cli/src/index.ts +++ b/tools/cli/src/index.ts @@ -1,4 +1,5 @@ import { buildPackage, buildTSDoc, buildTypescript } from '@lg-tools/build'; +import { migrator } from '@lg-tools/codemods'; import { createPackage } from '@lg-tools/create'; import { installLeafyGreen } from '@lg-tools/install'; import { linkPackages, unlinkPackages } from '@lg-tools/link'; @@ -166,6 +167,36 @@ cli ) .action(validate); +/** Migrator */ +cli + .command('codemod') + .description('Runs codemod transformations to upgrade LG components') + .argument( + '', + 'One of the codemods from: https://github.com/mongodb/leafygreen-ui/blob/main/tools/codemods/README.md#codemods-1', + ) + .argument( + '[path]', + 'Files or directory to transform. Can be a glob like like src/**.test.js', + ) + .option( + '--i, --ignore ', + 'Glob patterns to ignore. E.g. --i **/node_modules/** **/.next/**', + false, + ) + .option('--d, --dry', 'dry run (no changes are made to files)', false) + .option( + '--p, --print', + 'print transformed files to stdout, useful for development', + false, + ) + .option( + '--f, --force', + 'Bypass Git safety checks and forcibly run codemods', + false, + ) + .action(migrator); + /** Build steps */ cli .command('build-package') diff --git a/tools/cli/tsconfig.json b/tools/cli/tsconfig.json index f2e9faccd3..d18593a4f1 100644 --- a/tools/cli/tsconfig.json +++ b/tools/cli/tsconfig.json @@ -7,13 +7,18 @@ "rootDir": "src", "baseUrl": ".", "paths": { - "@lg-tools/*": ["../*/src"] + "@lg-tools/*": [ + "../*/src" + ] } }, "references": [ { "path": "../build" }, + { + "path": "../codemods" + }, { "path": "../create" }, diff --git a/tools/codemods/CONTRIBUTING.md b/tools/codemods/CONTRIBUTING.md new file mode 100644 index 0000000000..deee5e052e --- /dev/null +++ b/tools/codemods/CONTRIBUTING.md @@ -0,0 +1,142 @@ +# Contributing to codemods + +## Getting Started + +All codemods can be found under `src/codemods` and each codemod should have its own directory. + +``` +src + ┣ codemods # directory for all codemods + ┃ ┣ # directory for individual codemod + ┃ ┃ ┣ tests # directory for codemod tests + ┃ ┃ ┃ ┃ ┣ .input.tsx # input file for test + ┃ ┃ ┃ ┃ ┣ .output.tsx # output file for test + ┃ ┃ ┃ ┃ ┗ transform.spec.ts # jest test file + ┃ ┃ ┣ testing.tsx # (optional) file used to test `yarn lg codemod...` + ┃ ┃ ┗ transform.ts # transformer function (file that modifies the code ) + ┃ ┗ ... + ┗ ... +``` + +Utils can be found under `src/utils` + +``` +src + ┣ utils # directory for all utils + ┃ ┣ transformations # directory for reusable transformations + ┃ ┃ ┣ # directory for individual transformation + ┃ ┃ ┃ ┣ tests # directory for transformation tests + ┃ ┃ ┃ ┃ ┣ .input.tsx # input file for test + ┃ ┃ ┃ ┃ ┣ .output.tsx # output file for test + ┃ ┃ ┃ ┃ ┗ transform.spec.ts # jest test file + ┃ ┃ ┃ ┣ .ts # transformation code + ┃ ┃ ┃ ┣ index.ts # file to add exports + ┃ ┃ ┃ ┗ transform.ts # transfomer function used for testing purposes + ┃ ┃ ┗ ... + ┃ ┗ ... + ┗ ... +``` + +## Creating a codemod + +This package uses [jscodeshift](https://github.com/facebook/jscodeshift) to parse source code into an abstract syntax tree([AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree)). + +Each codemod needs a transformer function. A transformer function takes in a file to transform, a reference to the jscodeshift library and an optional list of options. This function will parse the source code from the file into an AST, perform the transformation on the AST, then convert the modified AST back into source code. + +e.g. + +```jsx +/** + * Example transformer function to consolidate props + * + * @param file the file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @param options an object containing options to pass to the transform function + * @returns Either the modified file or the original file + */ +export default function transformer( + file: FileInfo, + { jscodeshift: j }: API, + options: TransformerOptions, +) { + const source = j(file.source); // Use jscodeshift (j) to parse the source code into an AST + + const { + propToRemove, + propToUpdate, + propMapping, + propToRemoveType, + componentName, + } = options; + + elements.forEach(element => { + // Perform transformations on the AST + consolidateJSXAttributes({ + j, + element, + propToRemove, + propToUpdate, + propMapping, + propToRemoveType, + }); + }); + + return source.toSource(); // Return the transformed source code +} +``` + +Using a tool like [AST Explorer](https://astexplorer.net/) can help you visualize and experiment with Abstract Syntax Trees (ASTs) + +## Testing + +To test codemods, we need to create an input and output file. We then pass the input file through the transformer function and use [Jest](https://jestjs.io/) to verify if the transformed input file matches the output file. + +e.g. + +`input.tsx` + +```tsx +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + <> + + + + ); +}; +``` + +`output.tsx` + +```tsx +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + <> + + + + ); +}; +``` + +`test.spec.ts` + +```js +const formattedOutput = prettier.format(output, { parser }); +const formattedExpected = prettier.format(expected, { parser }); + +// Format output and expected with prettier for white spaces and line breaks consistency +expect(formattedOutput).toBe(formattedExpected); +``` diff --git a/tools/codemods/README.md b/tools/codemods/README.md new file mode 100644 index 0000000000..813931404b --- /dev/null +++ b/tools/codemods/README.md @@ -0,0 +1,148 @@ +# Codemods + +![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/codemods.svg) + +## Installation + +### Yarn + +```shell +yarn add @lg-tools/codemods +``` + +### NPM + +```shell +npm install @lg-tools/codemods +``` + +## Usage + +```jsx +yarn lg codemod [...options] +``` + +### Arguments + +#### `codemod` + +name of codemod, see available codemods below. + +
+ +#### `path` + +files or directory to transform + +
+ +### Options + +#### `--i or --ignore` + +Glob patterns to ignore + +```jsx +yarn lg codemod --ignore **/node_modules/** **/.next/** +``` + +#### `--d or --dry` + +Dry run (no changes to files are made) + +```jsx +yarn lg codemod --dry +``` + +#### `--p or --print` + +Print transformed files to stdout and changes are also made to files + +```jsx +yarn lg codemod --print +``` + +#### `--f or --force` + +Bypass Git safety checks and forcibly run codemods. + +```jsx +yarn lg codemod --force +``` + +## Codemods + +**_NOTE:_ These codemods are for testing purposes only** + +### `consolidate-props` + +This codemod consolidates two props into one. + +```jsx +yarn lg codemod codemode-props +``` + +E.g. +In this example, the `disabled` props is merged into the `state` prop. + +**Before**: + +```jsx + +``` + +**After**: + +```jsx + +``` + +
+ +### `rename-component-prop` + +This codemod renames a component prop + +```jsx +yarn lg codemod codemode-component-prop +``` + +E.g. +In this example, `prop` is renamed to `newProp`. + +**Before**: + +```jsx + +``` + +**After**: + +```jsx + +``` + +
+ +### `update-component-prop-value` + +This codemod updates a prop value + +```jsx +yarn lg codemod codemode-component-prop-value +``` + +E.g. +In this example, `value` is updated to `new prop value`. + +**Before**: + +```jsx + +``` + +**After**: + +```jsx + +``` diff --git a/tools/codemods/package.json b/tools/codemods/package.json new file mode 100644 index 0000000000..08cd3fd38a --- /dev/null +++ b/tools/codemods/package.json @@ -0,0 +1,38 @@ +{ + "name": "@lg-tools/codemods", + "version": "0.0.1", + "description": "Codemods for LeafyGreen UI", + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "license": "Apache-2.0", + "scripts": { + "build": "lg-internal-build-package", + "tsc": "tsc --build tsconfig.json" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "jscodeshift":"0.15.2", + "chalk": "4.1.2", + "glob": "10.3.12", + "is-git-clean": "1.1.0", + "prettier": "2.8.8", + "fs-extra": "11.1.1", + "@lg-tools/build": "0.5.0" + }, + "devDependencies": { + "@types/jscodeshift":"0.11.11", + "@types/is-git-clean": "1.1.0" + + }, + "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/codemods", + "repository": { + "type": "git", + "url": "https://github.com/mongodb/leafygreen-ui" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/PD/summary" + } +} diff --git a/tools/codemods/rollup.config.mjs b/tools/codemods/rollup.config.mjs new file mode 100644 index 0000000000..4ebd010c85 --- /dev/null +++ b/tools/codemods/rollup.config.mjs @@ -0,0 +1,35 @@ +import { esmConfig, umdConfig } from '@lg-tools/build/config/rollup.config.mjs'; +import { glob } from 'glob'; + +const codemodGlobs = glob.sync('./src/codemods/*!(tests)/*.ts'); + +export default [ + esmConfig, + umdConfig, + { + ...esmConfig, + input: [...codemodGlobs], + // This updates the dist/codemods dir to include .js files + output: { + ...esmConfig.output, + // cjs is fully supported in node.js + format: 'cjs', // overrides esm format from esmConfig.output + entryFileNames: '[name].js', + dir: 'dist', + preserveModules: true, + exports: 'auto', + }, + }, + { + ...esmConfig, + input: [...codemodGlobs], + // This updates the dist/esm dir to include the /codemods dir which includes .mjs files + output: { + ...esmConfig.output, + // esm is supported in node.js with the .mjs extension + entryFileNames: '[name].mjs', + preserveModules: true, + exports: 'auto', + }, + }, +]; diff --git a/tools/codemods/src/codemods/consolidate-props/tests/consolidate-props.input.tsx b/tools/codemods/src/codemods/consolidate-props/tests/consolidate-props.input.tsx new file mode 100644 index 0000000000..f4f142b9f0 --- /dev/null +++ b/tools/codemods/src/codemods/consolidate-props/tests/consolidate-props.input.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + const props = { + randomProp: 'value', + }; + + const Test = () => { + return ; + }; + + const TestTwo = () => { + return ( + <> + + + ); + }; + + return ( + <> + + + Hello + + + + + + + + + + + + ); +}; diff --git a/tools/codemods/src/codemods/consolidate-props/tests/consolidate-props.output.tsx b/tools/codemods/src/codemods/consolidate-props/tests/consolidate-props.output.tsx new file mode 100644 index 0000000000..5b3b10dd72 --- /dev/null +++ b/tools/codemods/src/codemods/consolidate-props/tests/consolidate-props.output.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + const props = { + randomProp: 'value', + }; + + const Test = () => { + return ( + /* Please update manually */ + + ); + }; + + const TestTwo = () => { + return ( + <> + {/* Please update manually */} + + + ); + }; + + return ( + <> + + + Hello + + + + + + {/* Please update manually */} + + + + + + + ); +}; diff --git a/tools/codemods/src/codemods/consolidate-props/tests/transform.spec.ts b/tools/codemods/src/codemods/consolidate-props/tests/transform.spec.ts new file mode 100644 index 0000000000..4f4e80052e --- /dev/null +++ b/tools/codemods/src/codemods/consolidate-props/tests/transform.spec.ts @@ -0,0 +1,6 @@ +import { transformTest } from '../../../utils/tests/transformTest'; + +transformTest(__dirname, { + fixture: 'consolidate-props', + transform: 'consolidate-props', +}); diff --git a/tools/codemods/src/codemods/consolidate-props/transform.ts b/tools/codemods/src/codemods/consolidate-props/transform.ts new file mode 100644 index 0000000000..7efab6b426 --- /dev/null +++ b/tools/codemods/src/codemods/consolidate-props/transform.ts @@ -0,0 +1,55 @@ +import type { API, FileInfo } from 'jscodeshift'; + +import { + consolidateJSXAttributes, + ConsolidateJSXAttributesOptions, +} from '../../utils/transformations'; + +type TransformerOptions = ConsolidateJSXAttributesOptions & { + componentName: string; +}; + +/** + * Example transformer function to consolidate props + * + * @param file the file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @param options an object containing options to pass to the transform function + * @returns Either the modified file or the original file + */ +export default function transformer( + file: FileInfo, + { jscodeshift: j }: API, + options: TransformerOptions, +) { + const source = j(file.source); + + const { + propToRemove = 'propToRemove', + propToUpdate = 'propToUpdate', + propMapping = { + value2: 'value3', + }, + propToRemoveType = 'string', + componentName = 'MyComponent', + } = options; + + // Check if the element is on the page + const elements = source.findJSXElements(componentName); + + // If there are no elements then return the original file + if (elements.length === 0) return file.source; + + elements.forEach(element => { + consolidateJSXAttributes({ + j, + element, + propToRemove, + propToUpdate, + propMapping, + propToRemoveType, + }); + }); + + return source.toSource(); +} diff --git a/tools/codemods/src/codemods/rename-component-prop/tests/rename-component-prop.input.tsx b/tools/codemods/src/codemods/rename-component-prop/tests/rename-component-prop.input.tsx new file mode 100644 index 0000000000..7f8a8928c2 --- /dev/null +++ b/tools/codemods/src/codemods/rename-component-prop/tests/rename-component-prop.input.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/codemods/rename-component-prop/tests/rename-component-prop.output.tsx b/tools/codemods/src/codemods/rename-component-prop/tests/rename-component-prop.output.tsx new file mode 100644 index 0000000000..f16bdc105a --- /dev/null +++ b/tools/codemods/src/codemods/rename-component-prop/tests/rename-component-prop.output.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/codemods/rename-component-prop/tests/transform.spec.ts b/tools/codemods/src/codemods/rename-component-prop/tests/transform.spec.ts new file mode 100644 index 0000000000..4e3b9b9f1b --- /dev/null +++ b/tools/codemods/src/codemods/rename-component-prop/tests/transform.spec.ts @@ -0,0 +1,6 @@ +import { transformTest } from '../../../utils/tests/transformTest'; + +transformTest(__dirname, { + fixture: 'rename-component-prop', + transform: 'rename-component-prop', +}); diff --git a/tools/codemods/src/codemods/rename-component-prop/transform.ts b/tools/codemods/src/codemods/rename-component-prop/transform.ts new file mode 100644 index 0000000000..cb70bd53ff --- /dev/null +++ b/tools/codemods/src/codemods/rename-component-prop/transform.ts @@ -0,0 +1,42 @@ +import type { API, FileInfo } from 'jscodeshift'; + +import { + replaceJSXAttributes, + ReplaceJSXAttributesType, +} from '../../utils/transformations'; + +type TransformerOptions = ReplaceJSXAttributesType & { componentName: string }; + +/** + * Example transformer function to rename a component prop + * + * @param file the file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @param options an object containing options to pass to the tranform function + * @returns Either the modified file or the original file + */ +export default function transformer( + file: FileInfo, + { jscodeshift: j }: API, + options: TransformerOptions, +) { + const { + propName = 'prop', + newPropName = 'newProp', + componentName = 'MyComponent', + } = options; + + const source = j(file.source); + + // Check if the element is on the page + const elements = source.findJSXElements(componentName); + + // If there are no elements then return the original file + if (elements.length === 0) return file.source; + + elements.forEach(element => { + replaceJSXAttributes({ j, element, propName, newPropName }); + }); + + return source.toSource(); +} diff --git a/tools/codemods/src/codemods/update-component-prop-value/tests/transform.spec.ts b/tools/codemods/src/codemods/update-component-prop-value/tests/transform.spec.ts new file mode 100644 index 0000000000..6b74f39a11 --- /dev/null +++ b/tools/codemods/src/codemods/update-component-prop-value/tests/transform.spec.ts @@ -0,0 +1,6 @@ +import { transformTest } from '../../../utils/tests/transformTest'; + +transformTest(__dirname, { + fixture: 'update-component-prop-value', + transform: 'update-component-prop-value', +}); diff --git a/tools/codemods/src/codemods/update-component-prop-value/tests/update-component-prop-value.input.tsx b/tools/codemods/src/codemods/update-component-prop-value/tests/update-component-prop-value.input.tsx new file mode 100644 index 0000000000..7f8a8928c2 --- /dev/null +++ b/tools/codemods/src/codemods/update-component-prop-value/tests/update-component-prop-value.input.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/codemods/update-component-prop-value/tests/update-component-prop-value.output.tsx b/tools/codemods/src/codemods/update-component-prop-value/tests/update-component-prop-value.output.tsx new file mode 100644 index 0000000000..f23bd7c539 --- /dev/null +++ b/tools/codemods/src/codemods/update-component-prop-value/tests/update-component-prop-value.output.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/codemods/update-component-prop-value/transform.ts b/tools/codemods/src/codemods/update-component-prop-value/transform.ts new file mode 100644 index 0000000000..622f43b523 --- /dev/null +++ b/tools/codemods/src/codemods/update-component-prop-value/transform.ts @@ -0,0 +1,48 @@ +import type { API, FileInfo } from 'jscodeshift'; + +import { + replaceJSXAttributes, + ReplaceJSXAttributesType, +} from '../../utils/transformations'; + +type TransformerOptions = ReplaceJSXAttributesType & { componentName: string }; + +/** + * Example transformer function to update a component prop value + * + * @param file the file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @param options an object containing options to pass to the tranform function + * @returns Either the modified file or the original file + */ +export default function transformer( + file: FileInfo, + { jscodeshift: j }: API, + options: TransformerOptions, +) { + const { + propName = 'prop', + newPropValue = 'new prop value', + componentName = 'MyComponent', + } = options; + + const source = j(file.source); + + // Check if the element is on the page + const elements = source.findJSXElements(componentName); + + // If there are no elements then return the original file + if (elements.length === 0) return file.source; + + elements.forEach(element => { + replaceJSXAttributes({ + j, + element, + propName, + newPropName: propName, + newPropValue, + }); + }); + + return source.toSource(); +} diff --git a/tools/codemods/src/constants.ts b/tools/codemods/src/constants.ts new file mode 100644 index 0000000000..c6c0f20943 --- /dev/null +++ b/tools/codemods/src/constants.ts @@ -0,0 +1,3 @@ +export const MIGRATOR_ERROR = { + manual: 'Please update manually', +}; diff --git a/tools/codemods/src/index.ts b/tools/codemods/src/index.ts new file mode 100644 index 0000000000..ab1ab42d76 --- /dev/null +++ b/tools/codemods/src/index.ts @@ -0,0 +1,84 @@ +/* eslint-disable no-console */ +import chalk from 'chalk'; +import fse from 'fs-extra'; +import { glob } from 'glob'; +import * as jscodeshift from 'jscodeshift/src/Runner'; +import path from 'path'; + +import { checkGitStatus } from './utils/checkGitStatus'; + +export interface MigrateOptions { + dry?: boolean; + print?: boolean; + force?: boolean; + ignore?: Array; +} + +export const migrator = async ( + codemod: string, + files?: string | Array, + options: MigrateOptions = {}, +) => { + let _files = files; + // Gets the path of the codemod e.g: /Users/.../leafygreen-ui/tools/codemods/dist/codemod/[codemod]/transform.js + const codemodFile = path.join( + __dirname, + `./codemods/${codemod}/transform.js`, + ); + + console.log(chalk.greenBright('Codemod File:'), codemodFile); + + try { + if (!fse.existsSync(codemodFile)) { + throw new Error( + `No codemod found for ${codemod}. The list of codemods can be found here: https://github.com/mongodb/leafygreen-ui/blob/main/tools/codemods/README.md#codemods-1`, + ); + } + + if (!_files) { + console.log( + chalk.yellow( + `No path provided. The current working directory, ${process.cwd()}, will be used`, + ), + ); + _files = process.cwd(); + } + + if (!options.dry) { + // Checks if the Git directory is in a "clean" state -- there are no uncommited changes or untracked files in the repo + checkGitStatus(options.force); + } + + const filepaths = glob.sync(_files, { cwd: process.cwd() }); + + if (filepaths.length === 0) { + throw new Error(`No files found for ${files}`); + } + + console.log(chalk.greenBright('filepaths:'), filepaths); + console.log(chalk.greenBright('Running codemod:'), codemod); + + const { ignore, ...allOptions } = options; + + await jscodeshift.run(codemodFile, filepaths, { + ignorePattern: [ + '**/node_modules/**', + '**/.next/**', + '**/build/**', + '**/dist/**', + ...(ignore ? ignore : []), + ], + extensions: 'tsx,ts,jsx,js', + parser: 'tsx', + verbose: 2, + ...allOptions, + }); + + console.log( + chalk.greenBright('🥬 Thank you for using @lg-tools/codemods!'), + ); + } catch (error) { + console.error(error); + process.exit(1); + } +}; diff --git a/tools/codemods/src/utils/checkGitStatus.ts b/tools/codemods/src/utils/checkGitStatus.ts new file mode 100644 index 0000000000..82c6217c71 --- /dev/null +++ b/tools/codemods/src/utils/checkGitStatus.ts @@ -0,0 +1,39 @@ +/* eslint-disable no-console */ +import chalk from 'chalk'; +import isGitClean from 'is-git-clean'; + +/** + * Check whether a Git directory is in a "clean" state, meaning there are no uncommitted changes or untracked files in the repository + * @param force boolean. Whether to forcibly continue even though the git directory is not clean + */ +export function checkGitStatus(force?: boolean) { + let clean = false; + let errorMessage = 'Unable to determine if git directory is clean'; + + try { + clean = isGitClean.sync(process.cwd()); + errorMessage = 'Git directory is not clean'; + } catch (err: any) { + if (err && err.stderr && err.stderr.indexOf('Not a git repository') >= 0) { + clean = true; + } + } + + if (!clean) { + if (force) { + console.log( + chalk.yellow(`WARNING: ${errorMessage}. Forcibly continuing.`), + ); + } else { + console.log( + chalk.greenBright('🥬 Thank you for using @lg-tools/codemods!'), + ); + console.log( + chalk.yellow( + '\nBut before we continue, please stash or commit your git changes. \nYou may use the --force flag to override this safety check.\n', + ), + ); + process.exit(1); + } + } +} diff --git a/tools/codemods/src/utils/index.ts b/tools/codemods/src/utils/index.ts new file mode 100644 index 0000000000..2615846eae --- /dev/null +++ b/tools/codemods/src/utils/index.ts @@ -0,0 +1,2 @@ +export { getJSXAttributes, insertJSXComment } from './jsx'; +export { transformTest } from './tests/transformTest'; diff --git a/tools/codemods/src/utils/jsx/getJSXAttributes.ts b/tools/codemods/src/utils/jsx/getJSXAttributes.ts new file mode 100644 index 0000000000..674e189141 --- /dev/null +++ b/tools/codemods/src/utils/jsx/getJSXAttributes.ts @@ -0,0 +1,23 @@ +import type { ASTPath } from 'jscodeshift'; +import type core from 'jscodeshift'; + +// https://astexplorer.net/#/gist/0735cadf00b74e764defef5f75c6d044/219778bcf6c39ff47dbe4483c4ae619fb8caab45 +/** + * Loops through each prop on the element and returns the specified prop + * + * @param j A reference to the jscodeshift library + * @param element The element to search for a specific prop + * @param propName Prop name to find on the element + * @returns Returns a collection with the node-path of the specified attribute(prop) + */ +export function getJSXAttributes( + j: core.JSCodeshift, + element: ASTPath, + propName: string, +) { + // Targeting the openingElement directly targets only the parent element rather than all of its children elements e.g j(element). We do this to avoid selecting children that might have props names identical to the parent element. + const elementCollection = j(element.value.openingElement); + return elementCollection.find(j.JSXAttribute).filter(attribute => { + return attribute.value.name.name === propName; + }); +} diff --git a/tools/codemods/src/utils/jsx/index.ts b/tools/codemods/src/utils/jsx/index.ts new file mode 100644 index 0000000000..6bb659daca --- /dev/null +++ b/tools/codemods/src/utils/jsx/index.ts @@ -0,0 +1,2 @@ +export { getJSXAttributes } from './getJSXAttributes'; +export { insertJSXComment } from './insertJSXComment'; diff --git a/tools/codemods/src/utils/jsx/insertJSXComment/index.ts b/tools/codemods/src/utils/jsx/insertJSXComment/index.ts new file mode 100644 index 0000000000..39a24c7fdb --- /dev/null +++ b/tools/codemods/src/utils/jsx/insertJSXComment/index.ts @@ -0,0 +1 @@ +export { insertJSXComment } from './insertJSXComment'; diff --git a/tools/codemods/src/utils/jsx/insertJSXComment/insertJSXComment.ts b/tools/codemods/src/utils/jsx/insertJSXComment/insertJSXComment.ts new file mode 100644 index 0000000000..1afeed6bfe --- /dev/null +++ b/tools/codemods/src/utils/jsx/insertJSXComment/insertJSXComment.ts @@ -0,0 +1,64 @@ +// Credit to [Polaris](https://github.com/Shopify/polaris/blob/995079cc7c5c5087d662609c75c11eea58920f6d/polaris-migrator/src/utilities/jsx.ts#L189) + +import type { ASTPath, Comment } from 'jscodeshift'; +import type core from 'jscodeshift'; + +/** + * Util that inserts comments before or after a line of code + * + * @param j a reference to the jscodeshift library + * @param element The element(component) to transform + * @param comment The comment to add + * @param position The position of the comment. + * */ +export function insertJSXComment( + j: core.JSCodeshift, + element: ASTPath, + comment: string, + position: 'before' | 'after' = 'before', +) { + // https://github.com/facebook/jscodeshift/issues/354 + const commentContent = j.jsxEmptyExpression(); + const commentConcat = ` ${comment} `; + commentContent.comments = [j.commentBlock(commentConcat, false, true)]; + const jsxComment = j.jsxExpressionContainer(commentContent); + const lineBreak = j.jsxText('\n'); + + if (position === 'before') { + // If the component is the first direct child after a return statement, this means it is not nested in another component so comments should look like /* comment */, without the brackets + if (element.parentPath.value.type === 'ReturnStatement') { + insertCommentBefore(j, element, commentConcat); + } else { + // The element is nested inside another component so comments should look like {/* comment */}, with the brackets + element.insertBefore(jsxComment); + element.insertBefore(lineBreak); + } + } + + if (position === 'after') { + element.insertAfter(lineBreak); + element.insertAfter(jsxComment); + } +} + +/** + * Util that inserts a comment into an array of comments. + * + * Components that are not nested inside another component can have an array of comments. + */ +export function insertCommentBefore( + j: core.JSCodeshift, + path: ASTPath, + commentString: string, +) { + path.value.comments = path.value.comments || []; + + const duplicateComment = path.value.comments.find( + (comment: Comment) => comment.value === commentString, + ); + + // Avoiding duplicates of the same comment + if (duplicateComment) return; + + path.value.comments.push(j.commentBlock(commentString)); +} diff --git a/tools/codemods/src/utils/jsx/insertJSXComment/tests/insert-jsx-comment.input.tsx b/tools/codemods/src/utils/jsx/insertJSXComment/tests/insert-jsx-comment.input.tsx new file mode 100644 index 0000000000..c3c1ebf321 --- /dev/null +++ b/tools/codemods/src/utils/jsx/insertJSXComment/tests/insert-jsx-comment.input.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + // Comment + const TestOne = () => { + return ; + }; + + const TestTwo = () => { + return ( + <> + + + ); + }; + + const TestThree = () => { + // comment + return ; + }; + + const TestFour = () => { + return ( + // comment + + ); + }; + + const TestFive = () => { + return ( + /* testing comment */ + + ); + }; + + const TestSix = () => { + /* testing comment */ + return ; + }; + + const TestSeven = () => { + /* testing comment */ + return ; // comment + }; + + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/tools/codemods/src/utils/jsx/insertJSXComment/tests/insert-jsx-comment.output.tsx b/tools/codemods/src/utils/jsx/insertJSXComment/tests/insert-jsx-comment.output.tsx new file mode 100644 index 0000000000..0fd455397b --- /dev/null +++ b/tools/codemods/src/utils/jsx/insertJSXComment/tests/insert-jsx-comment.output.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + // Comment + const TestOne = () => { + return ( + /* testing comment */ + + ); + }; + + const TestTwo = () => { + return ( + <> + {/* testing comment */} + + + ); + }; + + const TestThree = () => { + // comment + return ( + /* testing comment */ + + ); + }; + + const TestFour = () => { + return ( + // comment + /* testing comment */ + + ); + }; + + const TestFive = () => { + return ( + /* testing comment */ + + ); + }; + + const TestSix = () => { + /* testing comment */ + return ( + /* testing comment */ + + ); + }; + + const TestSeven = () => { + /* testing comment */ + return ( + /* testing comment */ + + ); // comment + }; + + return ( + <> + {/* testing comment */} + + {/* testing comment */} + + + {/* testing comment */} + + {/* testing comment */} + + + {/* testing comment */} + + {/* testing comment */} + + + + + + + + + + ); +}; diff --git a/tools/codemods/src/utils/jsx/insertJSXComment/tests/transform.spec.ts b/tools/codemods/src/utils/jsx/insertJSXComment/tests/transform.spec.ts new file mode 100644 index 0000000000..9b0408bed8 --- /dev/null +++ b/tools/codemods/src/utils/jsx/insertJSXComment/tests/transform.spec.ts @@ -0,0 +1,6 @@ +import { transformTest } from '../../../tests/transformTest'; + +transformTest(__dirname, { + fixture: 'insert-jsx-comment', + transform: 'insert-jsx-comment', +}); diff --git a/tools/codemods/src/utils/jsx/insertJSXComment/transform.ts b/tools/codemods/src/utils/jsx/insertJSXComment/transform.ts new file mode 100644 index 0000000000..a4ade3996c --- /dev/null +++ b/tools/codemods/src/utils/jsx/insertJSXComment/transform.ts @@ -0,0 +1,26 @@ +import type { API, FileInfo } from 'jscodeshift'; + +import { getJSXAttributes } from '../getJSXAttributes'; + +import { insertJSXComment } from './insertJSXComment'; + +/** + * Example transformer function to add comments above or below a line of code. This function is only used for testing purposes. + * + * @param file The file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @returns The modified code + */ +export default function transformer(file: FileInfo, { jscodeshift: j }: API) { + const source = j(file.source); + + source.findJSXElements('MyComponent').forEach(element => { + return getJSXAttributes(j, element, 'prop').forEach(el => { + // @ts-expect-error value does exist + const position = el.value.value?.value; + insertJSXComment(j, element, 'testing comment', position); + }); + }); + + return source.toSource(); +} diff --git a/tools/codemods/src/utils/tests/transformTest.ts b/tools/codemods/src/utils/tests/transformTest.ts new file mode 100644 index 0000000000..737b90f42d --- /dev/null +++ b/tools/codemods/src/utils/tests/transformTest.ts @@ -0,0 +1,108 @@ +// Credit to [Polaris](https://github.com/Shopify/polaris/blob/995079cc7c5c5087d662609c75c11eea58920f6d/polaris-migrator/src/utilities/check.ts) + +/* eslint-disable jest/no-export */ +import fs from 'fs'; +import jscodeshift, { type FileInfo } from 'jscodeshift'; +import path from 'path'; +// @ts-expect-error - no prettier types +import prettier from 'prettier'; + +interface ParserExtensionMap { + [key: string]: prettier.BuiltInParserName; +} + +const parserExtensionMap: ParserExtensionMap = { + tsx: 'typescript', +}; + +interface TestArgs { + /** + * The file name of the test. This name will be used to get the input and output file for the test. + */ + fixture: string; + + /** + * The name of the transformation to test + */ + transform: string; + + /** + * The extension of the file which is used as a parser for prettier. + * + * @default 'tsx' + */ + extension?: string; + + /** + * Options to pass to the transformer function + * + * @default {} + */ + options?: { [option: string]: any }; +} + +/** + * Test util that runs a file through jscodeshift and returns the modified code. + * + * @param tranform an import of the transform file e.g. transform.ts + * @param input the file to run the transformation against + * @param options options to pass to the transform function + */ +async function applyTransform( + transform: any, + input: FileInfo, + options?: { [option: string]: any }, +) { + // This get the default export from transform.ts + // Handle ES6 modules using default export for the transform + const transformer = transform.default ? transform.default : transform; + const output = await transformer( + input, + { + jscodeshift: jscodeshift.withParser('tsx'), + }, + options || {}, + ); + + return (output || '').trim(); +} + +/** + * Test util to test migrations in Jest. + * The input file within the fixture undergoes the appropriate transformation. The results are then compared to the output file. + * + * @param dirName the current directory + * @param options an object containing at least the fixture(test name) and transformation name + */ +export function transformTest( + dirName: string, + { fixture, transform, extension = 'tsx', options = {} }: TestArgs, +) { + describe(transform, () => { + test(fixture, async () => { + const fixtureDir = path.join(dirName); + const inputPath = path.join(fixtureDir, `${fixture}.input.${extension}`); + const parser = parserExtensionMap[extension]; + const source = fs.readFileSync(inputPath, 'utf8'); + const expected = fs.readFileSync( + path.join(fixtureDir, `${fixture}.output.${extension}`), + 'utf8', + ); + + // How many levels to go up from the test directory to find transform.ts + const levelsUp = '..'; + const module = await import(path.join(dirName, levelsUp, 'transform.ts')); + const output = await applyTransform( + { ...module }, + { source, path: inputPath }, + options, + ); + + const formattedOutput = prettier.format(output, { parser }); + const formattedExpected = prettier.format(expected, { parser }); + + // Format output and expected with prettier for white spaces and line breaks consistency + expect(formattedOutput).toBe(formattedExpected); + }); + }); +} diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/consolidateJSXAttributes.ts b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/consolidateJSXAttributes.ts new file mode 100644 index 0000000000..a53c59c4c5 --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/consolidateJSXAttributes.ts @@ -0,0 +1,193 @@ +import type { ASTNode, ASTPath, JSXAttribute, Options } from 'jscodeshift'; +import type core from 'jscodeshift'; + +import { MIGRATOR_ERROR } from '../../../constants'; +import { insertJSXComment } from '../../jsx/insertJSXComment/insertJSXComment'; + +export interface ConsolidateJSXAttributesOptions extends Options { + /** + * A reference to the jscodeshift library + */ + j: core.JSCodeshift; + + /** + * The element(component) to transform + */ + element: ASTPath; + + /** + * The prop to remove on the element + */ + propToRemove: string; + + /** + * The prop to update on the element + */ + propToUpdate: string; + + /** + * A map of values that will be used to update the value of `propToUpdate` + */ + propMapping: { [value: string]: string }; + + /** + * Whether the `propToRemove` is a string or boolean + */ + propToRemoveType?: 'string' | 'boolean'; +} + +// https://astexplorer.net/#/gist/9aa98b850fc7004100e1c13915fd147b/25313acbbef360bf7ca503db3bd9cb2d3d335ce3 +/** + * `consolidateJSXAttributes` takes in two attributes(props) to consolidate, a `propToRemove` and a `propToUpdate`. The `propToRemove` is removed and the `propToUpdate` is updated based on the value provided in `propMapping`. + * + * e.g: + * ```tsx + * propToRemove: disabled + * propToUpdate: state + * propMapping: {'true': 'disabled'} + * + * Before: + * + * After: + * + * ----------------------------------- + * Before: + * + * After: + * + * ----------------------------------- + * Before: + * + * After: + * + * ----------------------------------- + * Before: + * + * After: + * + * ``` + * + */ +export function consolidateJSXAttributes({ + j, + element, + propToRemove, + propToUpdate, + propMapping, + propToRemoveType, +}: ConsolidateJSXAttributesOptions) { + const isPropToRemoveABoolean = propToRemoveType === 'boolean'; + + // gets all the props on the elements opening tag + const allAttributes = element.node.openingElement.attributes; + + // checks if the there is a spread operator + const hasSpreadOperator = allAttributes.some( + (attribute: ASTNode) => attribute.type !== 'JSXAttribute', + ); + + // removes the spread operator from the rest of the attributes + const allAttributesWithoutSpread = allAttributes.filter( + (attribute: ASTNode) => attribute.type === 'JSXAttribute', + ); + + // Finds the propToRemove from the list of attributes + const _propToRemove: ASTPath = allAttributesWithoutSpread.find( + (attribute: ASTPath) => attribute.name.name === propToRemove, + ); + + // Finds the propToUpdate from the list of attributes + const _propToUpdate: ASTPath = allAttributesWithoutSpread.find( + (attribute: ASTPath) => attribute.name.name === propToUpdate, + ); + + // If the propToRemove does not exist then return the source without any changes + if (!_propToRemove) return; + + // finds the index of the propToRemove so that we can use it to remove it from the array of attributes + const attributeToRemoveIndex = allAttributes.indexOf(_propToRemove); + + const removePropToRemove = () => + allAttributes.splice(attributeToRemoveIndex, 1); + + // find the new value that propToUpdate should be updated with + const newValueMapping = + propMapping[getPropToRemoveValue(isPropToRemoveABoolean, _propToRemove)]; + + // if fromProp value is not in the mapping then remove that item from the atributes and return + if (!newValueMapping) { + removePropToRemove(); + return; + } + + // if the propToUpdate does not exist and there is a spread operator then return early since we don't know if the propToUpdate could be inside the spread + if (!_propToUpdate && hasSpreadOperator) { + insertJSXComment(j, element, MIGRATOR_ERROR.manual); + return; + } + + // if the propToUpdate does not exist then we update the propToRemove + if (!_propToUpdate) { + // remove the propToRemove + removePropToRemove(); + + // Creates a new stringLiteral node + const attributeValueNode = j.stringLiteral(newValueMapping); + // Create a new jsxAttribute node with the attribute name and value + const newAttribute = j.jsxAttribute( + j.jsxIdentifier(propToUpdate), + attributeValueNode, + ); + // Add the new attribute to the opening element + element.node.openingElement.attributes.push(newAttribute); + return; + } + + // If the propToUpdate does exist then update the value + if (_propToUpdate) { + j(_propToUpdate) + .find(j.StringLiteral) + .forEach(literal => { + literal.node.value = newValueMapping; + removePropToRemove(); + return; + }); + } +} + +/** + * This function checks whether the propToRemove is a string or a boolean and returns that value as a string. + * + * @param isPropToRemoveABoolean + * @returns string + */ +const getPropToRemoveValue = ( + isPropToRemoveABoolean: boolean, + propToRemove: ASTPath, +) => { + let propToRemoveValue; + const expressionType: string = propToRemove.value?.type; + const isString = expressionType === 'StringLiteral'; + const isBoolean = + expressionType === 'JSXExpressionContainer' || + propToRemove.value === null || + propToRemove.value === undefined; + + if (isPropToRemoveABoolean) { + if (isBoolean) { + propToRemoveValue = + propToRemove.value === null + ? true + : // @ts-expect-error: unsure why it says expression does not exist on type 'JSXAttribute'. + propToRemove?.value?.expression.value; + return propToRemoveValue.toString(); + } + } + + if (isString) { + propToRemoveValue = propToRemove.value?.value; + return propToRemoveValue; + } + + return ''; +}; diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/index.ts b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/index.ts new file mode 100644 index 0000000000..11499c5115 --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/index.ts @@ -0,0 +1,4 @@ +export { + consolidateJSXAttributes, + type ConsolidateJSXAttributesOptions, +} from './consolidateJSXAttributes'; diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-boolean.input.tsx b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-boolean.input.tsx new file mode 100644 index 0000000000..af5742a12c --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-boolean.input.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + const props = { + randomProp: 'value', + }; + + return ( + <> + {/* disabled=false */} + + + + + + + Hello + + + + {/* disabled=true */} + + + + + Hello + + + + + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-boolean.output.tsx b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-boolean.output.tsx new file mode 100644 index 0000000000..3969dd44f6 --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-boolean.output.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + const props = { + randomProp: 'value', + }; + + return ( + <> + {/* disabled=false */} + + + + + + + Hello + + + + {/* disabled=true */} + + + + + Hello + + + + {/* Please update manually */} + + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-string.input.tsx b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-string.input.tsx new file mode 100644 index 0000000000..f4f142b9f0 --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-string.input.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + const props = { + randomProp: 'value', + }; + + const Test = () => { + return ; + }; + + const TestTwo = () => { + return ( + <> + + + ); + }; + + return ( + <> + + + Hello + + + + + + + + + + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-string.output.tsx b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-string.output.tsx new file mode 100644 index 0000000000..5b3b10dd72 --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/consolidate-jsx-attributes-string.output.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + const props = { + randomProp: 'value', + }; + + const Test = () => { + return ( + /* Please update manually */ + + ); + }; + + const TestTwo = () => { + return ( + <> + {/* Please update manually */} + + + ); + }; + + return ( + <> + + + Hello + + + + + + {/* Please update manually */} + + + + + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/transform.spec.ts b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/transform.spec.ts new file mode 100644 index 0000000000..b51df6ddbb --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/tests/transform.spec.ts @@ -0,0 +1,38 @@ +import { transformTest } from '../../../tests/transformTest'; + +const transform = 'consolidate-jsx-attributes'; + +const tests = [ + { + name: 'consolidate-jsx-attributes-boolean', + options: { + componentName: 'MyComponent', + propToRemove: 'disabled', + propToUpdate: 'state', + propMapping: { + true: 'disabled', + }, + propToRemoveType: 'boolean', + }, + }, + { + name: 'consolidate-jsx-attributes-string', + options: { + componentName: 'MyComponent', + propToRemove: 'propToRemove', + propToUpdate: 'propToUpdate', + propMapping: { + value2: 'value3', + }, + propToRemoveType: 'string', + }, + }, +]; + +for (const test of tests) { + transformTest(__dirname, { + fixture: test.name, + transform, + options: test.options, + }); +} diff --git a/tools/codemods/src/utils/transformations/consolidateJSXAttributes/transform.ts b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/transform.ts new file mode 100644 index 0000000000..71b22e1496 --- /dev/null +++ b/tools/codemods/src/utils/transformations/consolidateJSXAttributes/transform.ts @@ -0,0 +1,53 @@ +import type { API, FileInfo } from 'jscodeshift'; + +import { + consolidateJSXAttributes, + ConsolidateJSXAttributesOptions, +} from '../consolidateJSXAttributes'; + +type TransformerOptions = ConsolidateJSXAttributesOptions & { + componentName: string; +}; + +/** + * Example transformer function to consolidate props + * + * @param file the file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @param options an object containing options to pass to the transform function + * @returns Either the modified file or the original file + */ +export default function transformer( + file: FileInfo, + { jscodeshift: j }: API, + options: TransformerOptions, +) { + const source = j(file.source); + + const { + propToRemove, + propToUpdate, + propMapping, + propToRemoveType, + componentName, + } = options; + + // Check if the element is on the page + const elements = source.findJSXElements(componentName); + + // If there are no elements then return the original file + if (elements.length === 0) return file.source; + + elements.forEach(element => { + consolidateJSXAttributes({ + j, + element, + propToRemove, + propToUpdate, + propMapping, + propToRemoveType, + }); + }); + + return source.toSource(); +} diff --git a/tools/codemods/src/utils/transformations/index.ts b/tools/codemods/src/utils/transformations/index.ts new file mode 100644 index 0000000000..4061074a50 --- /dev/null +++ b/tools/codemods/src/utils/transformations/index.ts @@ -0,0 +1,8 @@ +export { + consolidateJSXAttributes, + type ConsolidateJSXAttributesOptions, +} from './consolidateJSXAttributes/consolidateJSXAttributes'; +export { + replaceJSXAttributes, + type ReplaceJSXAttributesType, +} from './replaceJSXAttributes'; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/index.ts b/tools/codemods/src/utils/transformations/replaceJSXAttributes/index.ts new file mode 100644 index 0000000000..7b4ae8d79c --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/index.ts @@ -0,0 +1,4 @@ +export { + replaceJSXAttributes, + ReplaceJSXAttributesType, +} from './replaceJSXAttributes'; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/replaceJSXAttributes.ts b/tools/codemods/src/utils/transformations/replaceJSXAttributes/replaceJSXAttributes.ts new file mode 100644 index 0000000000..763a7490b5 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/replaceJSXAttributes.ts @@ -0,0 +1,93 @@ +import type { ASTPath } from 'jscodeshift'; +import type core from 'jscodeshift'; + +import { getJSXAttributes } from '../../jsx'; + +export interface ReplaceJSXAttributesType { + /** + * A reference to the jscodeshift library + */ + j: core.JSCodeshift; + + /** + * The element(component) to transform + */ + element: ASTPath; + + /** + * The name of the prop that will be replaced on the element + */ + propName: string; + + /** + * The new name of the prop + */ + newPropName: string; + + /** + * The new value of the prop. This can either be a string or a map of values. + */ + newPropValue?: + | string + | { + [key: string]: string; + }; +} + +/** + * `replaceJSXAttributes` can replace both the name and value of an attribute(prop). + * + * e.g: + * ```tsx + * propName: prop + * newPropName: newProp + * + * Before: + * + * After: + * + * ----------------------------------- + * propName: prop + * newPropName: prop + * newPropValue: {hey: 'hey new', bye: 'bye new`} + * + * Before: + * + * After: + * + * ``` + */ +export function replaceJSXAttributes({ + j, + element, + propName, + newPropName, + newPropValue, +}: ReplaceJSXAttributesType) { + // returns a Collection(Array) of NodePaths that we loop through. + // Each attribute is a NodePath + return getJSXAttributes(j, element, propName).forEach(attribute => { + attribute.node.name.name = newPropName; + + if (!newPropValue) { + return; + } + + j(attribute) + .find(j.StringLiteral) + .forEach(literal => { + const isStringLiteral = typeof newPropValue === 'string'; + + if (isStringLiteral) { + literal.node.value = newPropValue; + return; + } + + const currentPropValue = literal.node.value; + + if (currentPropValue in newPropValue) { + literal.node.value = newPropValue[currentPropValue]; + } + }); + }); +} diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/rename-component-prop.input.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/rename-component-prop.input.tsx new file mode 100644 index 0000000000..7f8a8928c2 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/rename-component-prop.input.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/rename-component-prop.output.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/rename-component-prop.output.tsx new file mode 100644 index 0000000000..f16bdc105a --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/rename-component-prop.output.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/transform.spec.ts b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/transform.spec.ts new file mode 100644 index 0000000000..a4e061f9b6 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/transform.spec.ts @@ -0,0 +1,54 @@ +import { transformTest } from '../../../tests/transformTest'; + +const transform = 'replace-jsx-attributes'; + +const tests = [ + { + name: 'rename-component-prop', + options: { + componentName: 'MyComponent', + propName: 'prop', + newPropName: 'newProp', + }, + }, + { + name: 'update-component-prop-value', + options: { + componentName: 'MyComponent', + propName: 'prop', + newPropName: 'prop', + newPropValue: 'new prop value', + }, + }, + { + name: 'update-component-prop-value-multiple', + options: { + componentName: 'MyComponent', + propName: 'prop', + newPropName: 'prop', + newPropValue: "new prop y'all", + }, + }, + { + name: 'update-component-prop-value-object', + options: { + componentName: 'MyComponent', + propName: 'prop', + newPropName: 'prop', + newPropValue: { + value1: 'value1Mapped', + value2: 'value2Mapped', + value3: 'value3Mapped', + value4: 'value4Mapped', + }, + }, + }, +]; + +for (const test of tests) { + transformTest(__dirname, { + fixture: test.name, + transform, + options: test.options, + }); +} diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-multiple.input.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-multiple.input.tsx new file mode 100644 index 0000000000..adb76e570d --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-multiple.input.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + <> + + Hello + + + + Hello + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-multiple.output.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-multiple.output.tsx new file mode 100644 index 0000000000..5367fd096f --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-multiple.output.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + <> + + Hello + + + + Hello + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-object.input.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-object.input.tsx new file mode 100644 index 0000000000..342743f296 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-object.input.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + <> + + + + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-object.output.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-object.output.tsx new file mode 100644 index 0000000000..f1d6b5d273 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value-object.output.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + <> + + + + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value.input.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value.input.tsx new file mode 100644 index 0000000000..7f8a8928c2 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value.input.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value.output.tsx b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value.output.tsx new file mode 100644 index 0000000000..f23bd7c539 --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/tests/update-component-prop-value.output.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const MyComponent = (props: any) => { + return

Testing {props.children}

; +}; + +const Child = (props: any) => { + return

Testing {props.children}

; +}; + +export const App = () => { + return ( + + Hello + + + ); +}; diff --git a/tools/codemods/src/utils/transformations/replaceJSXAttributes/transform.ts b/tools/codemods/src/utils/transformations/replaceJSXAttributes/transform.ts new file mode 100644 index 0000000000..a1f76901ba --- /dev/null +++ b/tools/codemods/src/utils/transformations/replaceJSXAttributes/transform.ts @@ -0,0 +1,49 @@ +import type { API, FileInfo } from 'jscodeshift'; + +import { + replaceJSXAttributes, + ReplaceJSXAttributesType, +} from './replaceJSXAttributes'; + +type TransformerOptions = ReplaceJSXAttributesType & { componentName: string }; + +/** + * Example transformer function to update a component prop value + * + * @param file the file to transform + * @param jscodeshiftOptions an object containing at least a reference to the jscodeshift library + * @param options an object containing options to pass to the tranform function + * @returns Either the modified file or the original file + */ +export default function transformer( + file: FileInfo, + { jscodeshift: j }: API, + options: TransformerOptions, +) { + const { + propName, + newPropValue = undefined, + componentName, + newPropName, + } = options; + + const source = j(file.source); + + // Check if the element is on the page + const elements = source.findJSXElements(componentName); + + // If there are no elements then return the original file + if (elements.length === 0) return file.source; + + elements.forEach(element => { + replaceJSXAttributes({ + j, + element, + propName, + newPropName, + newPropValue, + }); + }); + + return source.toSource(); +} diff --git a/tools/codemods/transform.config.js b/tools/codemods/transform.config.js new file mode 100644 index 0000000000..0146873f1f --- /dev/null +++ b/tools/codemods/transform.config.js @@ -0,0 +1,12 @@ +//TODO: is this doing anything? +function resolve(transform) { + return require.resolve(`./dist/codemods/${transform}/transform`); +} + +module.exports = { + presets: { + 'consolidate-props': resolve('consolidate-props'), + 'rename-component-prop': resolve('rename-component-prop'), + 'update-component-prop-value': resolve('update-component-prop-value'), + }, +}; diff --git a/tools/codemods/tsconfig.json b/tools/codemods/tsconfig.json new file mode 100644 index 0000000000..05778289f1 --- /dev/null +++ b/tools/codemods/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "@lg-tools/build/config/package.tsconfig.json", + "compilerOptions": { + "declarationDir": "dist", + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"], + "@leafygreen-ui/*": ["../*/src"] + } + }, + "include": [ + "src/**/*" + ], + "exclude": ["**/*.spec.*", "**/*.story.*"], + "references": [ + ] +} diff --git a/yarn.lock b/yarn.lock index 510a609fa4..4e3ce82507 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3785,6 +3785,39 @@ "@leafygreen-ui/polymorphic" "^1.3.7" "@leafygreen-ui/tokens" "^2.5.2" +"@lg-tools/build@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@lg-tools/build/-/build-0.5.0.tgz#b24c35f5845fc368dac603cf178ab95535a9e51f" + integrity sha512-QmBTJU0lIH4f66WZqjoN8EMeukSXd4HVb9RX0M7Np0gtA7UqpD3LgFPfyVDYuiOzCcvIEz1THdq5+9oZEpPylw== + dependencies: + "@babel/core" "7.24.3" + "@babel/plugin-proposal-export-default-from" "7.24.1" + "@babel/preset-env" "7.24.3" + "@babel/preset-react" "7.24.1" + "@babel/preset-typescript" "7.24.1" + "@babel/register" "^7.23.7" + "@babel/runtime" "7.24.1" + "@emotion/babel-plugin" "11.11.0" + "@rollup/plugin-babel" "6.0.4" + "@rollup/plugin-node-resolve" "15.1.0" + "@rollup/plugin-terser" "0.4.3" + "@rollup/plugin-url" "8.0.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/cli" "8.0.1" + "@svgr/rollup" "^8.1.0" + "@types/cross-spawn" "6.0.2" + "@types/fs-extra" "11.0.1" + chalk "4.1.2" + cross-spawn "7.0.3" + fs-extra "11.1.1" + glob "10.3.12" + lodash "4.17.21" + react-docgen-typescript "2.2.2" + rollup "3.25.3" + rollup-plugin-node-externals "6.1.1" + rollup-plugin-polyfill-node "0.12.0" + rollup-plugin-sizes "1.0.6" + "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" @@ -4225,7 +4258,7 @@ "@babel/helper-module-imports" "^7.18.6" "@rollup/pluginutils" "^5.0.1" -"@rollup/plugin-inject@^5.0.4": +"@rollup/plugin-inject@^5.0.1", "@rollup/plugin-inject@^5.0.4": version "5.0.5" resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz#616f3a73fe075765f91c5bec90176608bed277a3" integrity sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg== @@ -5924,6 +5957,11 @@ dependencies: ci-info "^3.1.0" +"@types/is-git-clean@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/is-git-clean/-/is-git-clean-1.1.0.tgz#89329258c703c04d8b1077eec49dd34910f95778" + integrity sha512-5LLlN3nkhSkAQVJ1birvnykwg9Xljb/x/yHWP6zeHmswyn6bwdd2GRuW3Y87TNdIqJOqhm7BfPtKPKMqKcn4Sg== + "@types/is-stream@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" @@ -5974,6 +6012,14 @@ jest-matcher-utils "^28.0.0" pretty-format "^28.0.0" +"@types/jscodeshift@0.11.11": + version "0.11.11" + resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.11.11.tgz#30d7c986f372cd63c670017371da8fbced2b7acf" + integrity sha512-d7CAfFGOupj5qCDqMODXxNz2/NwCv/Lha78ZFbnr6qpk3K98iSB8I+ig9ERE2+EeYML352VMRsjPyOpeA+04eQ== + dependencies: + ast-types "^0.14.1" + recast "^0.20.3" + "@types/jsdom@^20.0.0": version "20.0.1" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" @@ -6814,6 +6860,11 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + integrity sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ== + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -6895,7 +6946,7 @@ arraybuffer.prototype.slice@^1.0.1: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" -arrify@^1.0.1: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== @@ -6931,6 +6982,13 @@ ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== +ast-types@0.14.2, ast-types@^0.14.1: + version "0.14.2" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" + integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== + dependencies: + tslib "^2.0.1" + ast-types@^0.16.1: version "0.16.1" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" @@ -7867,6 +7925,14 @@ cosmiconfig@^8.1.3, cosmiconfig@^8.2.0: parse-json "^5.0.0" path-type "^4.0.0" +cross-spawn-async@^2.1.1: + version "2.2.5" + resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" + integrity sha512-snteb3aVrxYYOX9e8BabYFK9WhCDhTlw1YQktfTthBogxri4/2r9U2nQc0ffY73ZAxezDc+U8gvHAeU1wy1ubQ== + dependencies: + lru-cache "^4.0.0" + which "^1.2.8" + cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -9012,6 +9078,18 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" + integrity sha512-QPexBaNjeOjyiZ47q0FCukTO1kX3F+HMM0EWpnxXddcr3MZtElILMkz9Y38nmSZtp03+ZiSRMffrKWBPOIoSIg== + dependencies: + cross-spawn-async "^2.1.1" + is-stream "^1.1.0" + npm-run-path "^1.0.0" + object-assign "^4.0.1" + path-key "^1.0.0" + strip-eof "^1.0.0" + execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -10255,6 +10333,15 @@ is-generator-function@^1.0.7: dependencies: has-tostringtag "^1.0.0" +is-git-clean@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-git-clean/-/is-git-clean-1.1.0.tgz#13abd6dda711bb08aafd42604da487845ddcf88d" + integrity sha512-1aodl49sbfsEV8GsIhw5lJdqObgQFLSUB2TSOXNYujCD322chTJPBIY+Q1NjXSM4V7rGh6vrWyKidIcGaVae6g== + dependencies: + execa "^0.4.0" + is-obj "^1.0.1" + multimatch "^2.1.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -10307,6 +10394,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" @@ -11144,7 +11236,7 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jscodeshift@^0.15.1: +jscodeshift@0.15.2, jscodeshift@^0.15.1: version "0.15.2" resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.15.2.tgz#145563860360b4819a558c75c545f39683e5a0be" integrity sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA== @@ -11454,7 +11546,7 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== -lru-cache@^4.0.1: +lru-cache@^4.0.0, lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -12146,7 +12238,7 @@ min-indent@^1.0.0, min-indent@^1.0.1: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -12270,6 +12362,16 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multimatch@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + integrity sha512-0mzK8ymiWdehTBiJh0vClAzGyQbdtyWqzSVx//EK4N/D+599RFlGfTAsKw2zMSABtDG9C6Ul2+t8f2Lbdjf5mA== + dependencies: + array-differ "^1.0.0" + array-union "^1.0.1" + arrify "^1.0.0" + minimatch "^3.0.0" + multimatch@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" @@ -12488,6 +12590,13 @@ npm-run-all@^4.1.5: shell-quote "^1.6.1" string.prototype.padend "^3.0.0" +npm-run-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" + integrity sha512-PrGAi1SLlqNvKN5uGBjIgnrTb8fl0Jz0a3JJmeMcGnIBh7UE9Gc4zsAMlwDajOMg2b1OgP6UPvoLUboTmMZPFA== + dependencies: + path-key "^1.0.0" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -12870,6 +12979,11 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-key@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" + integrity sha512-T3hWy7tyXlk3QvPFnT+o2tmXRzU4GkitkUWLp/WZ0S/FXd7XMx176tRurgTvHTNMJOQzTcesHNpBqetH86mQ9g== + path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -13650,6 +13764,16 @@ readdirp@^3.5.0, readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +recast@^0.20.3: + version "0.20.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.20.5.tgz#8e2c6c96827a1b339c634dd232957d230553ceae" + integrity sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ== + dependencies: + ast-types "0.14.2" + esprima "~4.0.0" + source-map "~0.6.1" + tslib "^2.0.1" + recast@^0.23.1: version "0.23.3" resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.3.tgz#f205d1f46b2c6f730de413ab18f96c166263d85f" @@ -13919,11 +14043,23 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" +rollup-plugin-node-externals@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-externals/-/rollup-plugin-node-externals-6.1.1.tgz#dff1a85073fe3c0b2c423b280259fe80392026a8" + integrity sha512-127OFMkpH5rBVlRHRBDUMk1m1sGuzbGy7so5aj/IkpUb2r3+wOWjR/erUzd2ChEQWPsxsyQG6xpYYvPBAdcBRA== + rollup-plugin-node-externals@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/rollup-plugin-node-externals/-/rollup-plugin-node-externals-7.1.1.tgz#2a61e362a842fe15406fff86823392d2f5c79599" integrity sha512-rnIUt0zYdV05muRetoxOiFiKxrrfiyXuM/CgI+akv6NExBTaIiPkH2rCSE9JUCsjta1MXzQVAldpe5tJEszlwQ== +rollup-plugin-polyfill-node@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.12.0.tgz#33d421ddb7fcb69c234461e508ca6d2db6193f1d" + integrity sha512-PWEVfDxLEKt8JX1nZ0NkUAgXpkZMTb85rO/Ru9AQ69wYW8VUCfDgP4CGRXXWYni5wDF0vIeR1UoF3Jmw/Lt3Ug== + dependencies: + "@rollup/plugin-inject" "^5.0.1" + rollup-plugin-polyfill-node@0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/rollup-plugin-polyfill-node/-/rollup-plugin-polyfill-node-0.13.0.tgz#28e5705b59438da894e55133a0fe7a86b57d9b0a" @@ -13939,6 +14075,13 @@ rollup-plugin-sizes@1.0.6: filesize "^9.0.0" module-details-from-path "^1.0.3" +rollup@3.25.3: + version "3.25.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.25.3.tgz#f9a8986f0f244bcfde2208da91ba46b8fd252551" + integrity sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw== + optionalDependencies: + fsevents "~2.3.2" + rollup@4.16.1: version "4.16.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.16.1.tgz#5a60230987fe95ebe68bab517297c116dbb1a88d" @@ -14488,6 +14631,11 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -15618,7 +15766,7 @@ which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.2, gopd "^1.0.1" has-tostringtag "^1.0.0" -which@^1.2.9: +which@^1.2.8, which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==