Skip to content

Commit 6d624ad

Browse files
committed
Monorepo niceties
1 parent 2d37de4 commit 6d624ad

File tree

6 files changed

+165
-54
lines changed

6 files changed

+165
-54
lines changed

.changeset/shaggy-news-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codeshift/cli': minor
3+
---
4+
5+
CLI will now list all matching codeshift.config files based on your workspace configuration (To support monorepos)

.changeset/sour-dragons-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codeshift/fetcher': minor
3+
---
4+
5+
Adds fetchConfigs function, which will return multiple config files if the glob matches them

packages/cli/src/main.ts

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
import path from 'path';
2+
import fs from 'fs-extra';
23
import semver from 'semver';
34
import chalk from 'chalk';
45
import findUp from 'find-up';
56
import inquirer from 'inquirer';
67

7-
import { fetchConfigAtPath } from '@codeshift/fetcher';
8+
import { CodeshiftConfig } from '@codeshift/types';
9+
import { fetchConfigAtPath, fetchConfigs } from '@codeshift/fetcher';
810
import { PluginManager } from 'live-plugin-manager';
911
// @ts-ignore Run transform(s) on path https://github.com/facebook/jscodeshift/issues/398
1012
import * as jscodeshift from 'jscodeshift/src/Runner';
1113

1214
import { Flags } from './types';
1315
import { InvalidUserInputError } from './errors';
1416
import { fetchPackageConfig } from './fetch-package';
15-
import { getTransformPrompt } from './prompt';
17+
import { getConfigPrompt, getMultiConfigPrompt } from './prompt';
1618

