Skip to content

Commit b3bd2cd

Browse files
Merge pull request #30 from CodeshiftCommunity/cli-sub-commands
CLI sub-commands for creating and validating (not publishing yet) external codemods
2 parents 7dc1e9d + dd79f0e commit b3bd2cd

File tree

12 files changed

+223
-64
lines changed

12 files changed

+223
-64
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
"types:check": "tsc --noEmit --skipLibCheck",
2323
"monorepo:check": "manypkg check",
2424
"monorepo:fix": "manypkg fix && preconstruct fix",
25-
"init:codemods": "ts-node packages/initializer/src/index.ts",
25+
"init:codemods": "ts-node scripts/initialize.ts",
2626
"start:codemods": "node packages/cli/bin/codeshift-cli.js",
27-
"validate:codemods": "ts-node packages/validator/src/index.ts ./community",
27+
"validate:codemods": "ts-node scripts/validate.ts ./community",
2828
"release:codemods": "ts-node packages/publisher/src/index.ts ./community ./.tmp",
2929
"prerelease": "yarn validate && yarn test",
3030
"release": "yarn changeset publish"

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"start": "ts-node src/index.ts"
1414
},
1515
"dependencies": {
16+
"@codeshift/initializer": "0.1.0",
17+
"@codeshift/validator": "0.1.0",
1618
"chalk": "^4.1.0",
1719
"commander": "^7.2.0",
1820
"fs-extra": "^9.1.0",

packages/cli/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import chalk from 'chalk';
22
import main from './main';
33
import list from './list';
4+
import init from './init';
5+
import validate from './validate';
46
import {
57
ValidationError,
68
NoTransformsExistError,
@@ -65,6 +67,34 @@ program
6567
.description('list available codemods for provided packages')
6668
.action(packageNames => list(packageNames));
6769

70+
program
71+
.command('init [path]')
72+
.description('create a new codemod package')
73+
// FIXME: Commander seems to have issues parsing the paths and arguments
74+
.option('--package-name <name>', 'Name of the package')
75+
.option('--version <version>', 'Target version')
76+
.action((path, options) => init(options.packageName, options.version, path))
77+
.addHelpText(
78+
'after',
79+
`
80+
Examples:
81+
$ npx @codeshift/cli init --package-name foobar --version 10.0.0 ~/Desktop
82+
`,
83+
);
84+
85+
program
86+
.command('validate [path]')
87+
.description('validates if a codemod package is publishable')
88+
.action(path => validate(path))
89+
.addHelpText(
90+
'after',
91+
`
92+
Examples:
93+
$ npx @codeshift/cli validate
94+
$ npx @codeshift/cli validate ./codemods/my-codemods
95+
`,
96+
);
97+
6898
program.exitOverride();
6999

70100
program.parseAsync(process.argv).catch(e => {

packages/cli/src/init.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { initDirectory } from '@codeshift/initializer';
2+
3+
export default async function init(
4+
packageName: string,
5+
version: string,
6+
targetPath: string = '.',
7+
) {
8+
initDirectory(packageName, version, targetPath);
9+
10+
console.log(
11+
`🚚 New codemod package created at: ${targetPath}/${packageName}`,
12+
);
13+
}

packages/cli/src/validate.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { isValidConfig, isValidPackageJson } from '@codeshift/validator';
2+
3+
export default async function validate(targetPath: string = '.') {
4+
try {
5+
await isValidConfig(targetPath);
6+
await isValidPackageJson(targetPath);
7+
} catch (error) {
8+
console.warn(error);
9+
process.exit(1);
10+
}
11+
12+
console.log('Valid ✅');
13+
}

packages/initializer/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"name": "@codeshift/initializer",
3-
"private": true,
43
"version": "0.1.0",
54
"main": "dist/codeshift-initializer.cjs.js",
65
"types": "dist/codeshift-initializer.cjs.d.ts",

packages/initializer/src/index.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@ import fs from 'fs-extra';
22
import semver from 'semver';
33
import * as recast from 'recast';
44

5-
function main(packageName: string, version: string) {
6-
if (!packageName) throw new Error('Package name was not provided');
7-
if (!version) throw new Error('Version was not provided');
8-
5+
export function initDirectory(
6+
packageName: string,
7+
version: string,
8+
targetPath: string = './',
9+
) {
910
if (!semver.valid(version)) {
1011
throw new Error(
1112
`Provided version ${version} is not a valid semver version`,
1213
);
1314
}
1415

15-
const communityDirectoryPath = `${__dirname}/../../../community`;
16-
const safePackageName = packageName.replace('/', '__');
17-
const codemodBasePath = `${communityDirectoryPath}/${safePackageName}`;
18-
const codemodPath = `${codemodBasePath}/${version}`;
19-
const configPath = `${codemodBasePath}/codeshift.config.js`;
16+
const basePath = `${targetPath}/${packageName.replace('/', '__')}`;
17+
const codemodPath = `${basePath}/${version}`;
18+
const configPath = `${basePath}/codeshift.config.js`;
19+
const packagePath = `${basePath}/package.json`;
2020
const motionsPath = `${codemodPath}/motions`;
2121

2222
fs.mkdirSync(codemodPath, { recursive: true });
@@ -38,6 +38,22 @@ function main(packageName: string, version: string) {
3838

3939
fs.writeFileSync(`${codemodPath}/transform.spec.ts`, testFile);
4040

41+
fs.writeFileSync(
42+
packagePath,
43+
`{
44+
"name": "${packageName}",
45+
"version": "0.0.1",
46+
"license": "MIT",
47+
"main": "dist/${packageName}.cjs.js",
48+
"dependencies": {
49+
"@codeshift/utils": "^0.1.2"
50+
},
51+
"devDependencies": {
52+
"jscodeshift": "^0.12.0"
53+
}
54+
}`,
55+
);
56+
4157
if (!fs.existsSync(configPath)) {
4258
fs.writeFileSync(
4359
configPath,
@@ -88,10 +104,4 @@ function main(packageName: string, version: string) {
88104
recast.prettyPrint(ast, { quote: 'single', trailingComma: true }).code,
89105
);
90106
}
91-
92-
console.log(
93-
`🚚 New codemod package created at: community/${safePackageName}/${version}`,
94-
);
95107
}
96-
97-
main(process.argv[2], process.argv[3]);

packages/validator/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"name": "@codeshift/validator",
3-
"private": true,
43
"version": "0.1.0",
54
"main": "dist/codeshift-validator.cjs.js",
65
"types": "dist/codeshift-validator.cjs.d.ts",
@@ -11,6 +10,7 @@
1110
},
1211
"dependencies": {
1312
"fs-extra": "^9.1.0",
13+
"recast": "^0.20.4",
1414
"semver": "^7.3.5",
1515
"ts-node": "^9.1.1"
1616
}

packages/validator/src/index.ts

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,63 @@
1-
import fs, { lstatSync, existsSync } from 'fs-extra';
1+
import fs from 'fs-extra';
22
import semver from 'semver';
3+
import * as recast from 'recast';
34

45
const packageNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*__)?[a-z0-9-~][a-z0-9-._~]*$/;
56

6-
async function main(path: string) {
7-
const directories = await fs.readdir(path);
8-
9-
directories.forEach(async dir => {
10-
if (!dir.match(packageNameRegex)) {
11-
throw new Error(
12-
`Invalid package name: ${dir}.
13-
If this is a scoped package, please make sure rename the folder to use the "__" characters to denote submodule.
14-
For example: @atlaskit/avatar => @atlaskit__avatar`,
15-
);
16-
}
17-
18-
const subDirectories = await fs.readdir(`${path}/${dir}`);
19-
let hasConfigFile = false;
20-
21-
subDirectories.forEach(subDir => {
22-
if (subDir === 'codeshift.config.js') {
23-
hasConfigFile = true;
24-
}
7+
export function isValidPackageName(dir: string) {
8+
return dir.match(packageNameRegex);
9+
}
2510

26-
if (
27-
lstatSync(`${path}/${dir}/${subDir}`).isDirectory() &&
28-
!semver.valid(subDir)
29-
) {
30-
throw new Error(
31-
`Codemod folder name "${subDir}" has an invalid version name. Please make sure the file name is valid semver. For example "18.0.0"`,
32-
);
11+
export async function isValidConfig(path: string) {
12+
const configPath = path + `/codeshift.config.js`;
13+
const source = await fs.readFile(configPath, 'utf8');
14+
const ast = recast.parse(source);
15+
16+
let hasTransforms = false;
17+
let invalidSemverIds = [];
18+
let transformCount = 0;
19+
20+
recast.visit(ast, {
21+
visitProperty(path) {
22+
// @ts-ignore
23+
if (path.node.key.name === 'transforms') {
24+
hasTransforms = true;
25+
// @ts-ignore
26+
const properties = path.node.value.properties;
27+
transformCount = properties.length;
28+
// @ts-ignore
29+
properties.forEach(property => {
30+
if (!semver.valid(property.key.value)) {
31+
invalidSemverIds.push(property.key.value);
32+
}
33+
});
3334
}
3435

35-
if (
36-
lstatSync(`${path}/${dir}/${subDir}`).isDirectory() &&
37-
!existsSync(`${path}/${dir}/${subDir}/transform.ts`) &&
38-
!existsSync(`${path}/${dir}/${subDir}/transform.js`)
39-
) {
40-
throw new Error(
41-
`Unable to find transform entry-point for directory "${path}/${dir}/${subDir}". Please ensure you have a valid transform.(ts|js) file containing the entry-point for your codemod`,
42-
);
43-
}
44-
});
45-
46-
if (!hasConfigFile) {
47-
throw new Error(
48-
`No config file found at: ${path}/${dir}.
49-
Please create a config file named "codeshift.config.js"`,
50-
);
51-
}
36+
return false;
37+
},
5238
});
39+
40+
if (!hasTransforms || !transformCount) {
41+
throw new Error(
42+
'At least one transform should be specified for config at "${configPath}"',
43+
);
44+
}
45+
46+
if (invalidSemverIds.length) {
47+
throw new Error(`Invalid transform ids found for config at "${configPath}".
48+
Please make sure all transforms are identified by a valid semver version. ie 10.0.0`);
49+
}
5350
}
5451

55-
main(process.argv[2]);
52+
export async function isValidPackageJson(path: string) {
53+
const packageJsonRaw = await fs.readFile(path + '/package.json', 'utf8');
54+
const packageJson = JSON.parse(packageJsonRaw);
55+
56+
if (!packageJson.name) {
57+
throw new Error('No package name provided in package.json');
58+
}
59+
60+
if (!packageJson.main) {
61+
throw new Error('No main entrypoint provided in package.json');
62+
}
63+
}

scripts/initialize.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { initDirectory } from '@codeshift/initializer';
2+
3+
export function main(packageName: string, version: string) {
4+
const path = `${__dirname}/../community`;
5+
6+
if (!packageName) throw new Error('Package name was not provided');
7+
if (!version) throw new Error('Version was not provided');
8+
9+
initDirectory(packageName, version, path);
10+
11+
console.log(
12+
`🚚 New codemod package created at: community/${packageName}/${version}`,
13+
);
14+
}
15+
16+
main(process.argv[2], process.argv[3]);

scripts/validate.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import fs, { lstatSync, existsSync } from 'fs-extra';
2+
import semver from 'semver';
3+
import { isValidPackageName, isValidConfig } from '@codeshift/validator';
4+
5+
async function main(path: string) {
6+
const directories = await fs.readdir(path);
7+
8+
directories.forEach(async dir => {
9+
if (!isValidPackageName(dir)) {
10+
throw new Error(
11+
`Invalid package name: ${dir}.
12+
If this is a scoped package, please make sure rename the folder to use the "__" characters to denote submodule.
13+
For example: @foo/bar => @foo__bar`,
14+
);
15+
}
16+
17+
await isValidConfig(`${path}/${dir}`);
18+
19+
const subDirectories = await fs.readdir(`${path}/${dir}`);
20+
subDirectories.forEach(async subDir => {
21+
if (
22+
lstatSync(`${path}/${dir}/${subDir}`).isDirectory() &&
23+
!semver.valid(subDir)
24+
) {
25+
throw new Error(
26+
`Codemod folder name "${subDir}" has an invalid version name. Please make sure the file name is valid semver. For example "18.0.0"`,
27+
);
28+
}
29+
30+
if (
31+
lstatSync(`${path}/${dir}/${subDir}`).isDirectory() &&
32+
!existsSync(`${path}/${dir}/${subDir}/transform.ts`) &&
33+
!existsSync(`${path}/${dir}/${subDir}/transform.js`)
34+
) {
35+
throw new Error(
36+
`Unable to find transform entry-point for directory "${path}/${dir}/${subDir}". Please ensure you have a valid transform.(ts|js) file containing the entry-point for your codemod`,
37+
);
38+
}
39+
});
40+
});
41+
}
42+
43+
main(process.argv[2]);

website/docs/api/codeshift-cli.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,28 @@ Print a list of available codemods for a single package
117117
Print a list of available codemods for multiple packages
118118

119119
- `@codeshift/cli list mylib, @material-ui/button`
120+
121+
### init
122+
123+
Generates a new codemod at your desired path
124+
125+
**example:**
126+
127+
Create a new codemod package called foobar with a transform for version 10
128+
on the Desktop
129+
130+
- `@codeshift/cli init --packageName="foobar" --version"10.0.0" ~/Desktop`
131+
132+
### validate
133+
134+
Validates a codemod package at the desired path.
135+
136+
**example:**
137+
138+
Validate a codemod package called "my-codemods".
139+
140+
- `$ npx @codeshift/cli validate ./codemods/my-codemods`
141+
142+
Validate a codemod package from the current working directory
143+
144+
- `$ npx @codeshift/cli validate`

0 commit comments

Comments
 (0)