From cc87061032c20661b90d8aa1772f5b215eb51e85 Mon Sep 17 00:00:00 2001 From: Daniel Del Core Date: Mon, 25 Oct 2021 20:52:11 +1100 Subject: [PATCH 1/6] remote module fetching --- packages/cli/src/main.ts | 52 +++++++++++++++++++---- packages/cli/src/validate.ts | 4 +- packages/types/README.md | 1 + packages/types/package.json | 9 ++++ packages/types/src/index.ts | 7 +++ packages/validator/package.json | 1 + packages/validator/src/index.ts | 75 +++++++++++++++++++++------------ scripts/validate.ts | 4 +- 8 files changed, 112 insertions(+), 41 deletions(-) create mode 100755 packages/types/README.md create mode 100644 packages/types/package.json create mode 100644 packages/types/src/index.ts diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 5ae27236b..a5aa21991 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -1,12 +1,54 @@ import semver from 'semver'; import chalk from 'chalk'; +import path from 'path'; import { PluginManager } from 'live-plugin-manager'; // @ts-ignore Run transform(s) on path https://github.com/facebook/jscodeshift/issues/398 import * as jscodeshift from 'jscodeshift/src/Runner'; +import { isValidConfig } from '@codeshift/validator'; import { Flags } from './types'; import { InvalidUserInputError } from './errors'; +const packageManager = new PluginManager(); + +async function fetchPackageConfig(packageName: string) { + // Attempt to find package from the community folder + await packageManager.install(`@codeshift/mod-${packageName}`); + const commPkg = packageManager.require(packageName); + const commConfig = commPkg.default ? commPkg.default : commPkg; + + // if (!isValidConfig(commConfig)) { + // } + + // Attempt to find source package from npm + await packageManager.install(packageName); + // For source packages, fetching configs is a bit more elaborate + let config; + + // Attemp to fetch from the main entrypoint + const info = packageManager.getInfo(packageName); + const pkg = packageManager.require(packageName); + + if (info || pkg) { + config = pkg.default ? pkg.default : pkg; + + if (config && isValidConfig) { + // Found a config at the main entry-point + } + + config = require(path.join(info?.location, 'codeshift.config.js')); + config = require(path.join(info?.location, 'src', 'codeshift.config.js')); + config = require(path.join( + info?.location, + 'codemods', + 'codeshift.config.js', + )); + } + // if () + + return config; +} + export default async function main(paths: string[], flags: Flags) { let transforms: string[] = []; @@ -26,8 +68,6 @@ export default async function main(paths: string[], flags: Flags) { transforms.push(flags.transform); } - const packageManager = new PluginManager(); - if (flags.packages) { const pkgs = flags.packages.split(',').filter(pkg => !!pkg); @@ -36,14 +76,8 @@ export default async function main(paths: string[], flags: Flags) { .split(/[@#]/) .filter(str => !!str)[0] .replace('/', '__'); - const packageName = `@codeshift/mod-${pkgName}`; - - await packageManager.install(packageName); - const codeshiftPackage = packageManager.require(packageName); - const config = codeshiftPackage.default - ? codeshiftPackage.default - : codeshiftPackage; + const config = await fetchPackageConfig(pkgName); const rawTransformIds = pkg.split(/(?=[@#])/).filter(str => !!str); rawTransformIds.shift(); diff --git a/packages/cli/src/validate.ts b/packages/cli/src/validate.ts index eac725fbe..290524236 100644 --- a/packages/cli/src/validate.ts +++ b/packages/cli/src/validate.ts @@ -1,8 +1,8 @@ -import { isValidConfig, isValidPackageJson } from '@codeshift/validator'; +import { isValidConfigAtPath, isValidPackageJson } from '@codeshift/validator'; export default async function validate(targetPath: string = '.') { try { - await isValidConfig(targetPath); + await isValidConfigAtPath(targetPath); await isValidPackageJson(targetPath); } catch (error) { console.warn(error); diff --git a/packages/types/README.md b/packages/types/README.md new file mode 100755 index 000000000..6b2f3538a --- /dev/null +++ b/packages/types/README.md @@ -0,0 +1 @@ +# @codeshift/types diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 000000000..3e16e7183 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,9 @@ +{ + "name": "@codeshift/types", + "version": "0.0.0", + "main": "dist/codeshift-types.cjs.js", + "module": "dist/codeshift-types.esm.js", + "types": "dist/codeshift-types.cjs.d.ts", + "license": "MIT", + "repository": "https://github.com/CodeshiftCommunity/CodeshiftCommunity/tree/master/packages/types" +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 000000000..25616f495 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,7 @@ +export interface CodeshiftConfig { + target?: string[]; + maintainers?: string[]; + description?: string; + transforms?: Record; + presets?: Record; +} diff --git a/packages/validator/package.json b/packages/validator/package.json index 032dc4430..c85354095 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -6,6 +6,7 @@ "license": "MIT", "repository": "https://github.com/CodeshiftCommunity/CodeshiftCommunity/tree/master/packages/validator", "dependencies": { + "@codeshift/types": "^0.0.1", "fs-extra": "^9.1.0", "recast": "^0.20.4", "semver": "^7.3.5", diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 759a2829d..22432796e 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -1,50 +1,69 @@ import fs from 'fs-extra'; import semver from 'semver'; import path from 'path'; +import { CodeshiftConfig } from '@codeshift/types'; -export function isValidPackageName(dir: string) { - return dir.match(/^(@[a-z0-9-~][a-z0-9-._~]*__)?[a-z0-9-~][a-z0-9-._~]*$/); -} - -export async function isValidConfig(filePath: string) { +function getConfigFromPath(filePath: string): CodeshiftConfig { const configPath = path.join(process.cwd(), filePath, 'codeshift.config.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires - let config = require(configPath); + const config = require(configPath); + + return !!config.default ? config.default : config; +} - config = !!config.default ? config.default : config; +function hasValidTransforms(transforms?: Record) { + if (!transforms || !Object.keys(transforms).length) return false; - const invalidSemverIds = []; - const invalidPresetIds = []; + let isValid = true; - let hasTransforms = false; + Object.entries(transforms).forEach(([key]) => { + if (!semver.valid(key)) isValid = false; + }); - if (config.transforms && Object.keys(config.transforms).length) { - Object.entries(config.transforms).forEach(([key]) => { - hasTransforms = true; - if (!semver.valid(key)) invalidSemverIds.push(key); - }); - } + return isValid; +} - if (config.presets && Object.keys(config.presets).length) { - hasTransforms = true; - Object.entries(config.presets).forEach(([key]) => { - if (key.includes(' ')) invalidPresetIds.push(key); - }); - } +function hasValidPresets(presets?: Record) { + if (!presets || !Object.keys(presets).length) return false; + + let isValid = true; + + Object.entries(presets).forEach(([key]) => { + if (!key.match(/^[0-9a-zA-Z\-]+$/)) isValid = false; + }); + + return isValid; +} + +export function isValidPackageName(dir: string) { + return dir.match(/^(@[a-z0-9-~][a-z0-9-._~]*__)?[a-z0-9-~][a-z0-9-._~]*$/); +} + +export async function isValidConfig(config: CodeshiftConfig) { + return ( + hasValidTransforms(config.transforms) || hasValidPresets(config.presets) + ); +} + +export async function isValidConfigAtPath(filePath: string) { + const config = getConfigFromPath(filePath); - if (!hasTransforms) { + if ( + !hasValidTransforms(config.transforms) && + !hasValidPresets(config.presets) + ) { throw new Error( - `At least one transform should be specified for config at "${configPath}"`, + `At least one transform should be specified for config at "${filePath}"`, ); } - if (invalidSemverIds.length) { - throw new Error(`Invalid transform ids found for config at "${configPath}". + if (!hasValidTransforms(config.transforms)) { + throw new Error(`Invalid transform ids found for config at "${filePath}". Please make sure all transforms are identified by a valid semver version. ie 10.0.0`); } - if (invalidPresetIds.length) { - throw new Error(`Invalid preset ids found for config at "${configPath}". + if (!hasValidPresets(config.presets)) { + throw new Error(`Invalid preset ids found for config at "${filePath}". Please make sure all presets are kebab case and contain no spaces or special characters. ie sort-imports-by-scope`); } } diff --git a/scripts/validate.ts b/scripts/validate.ts index 57070ec04..0e1a2e33f 100644 --- a/scripts/validate.ts +++ b/scripts/validate.ts @@ -1,5 +1,5 @@ import fs, { lstatSync, existsSync } from 'fs-extra'; -import { isValidPackageName, isValidConfig } from '@codeshift/validator'; +import { isValidPackageName, isValidConfigAtPath } from '@codeshift/validator'; async function main(path: string) { const directories = await fs.readdir(path); @@ -13,7 +13,7 @@ async function main(path: string) { ); } - await isValidConfig(`${path}/${dir}`); + await isValidConfigAtPath(`${path}/${dir}`); const subDirectories = await fs.readdir(`${path}/${dir}`); subDirectories.forEach(async subDir => { From 6de9ac1ca2e9eef5460c5f2acc9cd640febcf758 Mon Sep 17 00:00:00 2001 From: Daniel Del Core Date: Wed, 27 Oct 2021 21:22:38 +1100 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=94=A8=20replace=20reduce=20with=20ev?= =?UTF-8?q?ery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/validator/src/index.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 22432796e..e2f373974 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -14,25 +14,15 @@ function getConfigFromPath(filePath: string): CodeshiftConfig { function hasValidTransforms(transforms?: Record) { if (!transforms || !Object.keys(transforms).length) return false; - let isValid = true; - - Object.entries(transforms).forEach(([key]) => { - if (!semver.valid(key)) isValid = false; - }); - - return isValid; + return Object.entries(transforms).every(([key]) => semver.valid(key)); } function hasValidPresets(presets?: Record) { if (!presets || !Object.keys(presets).length) return false; - let isValid = true; - - Object.entries(presets).forEach(([key]) => { - if (!key.match(/^[0-9a-zA-Z\-]+$/)) isValid = false; - }); - - return isValid; + return Object.entries(presets).every(([key]) => + key.match(/^[0-9a-zA-Z\-]+$/), + ); } export function isValidPackageName(dir: string) { From 5c220d4e1b018d011674bc644f40c5419e987466 Mon Sep 17 00:00:00 2001 From: Daniel Del Core Date: Wed, 27 Oct 2021 21:52:09 +1100 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9C=A8=20Remote=20config=20file=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/cli/src/main.ts | 76 ++++++++++++++++++++++----------- packages/types/package.json | 2 +- packages/validator/package.json | 4 ++ packages/validator/src/index.ts | 4 +- yarn.lock | 11 +++-- 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index a5aa21991..f8c1624b1 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -2,51 +2,73 @@ import semver from 'semver'; import chalk from 'chalk'; import path from 'path'; import { PluginManager } from 'live-plugin-manager'; +import merge from 'lodash/merge'; // @ts-ignore Run transform(s) on path https://github.com/facebook/jscodeshift/issues/398 import * as jscodeshift from 'jscodeshift/src/Runner'; import { isValidConfig } from '@codeshift/validator'; +import { CodeshiftConfig } from '@codeshift/types'; import { Flags } from './types'; import { InvalidUserInputError } from './errors'; const packageManager = new PluginManager(); -async function fetchPackageConfig(packageName: string) { - // Attempt to find package from the community folder - await packageManager.install(`@codeshift/mod-${packageName}`); - const commPkg = packageManager.require(packageName); - const commConfig = commPkg.default ? commPkg.default : commPkg; +async function fetchCommunityPackageConfig(packageName: string) { + const commPackageName = `@codeshift/mod-${packageName}`; + await packageManager.install(commPackageName); + const pkg = packageManager.require(packageName); + const config: CodeshiftConfig = pkg.default ? pkg.default : pkg; - // if (!isValidConfig(commConfig)) { - // } + if (!isValidConfig(config)) { + throw new Error(`Invalid config found in module ${commPackageName}`); + } - // Attempt to find source package from npm - await packageManager.install(packageName); - // For source packages, fetching configs is a bit more elaborate - let config; + return config; +} - // Attemp to fetch from the main entrypoint - const info = packageManager.getInfo(packageName); +async function fetchRemotePackageConfig(packageName: string) { + await packageManager.install(packageName); const pkg = packageManager.require(packageName); - if (info || pkg) { - config = pkg.default ? pkg.default : pkg; + if (pkg) { + const config: CodeshiftConfig = pkg.default ? pkg.default : pkg; - if (config && isValidConfig) { + if (config && isValidConfig(config)) { // Found a config at the main entry-point + return config; } + } - config = require(path.join(info?.location, 'codeshift.config.js')); - config = require(path.join(info?.location, 'src', 'codeshift.config.js')); - config = require(path.join( - info?.location, - 'codemods', - 'codeshift.config.js', - )); + const info = packageManager.getInfo(packageName); + + if (info) { + let config: any; + + [ + path.join(info?.location, 'codeshift.config.js'), + path.join(info?.location, 'codeshift.config.ts'), + path.join(info?.location, 'src', 'codeshift.config.js'), + path.join(info?.location, 'src', 'codeshift.config.ts'), + path.join(info?.location, 'codemods', 'codeshift.config.js'), + path.join(info?.location, 'codemods', 'codeshift.config.ts'), + ].forEach(searchPath => { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkg = require(searchPath); + const searchConfig: CodeshiftConfig = pkg.default ? pkg.default : pkg; + + if (isValidConfig(searchConfig)) { + config = searchConfig; + } + } catch (e) {} + }); + + if (config) return config; } - // if () - return config; + throw new Error( + `Unable to locate a valid codeshift.config in package ${packageName}`, + ); } export default async function main(paths: string[], flags: Flags) { @@ -77,7 +99,9 @@ export default async function main(paths: string[], flags: Flags) { .filter(str => !!str)[0] .replace('/', '__'); - const config = await fetchPackageConfig(pkgName); + const communityConfig = await fetchCommunityPackageConfig(pkgName); + const remoteConfig = await fetchRemotePackageConfig(pkgName); + const config: CodeshiftConfig = merge(communityConfig, remoteConfig); const rawTransformIds = pkg.split(/(?=[@#])/).filter(str => !!str); rawTransformIds.shift(); diff --git a/packages/types/package.json b/packages/types/package.json index 3e16e7183..1dc883240 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@codeshift/types", - "version": "0.0.0", + "version": "0.0.1", "main": "dist/codeshift-types.cjs.js", "module": "dist/codeshift-types.esm.js", "types": "dist/codeshift-types.cjs.d.ts", diff --git a/packages/validator/package.json b/packages/validator/package.json index c85354095..2b0a6138d 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -8,8 +8,12 @@ "dependencies": { "@codeshift/types": "^0.0.1", "fs-extra": "^9.1.0", + "lodash": "^4.17.21", "recast": "^0.20.4", "semver": "^7.3.5", "ts-node": "^9.1.1" + }, + "devDependencies": { + "@types/lodash": "^4.14.176" } } diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index e2f373974..19db92ece 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -29,13 +29,13 @@ export function isValidPackageName(dir: string) { return dir.match(/^(@[a-z0-9-~][a-z0-9-._~]*__)?[a-z0-9-~][a-z0-9-._~]*$/); } -export async function isValidConfig(config: CodeshiftConfig) { +export function isValidConfig(config: CodeshiftConfig) { return ( hasValidTransforms(config.transforms) || hasValidPresets(config.presets) ); } -export async function isValidConfigAtPath(filePath: string) { +export function isValidConfigAtPath(filePath: string) { const config = getConfigFromPath(filePath); if ( diff --git a/yarn.lock b/yarn.lock index 0ccb6c00f..d785c964c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,6 +1722,11 @@ resolved "https://registry.npmjs.org/@types/lockfile/-/lockfile-1.0.2.tgz#3f77e84171a2b7e3198bd5717c7547a54393baf8" integrity sha512-jD5VbvhfMhaYN4M3qPJuhMVUg3Dfc4tvPvLEAXn6GXbs/ajDFtCQahX37GIE65ipTI3I+hEvNaXS3MYAn9Ce3Q== +"@types/lodash@^4.14.176": + version "4.14.176" + resolved "https://packages.atlassian.com/api/npm/npm-remote/@types/lodash/-/lodash-4.14.176.tgz#641150fc1cda36fbfa329de603bbb175d7ee20c0" + integrity sha1-ZBFQ/BzaNvv6Mp3mA7uxddfuIMA= + "@types/minimatch@*": version "3.0.5" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -5009,10 +5014,10 @@ lodash.unescape@4.0.1: resolved "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= -lodash@4.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + resolved "https://packages.atlassian.com/api/npm/npm-remote/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw= loud-rejection@^1.0.0: version "1.6.0" From 819a913faa38b9a43c9226cfa5dea8aae308785a Mon Sep 17 00:00:00 2001 From: Daniel Del Core Date: Tue, 23 Nov 2021 15:08:35 +1100 Subject: [PATCH 4/6] fix isValidPackageName logic --- packages/validator/src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/validator/src/index.ts b/packages/validator/src/index.ts index 19db92ece..1456a8bef 100644 --- a/packages/validator/src/index.ts +++ b/packages/validator/src/index.ts @@ -17,7 +17,7 @@ function hasValidTransforms(transforms?: Record) { return Object.entries(transforms).every(([key]) => semver.valid(key)); } -function hasValidPresets(presets?: Record) { +function hasValidPresets(presets?: Record): boolean { if (!presets || !Object.keys(presets).length) return false; return Object.entries(presets).every(([key]) => @@ -25,8 +25,8 @@ function hasValidPresets(presets?: Record) { ); } -export function isValidPackageName(dir: string) { - return dir.match(/^(@[a-z0-9-~][a-z0-9-._~]*__)?[a-z0-9-~][a-z0-9-._~]*$/); +export function isValidPackageName(dir: string): boolean { + return !!dir.match(/^(@[a-z0-9-~][a-z0-9-._~]*__)?[a-z0-9-~][a-z0-9-._~]*$/); } export function isValidConfig(config: CodeshiftConfig) { @@ -69,4 +69,6 @@ export async function isValidPackageJson(path: string) { if (!packageJson.main) { throw new Error('No main entrypoint provided in package.json'); } + + return true; } From fb799fa956b8a041f5d8880df014469efd666a43 Mon Sep 17 00:00:00 2001 From: Daniel Del Core Date: Tue, 23 Nov 2021 16:47:55 +1100 Subject: [PATCH 5/6] fixes main logic --- packages/cli/src/main.ts | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index f8c1624b1..2df443c84 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -14,9 +14,11 @@ import { InvalidUserInputError } from './errors'; const packageManager = new PluginManager(); async function fetchCommunityPackageConfig(packageName: string) { - const commPackageName = `@codeshift/mod-${packageName}`; + const pkgName = packageName.replace('@', '').replace('/', '__'); + const commPackageName = `@codeshift/mod-${pkgName}`; + await packageManager.install(commPackageName); - const pkg = packageManager.require(packageName); + const pkg = packageManager.require(commPackageName); const config: CodeshiftConfig = pkg.default ? pkg.default : pkg; if (!isValidConfig(config)) { @@ -42,7 +44,7 @@ async function fetchRemotePackageConfig(packageName: string) { const info = packageManager.getInfo(packageName); if (info) { - let config: any; + let config: CodeshiftConfig | undefined; [ path.join(info?.location, 'codeshift.config.js'), @@ -94,14 +96,29 @@ export default async function main(paths: string[], flags: Flags) { const pkgs = flags.packages.split(',').filter(pkg => !!pkg); for (const pkg of pkgs) { - const pkgName = pkg - .split(/[@#]/) - .filter(str => !!str)[0] - .replace('/', '__'); - - const communityConfig = await fetchCommunityPackageConfig(pkgName); - const remoteConfig = await fetchRemotePackageConfig(pkgName); - const config: CodeshiftConfig = merge(communityConfig, remoteConfig); + const shouldPrependAtSymbol = pkg.startsWith('@') ? '@' : ''; + const pkgName = + shouldPrependAtSymbol + pkg.split(/[@#]/).filter(str => !!str)[0]; + + let communityConfig; + let remoteConfig; + + try { + communityConfig = await fetchCommunityPackageConfig(pkgName); + } catch (error) {} + + try { + remoteConfig = await fetchRemotePackageConfig(pkgName); + } catch (error) {} + + if (!communityConfig && !remoteConfig) { + throw new Error( + `Unable to locate package from the codeshift-community packages or as a standalone NPM package. +Make sure the package name ${pkgName} has been spelled correctly and exists before trying again.`, + ); + } + + const config: CodeshiftConfig = merge({}, communityConfig, remoteConfig); const rawTransformIds = pkg.split(/(?=[@#])/).filter(str => !!str); rawTransformIds.shift(); From 30bf0cfad3032e0a095d7261148ecd2630c9bd98 Mon Sep 17 00:00:00 2001 From: Daniel Del Core Date: Tue, 23 Nov 2021 19:39:23 +1100 Subject: [PATCH 6/6] fix tests --- .changeset/grumpy-swans-think.md | 5 ++ .changeset/silly-icons-whisper.md | 5 ++ .changeset/slimy-foxes-train.md | 5 ++ packages/cli/package.json | 1 + packages/cli/src/main.spec.ts | 116 ++++++++++++++++++++++++------ packages/cli/src/main.ts | 20 ++++-- 6 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 .changeset/grumpy-swans-think.md create mode 100644 .changeset/silly-icons-whisper.md create mode 100644 .changeset/slimy-foxes-train.md diff --git a/.changeset/grumpy-swans-think.md b/.changeset/grumpy-swans-think.md new file mode 100644 index 000000000..8d545a275 --- /dev/null +++ b/.changeset/grumpy-swans-think.md @@ -0,0 +1,5 @@ +--- +'@codeshift/validator': minor +--- + +Fundamentally simplifies and improves on how validation works. diff --git a/.changeset/silly-icons-whisper.md b/.changeset/silly-icons-whisper.md new file mode 100644 index 000000000..9ff93b573 --- /dev/null +++ b/.changeset/silly-icons-whisper.md @@ -0,0 +1,5 @@ +--- +'@codeshift/types': patch +--- + +Initial release diff --git a/.changeset/slimy-foxes-train.md b/.changeset/slimy-foxes-train.md new file mode 100644 index 000000000..b9a7fe781 --- /dev/null +++ b/.changeset/slimy-foxes-train.md @@ -0,0 +1,5 @@ +--- +'@codeshift/cli': minor +--- + +Codemods can now be sourced from standalone npm packages such as react as long as they provide a codeshift.config.js. This allows for greater flexibility for where codemods may be distributed diff --git a/packages/cli/package.json b/packages/cli/package.json index 533859365..9e4331b00 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,6 +22,7 @@ "fs-extra": "^9.1.0", "jscodeshift": "^0.12.0", "live-plugin-manager": "^0.15.1", + "lodash": "^4.17.21", "semver": "^7.3.5", "ts-node": "^9.1.1" } diff --git a/packages/cli/src/main.spec.ts b/packages/cli/src/main.spec.ts index 66a3c3607..581c0c3a1 100644 --- a/packages/cli/src/main.spec.ts +++ b/packages/cli/src/main.spec.ts @@ -6,29 +6,12 @@ jest.mock('jscodeshift/src/Runner', () => ({ // @ts-ignore import * as jscodeshift from 'jscodeshift/src/Runner'; import { PluginManager } from 'live-plugin-manager'; + import main from './main'; const mockPath = 'src/pages/home-page/'; describe('main', () => { - beforeEach(() => { - (PluginManager as jest.Mock).mockReturnValue({ - install: () => Promise.resolve(undefined), - require: (codemodName: string) => ({ - transforms: { - '18.0.0': `${codemodName}/path/to/18.js`, - '19.0.0': `${codemodName}/path/to/19.js`, - '20.0.0': `${codemodName}/path/to/20.js`, - }, - presets: { - 'update-formatting': `${codemodName}/path/to/update-formatting.js`, - 'update-imports': `${codemodName}/path/to/update-imports.js`, - }, - }), - uninstallAll: () => Promise.resolve(), - }); - }); - afterEach(() => { jest.resetAllMocks(); }); @@ -134,6 +117,26 @@ describe('main', () => { }); describe('when running transforms with the -p flag', () => { + beforeEach(() => { + (PluginManager as jest.Mock).mockImplementation(() => ({ + install: jest.fn().mockResolvedValue(undefined), + require: jest.fn().mockImplementation((codemodName: string) => { + if (!codemodName.startsWith('@codeshift')) { + throw new Error('Attempted to fetch codemod from npm'); + } + + return { + transforms: { + '18.0.0': `${codemodName}/path/to/18.js`, + '19.0.0': `${codemodName}/path/to/19.js`, + '20.0.0': `${codemodName}/path/to/20.js`, + }, + }; + }), + uninstallAll: jest.fn().mockResolvedValue(undefined), + })); + }); + it('should run package transform for single version', async () => { await main([mockPath], { packages: 'mylib@18.0.0', @@ -234,6 +237,7 @@ describe('main', () => { expect.any(Object), ); }); + it('should run multiple transforms of the same package', async () => { await main([mockPath], { packages: '@myscope/mylib@20.0.0@19.0.0', @@ -374,6 +378,25 @@ describe('main', () => { }); describe('when running presets with the -p flag', () => { + beforeEach(() => { + (PluginManager as jest.Mock).mockImplementation(() => ({ + install: jest.fn().mockResolvedValue(undefined), + require: jest.fn().mockImplementation((codemodName: string) => { + if (!codemodName.startsWith('@codeshift')) { + throw new Error('Attempted to fetch codemod from npm'); + } + + return { + presets: { + 'update-formatting': `${codemodName}/path/to/update-formatting.js`, + 'update-imports': `${codemodName}/path/to/update-imports.js`, + }, + }; + }), + uninstallAll: jest.fn().mockResolvedValue(undefined), + })); + }); + it('should run single preset', async () => { await main([mockPath], { packages: 'mylib#update-formatting', @@ -508,18 +531,71 @@ describe('main', () => { }); }); + describe('when running transforms from NPM with the -p flag', () => { + beforeEach(() => { + (PluginManager as jest.Mock).mockImplementation(() => ({ + install: jest.fn().mockResolvedValue(undefined), + require: jest.fn().mockImplementation((codemodName: string) => { + if (codemodName.startsWith('@codeshift')) { + throw new Error('Attempted to fetch codemod from community folder'); + } + + return { + transforms: { + '18.0.0': `${codemodName}/path/to/18.js`, + }, + presets: { + 'update-formatting': `${codemodName}/path/to/update-formatting.js`, + }, + }; + }), + uninstallAll: jest.fn().mockResolvedValue(undefined), + })); + }); + + it('should run package transform for single version', async () => { + await main([mockPath], { + packages: 'mylib@18.0.0', + parser: 'babel', + extensions: 'js', + }); + + expect(jscodeshift.run).toHaveBeenCalledTimes(1); + expect(jscodeshift.run).toHaveBeenCalledWith( + 'mylib/path/to/18.js', + expect.arrayContaining([mockPath]), + expect.anything(), + ); + }); + + it('should run single preset', async () => { + await main([mockPath], { + packages: 'mylib#update-formatting', + parser: 'babel', + extensions: 'js', + }); + + expect(jscodeshift.run).toHaveBeenCalledTimes(1); + expect(jscodeshift.run).toHaveBeenCalledWith( + 'mylib/path/to/update-formatting.js', + expect.arrayContaining([mockPath]), + expect.anything(), + ); + }); + }); + describe('when reading configs using non-cjs exports', () => { it('should read configs exported with export default', async () => { (PluginManager as jest.Mock).mockReturnValue({ install: () => Promise.resolve(undefined), // @ts-ignore - require: (codemodName: string) => ({ + require: jest.fn().mockImplementationOnce((codemodName: string) => ({ default: { transforms: { '18.0.0': `${codemodName}/path/to/18.js`, }, }, - }), + })), uninstallAll: () => Promise.resolve(), }); diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 2df443c84..e35ac4eeb 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -11,9 +11,10 @@ import { CodeshiftConfig } from '@codeshift/types'; import { Flags } from './types'; import { InvalidUserInputError } from './errors'; -const packageManager = new PluginManager(); - -async function fetchCommunityPackageConfig(packageName: string) { +async function fetchCommunityPackageConfig( + packageName: string, + packageManager: PluginManager, +) { const pkgName = packageName.replace('@', '').replace('/', '__'); const commPackageName = `@codeshift/mod-${pkgName}`; @@ -28,7 +29,10 @@ async function fetchCommunityPackageConfig(packageName: string) { return config; } -async function fetchRemotePackageConfig(packageName: string) { +async function fetchRemotePackageConfig( + packageName: string, + packageManager: PluginManager, +) { await packageManager.install(packageName); const pkg = packageManager.require(packageName); @@ -74,6 +78,7 @@ async function fetchRemotePackageConfig(packageName: string) { } export default async function main(paths: string[], flags: Flags) { + const packageManager = new PluginManager(); let transforms: string[] = []; if (!flags.transform && !flags.packages) { @@ -104,11 +109,14 @@ export default async function main(paths: string[], flags: Flags) { let remoteConfig; try { - communityConfig = await fetchCommunityPackageConfig(pkgName); + communityConfig = await fetchCommunityPackageConfig( + pkgName, + packageManager, + ); } catch (error) {} try { - remoteConfig = await fetchRemotePackageConfig(pkgName); + remoteConfig = await fetchRemotePackageConfig(pkgName, packageManager); } catch (error) {} if (!communityConfig && !remoteConfig) {