1719
export default async function main(paths: string[], flags: Flags) {
20+
if (paths.length === 0) {
21+
throw new InvalidUserInputError(
22+
'No path provided, please specify which files your codemod should modify',
23+
);
24+
}
25+
1826
const packageManager = new PluginManager({
1927
pluginsPath: path.join(__dirname, 'node_modules'),
2028
});
@@ -24,47 +32,94 @@ export default async function main(paths: string[], flags: Flags) {
2432
if (!flags.transform && !flags.packages) {
2533
console.log(
2634
chalk.green(
27-
'No transforms specified, attempting to find local codeshift.config file',
35+
'No transforms specified, attempting to find local codeshift.config file(s)',
2836
),
2937
);
3038

31-
const configFilePath = await findUp([
32-
'codeshift.config.js',
33-
'codeshift.config.ts',
34-
'codeshift.config.tsx',
35-
'src/codeshift.config.js',
36-
'src/codeshift.config.ts',
37-
'src/codeshift.config.tsx',
38-
'codemods/codeshift.config.js',
39-
'codemods/codeshift.config.ts',
40-
'codemods/codeshift.config.tsx',
41-
]);
42-
43-
if (!configFilePath) {
44-
throw new InvalidUserInputError(
45-
'No transform provided, please specify a transform with either the --transform or --packages flags',
46-
);
39+
/**
40+
* Attempt to locate a root package json with a workspaces config.
41+
* If found, show a prompt with all available codemods
42+
*/
43+
let rootPackageJson: any;
44+
const packageJsonPath = await findUp('package.json');
45+
46+
if (packageJsonPath) {
47+
const packageJsonRaw = await fs.readFile(packageJsonPath, 'utf8');
48+
rootPackageJson = JSON.parse(packageJsonRaw);
4749
}
4850

49-
console.log(
50-
chalk.green('Found local codeshift.config file at:'),
51-
configFilePath,
52-
);
51+
if (rootPackageJson && rootPackageJson.workspaces) {
52+
const configs = await (rootPackageJson.workspaces as any[]).reduce<
53+
Promise<{ filePath: string; config: CodeshiftConfig }[]>
54+
>(async (accum, filePath) => {
55+
const configs = await fetchConfigs(filePath);
56+
if (!configs.length) return accum;
57+
const results = await accum;
58+
return [...results, ...configs];
59+
}, Promise.resolve([]));
60+
61+
const answers = await inquirer.prompt([getMultiConfigPrompt(configs)]);
62+
const selectedConfig = configs.find(
63+
({ filePath }) => answers.codemod.filePath === filePath,
64+
);
5365

54-
const config = await fetchConfigAtPath(configFilePath);
55-
const answers = await inquirer.prompt([getTransformPrompt(config)]);
66+
if (!selectedConfig) {
67+
throw new Error(
68+
`Unable to locate config at: ${answers.codemod.filePath}`,
69+
);
70+
}
5671

57-
if (config.transforms && config.transforms[answers.transform]) {
58-
transforms.push(config.transforms[answers.transform]);
59-
} else if (config.presets && config.presets[answers.transform]) {
60-
transforms.push(config.presets[answers.transform]);
61-
}
62-
}
72+
if (
73+
selectedConfig.config.transforms &&
74+
selectedConfig.config.transforms[answers.codemod.selection]
75+
) {
76+
transforms.push(
77+
selectedConfig.config.transforms[answers.codemod.selection],
78+
);
79+
} else if (
80+
selectedConfig.config.presets &&
81+
selectedConfig.config.presets[answers.codemod.selection]
82+
) {
83+
transforms.push(
84+
selectedConfig.config.presets[answers.codemod.selection],
85+
);
86+
}
87+
} else {
88+
/**
89+
* Otherwise, locate any config files in parent directories
90+
*/
91+
const configFilePath = await findUp([
92+
'codeshift.config.js',
93+
'codeshift.config.ts',
94+
'codeshift.config.tsx',
95+
'src/codeshift.config.js',
96+
'src/codeshift.config.ts',
97+
'src/codeshift.config.tsx',
98+
'codemods/codeshift.config.js',
99+
'codemods/codeshift.config.ts',
100+
'codemods/codeshift.config.tsx',
101+
]);
102+
103+
if (!configFilePath) {
104+
throw new InvalidUserInputError(
105+
'No transform provided, please specify a transform with either the --transform or --packages flags',
106+
);
107+
}
63108

64-
if (paths.length === 0) {
65-
throw new InvalidUserInputError(
66-
'No path provided, please specify which files your codemod should modify',
67-
);
109+
console.log(
110+
chalk.green('Found local codeshift.config file at:'),
111+
configFilePath,
112+
);
113+
114+
const config = await fetchConfigAtPath(configFilePath);
115+
const answers = await inquirer.prompt([getConfigPrompt(config)]);
116+
117+
if (config.transforms && config.transforms[answers.codemod]) {
118+
transforms.push(config.transforms[answers.codemod]);
119+
} else if (config.presets && config.presets[answers.codemod]) {
120+
transforms.push(config.presets[answers.codemod]);
121+
}
122+
}
68123
}
69124

70125
if (flags.transform) {
@@ -125,7 +180,7 @@ export default async function main(paths: string[], flags: Flags) {
125180
});
126181

127182
if (presetIds.length === 0 && transformIds.length === 0) {
128-
const res = await inquirer.prompt([getTransformPrompt(config)]);
183+
const res = await inquirer.prompt([getConfigPrompt(config)]);
129184

130185
if (semver.valid(semver.coerce(res.transform))) {
131186
transformIds.push(res.transform);

packages/cli/src/prompt.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import inquirer from 'inquirer';
22

33
import { CodeshiftConfig } from '@codeshift/types';
44

5-
export const getTransformPrompt = (config: CodeshiftConfig) => {
5+
export const getConfigPrompt = (config: CodeshiftConfig) => {
66
const transforms = Object.keys(config.transforms || {});
77
const presets = Object.keys(config.presets || {});
88

@@ -15,8 +15,44 @@ export const getTransformPrompt = (config: CodeshiftConfig) => {
1515

1616
return {
1717
type: 'list',
18-
name: 'transform',
19-
message: 'Which transform would you like to run?',
18+
name: 'codemod',
19+
message: 'Which codemod would you like to run?',
20+
choices,
21+
};
22+
};
23+
24+
export const getMultiConfigPrompt = (
25+
configs: { filePath: string; config: CodeshiftConfig }[],
26+
) => {
27+
const choices = configs.reduce<any[]>((accum, { filePath, config }) => {
28+
function mapToConfig(codemods: Record<string, string> = {}) {
29+
return Object.keys(codemods).map(codemodKey => ({
30+
name: codemodKey,
31+
value: {
32+
filePath,
33+
selection: codemodKey,
34+
},
35+
short: `${codemodKey} from ${filePath}`,
36+
}));
37+
}
38+
39+
const transforms = mapToConfig(config.transforms);
40+
const presets = mapToConfig(config.presets);
41+
42+
return [
43+
...accum,
44+
new inquirer.Separator(filePath),
45+
transforms.length ? new inquirer.Separator('Transforms') : undefined,
46+
...transforms,
47+
presets.length ? new inquirer.Separator('Presets') : undefined,
48+
...presets,
49+
].filter(item => item !== undefined);
50+
}, []);
51+
52+
return {
53+
type: 'list',
54+
name: 'codemod',
55+
message: 'Which codemod would you like to run?',
2056
choices,
2157
};
2258
};

packages/fetcher/src/index.spec.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ describe('fetcher', () => {
1919
let mockMatchedPaths: string[] = [];
2020

2121
beforeEach(() => {
22-
mockMatchedPaths = [
23-
path.join(mockBasePath, 'codeshift.config.js'),
24-
path.join(mockBasePath, 'src', 'codeshift.config.ts'),
25-
path.join(mockBasePath, 'codemods', 'codeshift.config.tsx'),
26-
];
22+
mockMatchedPaths = [path.join(mockBasePath, 'codeshift.config.js')];
2723

2824
(globby as unknown as jest.Mock).mockImplementation(() =>
2925
Promise.resolve(mockMatchedPaths),
@@ -40,10 +36,7 @@ describe('fetcher', () => {
4036
it('fetches config with default export', async () => {
4137
jest.mock(
4238
`${__dirname}/path/to/codeshift.config.js`,
43-
() => ({
44-
__esModule: true,
45-
default: mockConfig,
46-
}),
39+
() => ({ __esModule: true, default: mockConfig }),
4740
{ virtual: true },
4841
);
4942

@@ -65,10 +58,13 @@ describe('fetcher', () => {
6558
it('fetches first matched config when multiple are found', async () => {
6659
jest.mock(
6760
`${__dirname}/path/to/src/codeshift.config.ts`,
68-
() => ({
69-
__esModule: true,
70-
default: mockConfig,
71-
}),
61+
() => ({ __esModule: true, default: mockConfig }),
62+
{ virtual: true },
63+
);
64+
65+
jest.mock(
66+
`${__dirname}/path/to/codemods/codeshift.config.tsx`,
67+
() => ({ __esModule: true, default: mockConfig }),
7268
{ virtual: true },
7369
);
7470

packages/fetcher/src/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ function requireConfig(filePath: string, resolvedPath: string) {
1515
const pkg = require(resolvedPath);
1616
return resolveConfigExport(pkg);
1717
} catch (e) {
18+
console.log(resolvedPath, e);
19+
1820
throw new Error(
1921
`Found config file "${filePath}" but was unable to parse it. This can be caused when transform or preset paths are incorrect.`,
2022
);
@@ -24,22 +26,34 @@ function requireConfig(filePath: string, resolvedPath: string) {
2426
export async function fetchConfig(
2527
filePath: string,
2628
): Promise<CodeshiftConfig | undefined> {
29+
const configs = await fetchConfigs(filePath);
30+
return configs[0]?.config || undefined;
31+
}
32+
33+
export async function fetchConfigs(
34+
filePath: string,
35+
): Promise<{ filePath: string; config: CodeshiftConfig }[]> {
2736
const matchedPaths = await globby([
2837
path.join(filePath, 'codeshift.config.(js|ts|tsx)'),
2938
path.join(filePath, 'src', 'codeshift.config.(js|ts|tsx)'),
3039
path.join(filePath, 'codemods', 'codeshift.config.(js|ts|tsx)'),
3140
]);
3241

42+
const configs = [];
43+
3344
for (const matchedPath of matchedPaths) {
3445
const resolvedMatchedPath = path.resolve(matchedPath);
3546
const exists = fs.existsSync(resolvedMatchedPath);
3647

3748
if (!exists) continue;
3849

39-
return requireConfig(matchedPath, resolvedMatchedPath);
50+
configs.push({
51+
filePath: matchedPath,
52+
config: requireConfig(matchedPath, resolvedMatchedPath),
53+
});
4054
}
4155

42-
return undefined;
56+
return configs;
4357
}
4458

4559
export async function fetchConfigAtPath(

0 commit comments

Comments
 (0)