Skip to content

Commit ad94808

Browse files
Merge pull request #58 from CodeshiftCommunity/remote-config
Remote config fetching
2 parents 506fa08 + 30bf0cf commit ad94808

File tree

14 files changed

+282
-69
lines changed

14 files changed

+282
-69
lines changed

.changeset/grumpy-swans-think.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codeshift/validator': minor
3+
---
4+
5+
Fundamentally simplifies and improves on how validation works.

.changeset/silly-icons-whisper.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codeshift/types': patch
3+
---
4+
5+
Initial release

.changeset/slimy-foxes-train.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+
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

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"fs-extra": "^9.1.0",
2323
"jscodeshift": "^0.12.0",
2424
"live-plugin-manager": "^0.15.1",
25+
"lodash": "^4.17.21",
2526
"semver": "^7.3.5",
2627
"ts-node": "^9.1.1"
2728
}

packages/cli/src/main.spec.ts

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,12 @@ jest.mock('jscodeshift/src/Runner', () => ({
66
// @ts-ignore
77
import * as jscodeshift from 'jscodeshift/src/Runner';
88
import { PluginManager } from 'live-plugin-manager';
9+
910
import main from './main';
1011

1112
const mockPath = 'src/pages/home-page/';
1213

1314
describe('main', () => {
14-
beforeEach(() => {
15-
(PluginManager as jest.Mock).mockReturnValue({
16-
install: () => Promise.resolve(undefined),
17-
require: (codemodName: string) => ({
18-
transforms: {
19-
'18.0.0': `${codemodName}/path/to/18.js`,
20-
'19.0.0': `${codemodName}/path/to/19.js`,
21-
'20.0.0': `${codemodName}/path/to/20.js`,
22-
},
23-
presets: {
24-
'update-formatting': `${codemodName}/path/to/update-formatting.js`,
25-
'update-imports': `${codemodName}/path/to/update-imports.js`,
26-
},
27-
}),
28-
uninstallAll: () => Promise.resolve(),
29-
});
30-
});
31-
3215
afterEach(() => {
3316
jest.resetAllMocks();
3417
});
@@ -134,6 +117,26 @@ describe('main', () => {
134117
});
135118

136119
describe('when running transforms with the -p flag', () => {
120+
beforeEach(() => {
121+
(PluginManager as jest.Mock).mockImplementation(() => ({
122+
install: jest.fn().mockResolvedValue(undefined),
123+
require: jest.fn().mockImplementation((codemodName: string) => {
124+
if (!codemodName.startsWith('@codeshift')) {
125+
throw new Error('Attempted to fetch codemod from npm');
126+
}
127+
128+
return {
129+
transforms: {
130+
'18.0.0': `${codemodName}/path/to/18.js`,
131+
'19.0.0': `${codemodName}/path/to/19.js`,
132+
'20.0.0': `${codemodName}/path/to/20.js`,
133+
},
134+
};
135+
}),
136+
uninstallAll: jest.fn().mockResolvedValue(undefined),
137+
}));
138+
});
139+
137140
it('should run package transform for single version', async () => {
138141
await main([mockPath], {
139142
packages: 'mylib@18.0.0',
@@ -234,6 +237,7 @@ describe('main', () => {
234237
expect.any(Object),
235238
);
236239
});
240+
237241
it('should run multiple transforms of the same package', async () => {
238242
await main([mockPath], {
239243
packages: '@myscope/mylib@20.0.0@19.0.0',
@@ -374,6 +378,25 @@ describe('main', () => {
374378
});
375379

376380
describe('when running presets with the -p flag', () => {
381+
beforeEach(() => {
382+
(PluginManager as jest.Mock).mockImplementation(() => ({
383+
install: jest.fn().mockResolvedValue(undefined),
384+
require: jest.fn().mockImplementation((codemodName: string) => {
385+
if (!codemodName.startsWith('@codeshift')) {
386+
throw new Error('Attempted to fetch codemod from npm');
387+
}
388+
389+
return {
390+
presets: {
391+
'update-formatting': `${codemodName}/path/to/update-formatting.js`,
392+
'update-imports': `${codemodName}/path/to/update-imports.js`,
393+
},
394+
};
395+
}),
396+
uninstallAll: jest.fn().mockResolvedValue(undefined),
397+
}));
398+
});
399+
377400
it('should run single preset', async () => {
378401
await main([mockPath], {
379402
packages: 'mylib#update-formatting',
@@ -508,18 +531,71 @@ describe('main', () => {
508531
});
509532
});
510533

534+
describe('when running transforms from NPM with the -p flag', () => {
535+
beforeEach(() => {
536+
(PluginManager as jest.Mock).mockImplementation(() => ({
537+
install: jest.fn().mockResolvedValue(undefined),
538+
require: jest.fn().mockImplementation((codemodName: string) => {
539+
if (codemodName.startsWith('@codeshift')) {
540+
throw new Error('Attempted to fetch codemod from community folder');
541+
}
542+
543+
return {
544+
transforms: {
545+
'18.0.0': `${codemodName}/path/to/18.js`,
546+
},
547+
presets: {
548+
'update-formatting': `${codemodName}/path/to/update-formatting.js`,
549+
},
550+
};
551+
}),
552+
uninstallAll: jest.fn().mockResolvedValue(undefined),
553+
}));
554+
});
555+
556+
it('should run package transform for single version', async () => {
557+
await main([mockPath], {
558+
packages: 'mylib@18.0.0',
559+
parser: 'babel',
560+
extensions: 'js',
561+
});
562+
563+
expect(jscodeshift.run).toHaveBeenCalledTimes(1);
564+
expect(jscodeshift.run).toHaveBeenCalledWith(
565+
'mylib/path/to/18.js',
566+
expect.arrayContaining([mockPath]),
567+
expect.anything(),
568+
);
569+
});
570+
571+
it('should run single preset', async () => {
572+
await main([mockPath], {
573+
packages: 'mylib#update-formatting',
574+
parser: 'babel',
575+
extensions: 'js',
576+
});
577+
578+
expect(jscodeshift.run).toHaveBeenCalledTimes(1);
579+
expect(jscodeshift.run).toHaveBeenCalledWith(
580+
'mylib/path/to/update-formatting.js',
581+
expect.arrayContaining([mockPath]),
582+
expect.anything(),
583+
);
584+
});
585+
});
586+
511587
describe('when reading configs using non-cjs exports', () => {
512588
it('should read configs exported with export default', async () => {
513589
(PluginManager as jest.Mock).mockReturnValue({
514590
install: () => Promise.resolve(undefined),
515591
// @ts-ignore
516-
require: (codemodName: string) => ({
592+
require: jest.fn().mockImplementationOnce((codemodName: string) => ({
517593
default: {
518594
transforms: {
519595
'18.0.0': `${codemodName}/path/to/18.js`,
520596
},
521597
},
522-
}),
598+
})),
523599
uninstallAll: () => Promise.resolve(),
524600
});
525601

packages/cli/src/main.ts

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,84 @@
11
import semver from 'semver';
22
import chalk from 'chalk';
3+
import path from 'path';
34
import { PluginManager } from 'live-plugin-manager';
5+
import merge from 'lodash/merge';
46
// @ts-ignore Run transform(s) on path https://github.com/facebook/jscodeshift/issues/398
57
import * as jscodeshift from 'jscodeshift/src/Runner';
8+
import { isValidConfig } from '@codeshift/validator';
9+
import { CodeshiftConfig } from '@codeshift/types';
610

711
import { Flags } from './types';
812
import { InvalidUserInputError } from './errors';
913

14+
async function fetchCommunityPackageConfig(
15+
packageName: string,
16+
packageManager: PluginManager,
17+
) {
18+
const pkgName = packageName.replace('@', '').replace('/', '__');
19+
const commPackageName = `@codeshift/mod-${pkgName}`;
20+
21+
await packageManager.install(commPackageName);
22+
const pkg = packageManager.require(commPackageName);
23+
const config: CodeshiftConfig = pkg.default ? pkg.default : pkg;
24+
25+
if (!isValidConfig(config)) {
26+
throw new Error(`Invalid config found in module ${commPackageName}`);
27+
}
28+
29+
return config;
30+
}
31+
32+
async function fetchRemotePackageConfig(
33+
packageName: string,
34+
packageManager: PluginManager,
35+
) {
36+
await packageManager.install(packageName);
37+
const pkg = packageManager.require(packageName);
38+
39+
if (pkg) {
40+
const config: CodeshiftConfig = pkg.default ? pkg.default : pkg;
41+
42+
if (config && isValidConfig(config)) {
43+
// Found a config at the main entry-point
44+
return config;
45+
}
46+
}
47+
48+
const info = packageManager.getInfo(packageName);
49+
50+
if (info) {
51+
let config: CodeshiftConfig | undefined;
52+
53+
[
54+
path.join(info?.location, 'codeshift.config.js'),
55+
path.join(info?.location, 'codeshift.config.ts'),
56+
path.join(info?.location, 'src', 'codeshift.config.js'),
57+
path.join(info?.location, 'src', 'codeshift.config.ts'),
58+
path.join(info?.location, 'codemods', 'codeshift.config.js'),
59+
path.join(info?.location, 'codemods', 'codeshift.config.ts'),
60+
].forEach(searchPath => {
61+
try {
62+
// eslint-disable-next-line @typescript-eslint/no-var-requires
63+
const pkg = require(searchPath);
64+
const searchConfig: CodeshiftConfig = pkg.default ? pkg.default : pkg;
65+
66+
if (isValidConfig(searchConfig)) {
67+
config = searchConfig;
68+
}
69+
} catch (e) {}
70+
});
71+
72+
if (config) return config;
73+
}
74+
75+
throw new Error(
76+
`Unable to locate a valid codeshift.config in package ${packageName}`,
77+
);
78+
}
79+
1080
export default async function main(paths: string[], flags: Flags) {
81+
const packageManager = new PluginManager();
1182
let transforms: string[] = [];
1283

1384
if (!flags.transform && !flags.packages) {
@@ -26,24 +97,36 @@ export default async function main(paths: string[], flags: Flags) {
2697
transforms.push(flags.transform);
2798
}
2899

29-
const packageManager = new PluginManager();
30-
31100
if (flags.packages) {
32101
const pkgs = flags.packages.split(',').filter(pkg => !!pkg);
33102

34103
for (const pkg of pkgs) {
35-
const pkgName = pkg
36-
.split(/[@#]/)
37-
.filter(str => !!str)[0]
38-
.replace('/', '__');
39-
const packageName = `@codeshift/mod-${pkgName}`;
40-
41-
await packageManager.install(packageName);
42-
const codeshiftPackage = packageManager.require(packageName);
43-
44-
const config = codeshiftPackage.default
45-
? codeshiftPackage.default
46-
: codeshiftPackage;
104+
const shouldPrependAtSymbol = pkg.startsWith('@') ? '@' : '';
105+
const pkgName =
106+
shouldPrependAtSymbol + pkg.split(/[@#]/).filter(str => !!str)[0];
107+
108+
let communityConfig;
109+
let remoteConfig;
110+
111+
try {
112+
communityConfig = await fetchCommunityPackageConfig(
113+
pkgName,
114+
packageManager,
115+
);
116+
} catch (error) {}
117+
118+
try {
119+
remoteConfig = await fetchRemotePackageConfig(pkgName, packageManager);
120+
} catch (error) {}
121+
122+
if (!communityConfig && !remoteConfig) {
123+
throw new Error(
124+
`Unable to locate package from the codeshift-community packages or as a standalone NPM package.
125+
Make sure the package name ${pkgName} has been spelled correctly and exists before trying again.`,
126+
);
127+
}
128+
129+
const config: CodeshiftConfig = merge({}, communityConfig, remoteConfig);
47130

48131
const rawTransformIds = pkg.split(/(?=[@#])/).filter(str => !!str);
49132
rawTransformIds.shift();

packages/cli/src/validate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { isValidConfig, isValidPackageJson } from '@codeshift/validator';
1+
import { isValidConfigAtPath, isValidPackageJson } from '@codeshift/validator';
22

33
export default async function validate(targetPath: string = '.') {
44
try {
5-
await isValidConfig(targetPath);
5+
await isValidConfigAtPath(targetPath);
66
await isValidPackageJson(targetPath);
77
} catch (error) {
88
console.warn(error);

packages/types/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @codeshift/types

packages/types/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@codeshift/types",
3+
"version": "0.0.1",
4+
"main": "dist/codeshift-types.cjs.js",
5+
"module": "dist/codeshift-types.esm.js",
6+
"types": "dist/codeshift-types.cjs.d.ts",
7+
"license": "MIT",
8+
"repository": "https://github.com/CodeshiftCommunity/CodeshiftCommunity/tree/master/packages/types"
9+
}

packages/types/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface CodeshiftConfig {
2+
target?: string[];
3+
maintainers?: string[];
4+
description?: string;
5+
transforms?: Record<string, string>;
6+
presets?: Record<string, string>;
7+
}

0 commit comments

Comments
 (0)