Skip to content

Commit

Permalink
Feat(web-react): Add spirit-codemod package
Browse files Browse the repository at this point in the history
- Added codemod-spirit package with first
transform for buttonLabel->buttonText

- Added test to this transform
  • Loading branch information
pavelklibani committed Mar 5, 2024
1 parent 07742b8 commit 1e9fc85
Show file tree
Hide file tree
Showing 20 changed files with 301 additions and 14 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions packages/codemods/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -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__/**
1 change: 1 addition & 0 deletions packages/codemods/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules

# Caches
.cache
.DS_Store

# Log files
*.log
Expand Down
37 changes: 37 additions & 0 deletions packages/codemods/README.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion packages/codemods/config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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: ['<rootDir>/dist/'],
modulePathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/*/__testfixtures__/'],
};

export default config;
9 changes: 7 additions & 2 deletions packages/codemods/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./",
Expand All @@ -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",
Expand Down
42 changes: 42 additions & 0 deletions packages/codemods/scripts/copyTransforms.js
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 0 additions & 3 deletions packages/codemods/src/bin/spirit-codemod.js

This file was deleted.

5 changes: 5 additions & 0 deletions packages/codemods/src/bin/spirit-codemods.js
Original file line number Diff line number Diff line change
@@ -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);
55 changes: 55 additions & 0 deletions packages/codemods/src/cli.ts
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 4 additions & 0 deletions packages/codemods/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { errorMessage, infoMessage, logMessage } from './message';
import { _dirname } from './path';

export { _dirname, errorMessage, infoMessage, logMessage };
7 changes: 7 additions & 0 deletions packages/codemods/src/helpers/message.ts
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 3 additions & 0 deletions packages/codemods/src/helpers/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import filedirname from 'filedirname';

export const [_filename, _dirname] = filedirname();
3 changes: 3 additions & 0 deletions packages/codemods/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import cli from './cli';

export { cli };
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';
// @ts-ignore
import { Button } from '@lmc-eu/spirit-web-react';

export const MyComponent = () => <Button buttonLabel="Click me" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';
// @ts-ignore
import { Button } from '@lmc-eu/spirit-web-react';

export const MyComponent = () => <Button buttonText="Click me" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// eslint-disable-next-line import/extensions
const { defineTest } = require('jscodeshift/dist/testUtils');

defineTest(__dirname, 'button-text', null, 'button-text', {
parser: 'tsx',
fixture: 'input',
snapshot: true,
});
54 changes: 54 additions & 0 deletions packages/codemods/src/transforms/v2/web-react/button-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { API, FileInfo } from 'jscodeshift';

const transform = (fileInfo: FileInfo, api: API) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);

// Find import statements for the specific module and Button specifier
const importStatements = root.find(j.ImportDeclaration, {
// source: {
// value: (value: string) => value === '@lmc-eu/spirit-web-react' || value === '@lmc-eu/spirit-web-react/components',
// },
source: {
value: (value: string) => /^@lmc-eu\/spirit-web-react(\/.*)?$/.test(value),
},
});

// Check if the module is imported
if (importStatements.length > 0) {
const buttonSpecifier = importStatements.find(j.ImportSpecifier, {
imported: {
type: 'Identifier',
name: 'Button',
},
});

// Check if Button specifier is present
if (buttonSpecifier.length > 0) {
// Find Button components in the module
const buttonComponents = root.find(j.JSXOpeningElement, {
name: {
type: 'JSXIdentifier',
name: 'Button',
},
});

// Rename 'buttonLabel' attribute to 'buttonText'
buttonComponents
.find(j.JSXAttribute, {
name: {
type: 'JSXIdentifier',
name: 'buttonLabel',
},
})
.forEach((attributePath) => {
// Change attribute name to 'buttonText'
attributePath.node.name.name = 'buttonText';
});
}
}

return root.toSource();
};

export default transform;
2 changes: 1 addition & 1 deletion packages/codemods/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@
"module": "es2020"
},
"include": ["./", "./.eslintrc.js"],
"exclude": ["./node_modules"]
"exclude": ["./node_modules", "**/__testfixtures__/**"]
}
Loading

0 comments on commit 1e9fc85

Please sign in to comment.