Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(codmods): Introduce codemods package and first React codemod #DS-1142 #1303

Merged
merged 1 commit into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
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
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
# https://github.com/facebook/jscodeshift?tab=readme-ov-file#unit-testing
**/__testfixtures__/**
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`.
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved

## 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
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
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';
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved

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 exist');
process.exit(1);
}

const { stdout } = await $`jscodeshift --transform ${codemodPath} --extensions ${
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
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 };
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';
// @ts-ignore: No declaration -- The library is not installed; we don't need to install it for fixtures.
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: No declaration -- The library is not installed; we don't need to install it for fixtures.
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,
});
51 changes: 51 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,51 @@
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) => /^@lmc-eu\/spirit-web-react(\/.*)?$/.test(value),
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
},
});

// 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