diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f48a5029de..b2a76d4203 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,3 +10,4 @@ /packages/web @literat @adamkudrna @crishpeen @pavelklibani @lmc-eu/spirit-design-system /packages/web-react @literat @pavelklibani @lmc-eu/spirit-design-system /packages/web-twig @literat @adamkudrna @crishpeen @pavelklibani @lmc-eu/spirit-design-system +/packages/codemods @literat @adamkudrna @crishpeen @pavelklibani @lmc-eu/spirit-design-system diff --git a/packages/codemods/.eslintignore b/packages/codemods/.eslintignore index cc36ce8bf9..f8bf6e6817 100644 --- a/packages/codemods/.eslintignore +++ b/packages/codemods/.eslintignore @@ -16,3 +16,8 @@ build # Some tools use this pattern for their configuration files. Lint them! !*.config.js + +# Folder __testfixtures__ is used by jscodeshift and cannot be renamed +# There are just test fixtures, no need to lint them +# https://github.com/facebook/jscodeshift?tab=readme-ov-file#unit-testing +**/__testfixtures__/** \ No newline at end of file diff --git a/packages/codemods/.gitignore b/packages/codemods/.gitignore index 1492cd3aa6..71101ec15f 100644 --- a/packages/codemods/.gitignore +++ b/packages/codemods/.gitignore @@ -3,6 +3,7 @@ node_modules # Caches .cache +.DS_Store # Log files *.log diff --git a/packages/codemods/README.md b/packages/codemods/README.md index dcb9ee5d1f..9437f7da7c 100644 --- a/packages/codemods/README.md +++ b/packages/codemods/README.md @@ -1,5 +1,42 @@ # @lmc-eu/spirit-codemods +> Codemods for migration to the newer version of the Spirit Design library. + +`spirit-codemods` is a **CLI tool** designed to assist you in migrating to the latest version of our Spirit Design System library. This tool efficiently handles the removal of breaking changes and deprecations with simple commands. + +For React transformations, it utilizes the [jscodeshift][jscodeshift] library. + ## Install +No installation of this package is necessary; you can run it using `npx`. + ## Usage + +To view the available arguments for this package, use `-h` or `--help` as shown in the example below: + +```shell +npx @lmc-eu/spirit-codemods -h +``` + +There are **two mandatory arguments**: `-p`/`--path` and `-t`/`--transformation`. +The former specifies the directory path where you want to execute transforms, while the latter specifies the desired codemod to run. + +```shell +npx @lmc-eu/spirit-codemods -p ./ -t v2/web-react/codemod-name +``` + +Other optional arguments include: + +- `-v`/`--version` - Displays current version +- `-h`/`--help` - Displays this message +- `-e`/`--extensions` - Extensions of the transformed files, default: `ts,tsx,js,jsx` +- `--parser` - Parser to use (babel, ts, tsx, flow), default: `tsx` +- `--ignore` - Ignore files or directories, default: `**/node_modules/**` + +For example, this could be the command you will run: + +```shell +npx @lmc-eu/spirit-codemods -p ./src -t v2/web-react/button-text -e js,jsx --parser babel +``` + +[jscodeshift]: https://github.com/facebook/jscodeshift diff --git a/packages/codemods/config/jest.config.js b/packages/codemods/config/jest.config.js index 0efd738d31..8db81784df 100644 --- a/packages/codemods/config/jest.config.js +++ b/packages/codemods/config/jest.config.js @@ -31,6 +31,8 @@ const config = { // An array of regexp pattern strings that are matched against all file paths before executing the test. // https://jestjs.io/docs/configuration#coveragepathignorepatterns-arraystring + // Folder __testfixtures__ is used by jscodeshift and cannot be renamed + // https://github.com/facebook/jscodeshift?tab=readme-ov-file#unit-testing coveragePathIgnorePatterns: ['__fixtures__', '__testfixtures__', 'bin'], // A list of reporter names that Jest uses when writing coverage reports. Any istanbul reporter can be used. @@ -43,7 +45,7 @@ const config = { // An array of regexp pattern strings that are matched against all module paths before those paths are 'visible' to the loader. // https://jestjs.io/docs/configuration#modulepathignorepatterns-arraystring - modulePathIgnorePatterns: ['/dist/'], + modulePathIgnorePatterns: ['/dist/', '/*/__testfixtures__/'], }; export default config; diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 968a7bfbaa..2c5eaaf912 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -32,7 +32,7 @@ "scripts": { "prebuild": "shx rm -rf dist && shx mkdir -p dist", "build": "tsup --config ./config/tsup.config.ts", - "postbuild": "shx cp -r package.json README.md src/bin dist/", + "postbuild": "shx cp -r package.json README.md src/bin dist/ && node scripts/copyTransforms.js", "start": "tsup src/index.ts --watch", "types": "tsc", "lint": "eslint ./", @@ -43,7 +43,12 @@ "test:unit:coverage": "yarn test:unit --coverage" }, "dependencies": { - "jscodeshift": "^0.15.1" + "@types/jscodeshift": "^0.11.11", + "execa": "^8.0.1", + "filedirname": "^3.0.0", + "jscodeshift": "^0.15.1", + "sade": "^1.8.1", + "zx": "^7.2.2" }, "devDependencies": { "@lmc-eu/eslint-config-jest": "3.0.2", diff --git a/packages/codemods/scripts/copyTransforms.js b/packages/codemods/scripts/copyTransforms.js new file mode 100644 index 0000000000..9223556afe --- /dev/null +++ b/packages/codemods/scripts/copyTransforms.js @@ -0,0 +1,42 @@ +import fs from 'fs'; +import path from 'path'; + +const sourcePath = './src/transforms'; +const destinationPath = './dist/transforms'; +const excludedDirectories = ['__tests__', '__testfixtures__']; + +function copyFolderRecursive(src, dest) { + // Check if the source path exists + if (!fs.existsSync(src)) { + throw new Error(`Source path doesn't exist: ${src}`); + } + + // Create destination folder if it doesn't exist + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest); + } + + // Get all files and subdirectories in the source path + const items = fs.readdirSync(src); + + // Copy each item to the destination path + items.forEach((item) => { + const srcPath = path.join(src, item); + const destPath = path.join(dest, item); + + // Check if the item is a directory + if (fs.statSync(srcPath).isDirectory()) { + // Check if the directory is not in the excluded list + if (!excludedDirectories.includes(item)) { + // Recursively copy the directory + copyFolderRecursive(srcPath, destPath); + } + } else { + // Copy the file + fs.copyFileSync(srcPath, destPath); + } + }); +} + +// Call the function to start the copy process +copyFolderRecursive(sourcePath, destinationPath); diff --git a/packages/codemods/src/bin/spirit-codemod.js b/packages/codemods/src/bin/spirit-codemod.js deleted file mode 100644 index b3e143b069..0000000000 --- a/packages/codemods/src/bin/spirit-codemod.js +++ /dev/null @@ -1,3 +0,0 @@ -// TODO: will be removed in next issue: https://jira.almacareer.tech/browse/DS-1142 -// eslint-disable-next-line no-console -console.log('spirit-codemods'); diff --git a/packages/codemods/src/bin/spirit-codemods.js b/packages/codemods/src/bin/spirit-codemods.js new file mode 100755 index 0000000000..856928090c --- /dev/null +++ b/packages/codemods/src/bin/spirit-codemods.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +// eslint-disable-next-line import/no-unresolved -- The import is relative to the `dist` root +import { cli } from '../index.js'; + +cli(process.argv); diff --git a/packages/codemods/src/cli.ts b/packages/codemods/src/cli.ts new file mode 100644 index 0000000000..5146304c1f --- /dev/null +++ b/packages/codemods/src/cli.ts @@ -0,0 +1,55 @@ +import { $ } from 'execa'; +import sade from 'sade'; +import { fs, path } from 'zx'; +import { _dirname, errorMessage, logMessage } from './helpers'; + +const packageJson = fs.readJsonSync(path.resolve(_dirname, './package.json')); + +export default async function cli(args: string[]) { + sade('spirit-codemods', true) + .version(packageJson.version) + .describe(packageJson.description) + .option('-p, --path', 'Path to the code to be transformed') + .example('-p ./') + .option('-t, --transformation', 'Codemod transformation name to run') + .example('-t v2/web-react/codemodName') + .option('-e, --extensions', 'Extensions to look for when transforming files, default: ts,tsx,js,jsx') + .example('-e ts, tsx, js, jsx') + .option('-i, --ignore', 'Ignore files or directories, default: **/node_modules/**') + .example('-i **/node_modules/**') + .option('-r, --parser', 'Parser to use (babel, ts, tsx, flow), default: tsx') + .example('--parser babel') + .action(async ({ path: codePath, transformation, extensions, ignore, parser }) => { + const defaultExtensions = 'ts,tsx,js,jsx'; + const defaultIgnore = '**/node_modules/**'; + const defaultParser = 'tsx'; + + if (!codePath || !fs.existsSync(codePath)) { + errorMessage(codePath); + errorMessage('Please provide a valid path'); + process.exit(1); + } + + if (!transformation) { + errorMessage('Please provide a codemod name'); + process.exit(1); + } + + const codemodPath = path.resolve(_dirname, `./transforms/${transformation}.ts`); + + if (!fs.existsSync(codemodPath)) { + errorMessage('Codemod does not exists'); + process.exit(1); + } + + const { stdout } = await $`jscodeshift --transform ${codemodPath} --extensions ${ + extensions || defaultExtensions + } --ignore-pattern=${ignore || defaultIgnore} --parser=${parser || defaultParser} ${codePath}`; + + // stdout object from jscodeshift + logMessage(stdout); + + process.exit(0); + }) + .parse(args); +} diff --git a/packages/codemods/src/helpers/index.ts b/packages/codemods/src/helpers/index.ts new file mode 100644 index 0000000000..9f7bc11221 --- /dev/null +++ b/packages/codemods/src/helpers/index.ts @@ -0,0 +1,4 @@ +import { errorMessage, infoMessage, logMessage } from './message'; +import { _dirname } from './path'; + +export { _dirname, errorMessage, infoMessage, logMessage }; diff --git a/packages/codemods/src/helpers/message.ts b/packages/codemods/src/helpers/message.ts new file mode 100644 index 0000000000..aeca397df3 --- /dev/null +++ b/packages/codemods/src/helpers/message.ts @@ -0,0 +1,7 @@ +// Here we are defining function with only usage of Console +/* eslint-disable no-console */ +import { chalk } from 'zx'; + +export const errorMessage = (message: string) => console.error(chalk.red(message)); +export const infoMessage = (message: string) => console.info(chalk.magenta.bold(message)); +export const logMessage = (message: string) => console.log(message); diff --git a/packages/codemods/src/helpers/path.ts b/packages/codemods/src/helpers/path.ts new file mode 100644 index 0000000000..1c9ca8a01b --- /dev/null +++ b/packages/codemods/src/helpers/path.ts @@ -0,0 +1,3 @@ +import filedirname from 'filedirname'; + +export const [_filename, _dirname] = filedirname(); diff --git a/packages/codemods/src/index.ts b/packages/codemods/src/index.ts index e69de29bb2..a67a0e2399 100644 --- a/packages/codemods/src/index.ts +++ b/packages/codemods/src/index.ts @@ -0,0 +1,3 @@ +import cli from './cli'; + +export { cli }; diff --git a/packages/codemods/src/transforms/v2/web-react/__testfixtures__/button-text.input.tsx b/packages/codemods/src/transforms/v2/web-react/__testfixtures__/button-text.input.tsx new file mode 100644 index 0000000000..d48a4f264e --- /dev/null +++ b/packages/codemods/src/transforms/v2/web-react/__testfixtures__/button-text.input.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +// @ts-ignore +import { Button } from '@lmc-eu/spirit-web-react'; + +export const MyComponent = () =>