Skip to content

Commit e3670ae

Browse files
committed
refactors main logic and splits out fetchers and resolvers
1 parent be4a5ae commit e3670ae

File tree

7 files changed

+365
-201
lines changed

7 files changed

+365
-201
lines changed

packages/cli/src/fetchers/app.ts

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import fs from 'fs-extra';
2+
import ora from 'ora';
3+
import chalk from 'chalk';
4+
import path from 'path';
5+
6+
interface MarketPlaceEntry {
7+
id: string;
8+
slug: string;
9+
description: string;
10+
status: string;
11+
tags: string[];
12+
type: 'OPTIMIZATION' | 'MIGRATION';
13+
transformId: string;
14+
transform: Transform;
15+
}
16+
17+
interface Transform {
18+
author: { image: string; name: string };
19+
id: string;
20+
name: string;
21+
sources: Source[];
22+
}
23+
24+
interface Source {
25+
id: string;
26+
name: string;
27+
code: string;
28+
}
29+
30+
function writeSourceFiles(slug: string, transform: Transform, dir: string) {
31+
transform.sources.forEach(source => {
32+
const filePath = path.join(dir, slug, source.name);
33+
fs.outputFileSync(filePath, source.code);
34+
});
35+
}
36+
37+
export async function fetchHmPkg(slug: string, dir: string) {
38+
const spinner = ora(
39+
`${chalk.green('Attempting to download Hypermod transform:')} ${slug}`,
40+
).start();
41+
42+
let marketplaceEntry: MarketPlaceEntry;
43+
44+
try {
45+
// @ts-expect-error
46+
// transform = await fetch(`https://www.hypermod.io/api/cli/transforms/${slug}`).then(
47+
marketplaceEntry = await fetch(
48+
`http://localhost:3000/api/cli/transforms/${slug.replace('hm-', '')}`,
49+
).then((res: any) => {
50+
if (res.status === 404) {
51+
throw new Error(`Transform not found: ${slug}`);
52+
}
53+
54+
if (!res.ok) {
55+
throw new Error(`Error fetching transform: ${res.statusText}`);
56+
}
57+
58+
return res.json();
59+
});
60+
61+
spinner.succeed(
62+
`${chalk.green(
63+
'Found Hypermod transform:',
64+
)} https://www.hypermod.io/explore/${slug}`,
65+
);
66+
} catch (error) {
67+
spinner.fail(`${chalk.red('Unable to fetch Hypermod transform:')} ${slug}`);
68+
throw new Error(`Unable to locate Hypermod transform: ${slug}\n${error}`);
69+
}
70+
71+
writeSourceFiles(slug, marketplaceEntry.transform, dir);
72+
73+
return marketplaceEntry;
74+
}

packages/cli/src/utils/fetch-package.ts renamed to packages/cli/src/fetchers/npm.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
} from '@hypermod/fetcher';
1010
import { isValidConfig } from '@hypermod/validator';
1111

12-
import { getHypermodPackageName } from './package-names';
12+
import { getHypermodPackageName } from '../utils/package-names';
1313

14-
export async function fetchPackages(
14+
export async function fetchNpmPkg(
1515
packageName: string,
1616
packageManager: ModuleLoader,
1717
) {

packages/cli/src/list.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import chalk from 'chalk';
22
import { PluginManager } from 'live-plugin-manager';
33

4-
import { fetchPackages } from './utils/fetch-package';
4+
import { fetchNpmPkg } from './fetchers/npm';
55
import { getHypermodPackageName } from './utils/package-names';
66

77
export default async function list(packages: string[]) {
@@ -10,7 +10,7 @@ export default async function list(packages: string[]) {
1010

1111
for (const packageName of packages) {
1212
try {
13-
const { community, remote } = await fetchPackages(
13+
const { community, remote } = await fetchNpmPkg(
1414
packageName,
1515
packageManager,
1616
);

packages/cli/src/main.ts

+33-197
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
import path from 'path';
2-
import semver from 'semver';
32
import chalk from 'chalk';
4-
import findUp from 'find-up';
5-
import inquirer from 'inquirer';
63
import { PluginManager, PluginManagerOptions } from 'live-plugin-manager';
74

85
import * as core from '@hypermod/core';
9-
import {
10-
type ModuleLoader as MdlLoader,
11-
fetchConfigAtPath,
12-
} from '@hypermod/fetcher';
6+
import { type ModuleLoader as MdlLoader } from '@hypermod/fetcher';
137

148
import { InvalidUserInputError } from './errors';
15-
import { fetchPackages } from './utils/fetch-package';
16-
import { mergeConfigs } from './utils/merge-configs';
17-
import { fetchConfigsForWorkspaces, getPackageJson } from './utils/file-system';
189
import ModuleLoader from './utils/module-loader';
19-
import { getConfigPrompt, getMultiConfigPrompt } from './prompt';
10+
import { resolveLocalTransforms } from './resolvers/local';
11+
import { resolveNpmTransforms } from './resolvers/npm';
12+
import { resolveAppTransforms } from './resolvers/app';
13+
14+
const CLI_DIR = path.join(__dirname, '..', 'node_modules');
2015

2116
export default async function main(
2217
paths: string[],
@@ -29,7 +24,7 @@ export default async function main(
2924
}
3025

3126
const pluginManagerConfig: Partial<PluginManagerOptions> = {
32-
pluginsPath: path.join(__dirname, '..', 'node_modules'),
27+
pluginsPath: CLI_DIR,
3328
};
3429

3530
// If a registry is provided in the CLI flags, use it for the pluginManagers configuration.
@@ -53,122 +48,22 @@ export default async function main(
5348

5449
let transforms: string[] = [];
5550

51+
/**
52+
* If no transforms are provided, attempt to find codemods in the local hypermod.config file
53+
* or in the local package.json file.
54+
*/
5655
if (!flags.transform && !flags.packages) {
5756
console.log(
5857
chalk.green(
5958
'No transforms specified, attempting to find local hypermod.config file(s)',
6059
),
6160
);
6261

63-
/**
64-
* Attempt to locate a root package.json with a workspaces config.
65-
* If found, show a prompt with all available codemods
66-
*/
67-
const localPackageJson = await getPackageJson();
68-
69-
if (localPackageJson && localPackageJson.workspaces) {
70-
const configs = await fetchConfigsForWorkspaces(
71-
localPackageJson.workspaces,
72-
);
73-
const answers = await inquirer.prompt([getMultiConfigPrompt(configs)]);
74-
const selectedConfig = configs.find(
75-
({ filePath }) => answers.codemod.filePath === filePath,
76-
);
77-
78-
if (!selectedConfig) {
79-
throw new Error(
80-
`Unable to locate config at: ${answers.codemod.filePath}`,
81-
);
82-
}
83-
84-
if (
85-
selectedConfig.config.transforms &&
86-
selectedConfig.config.transforms[answers.codemod.selection]
87-
) {
88-
if (flags.sequence) {
89-
Object.entries(
90-
selectedConfig.config.transforms as Record<string, string>,
91-
)
92-
.filter(([key]) =>
93-
semver.satisfies(key, `>=${answers.codemod.selection}`),
94-
)
95-
.forEach(([, path]) => transforms.push(path));
96-
} else {
97-
transforms.push(
98-
selectedConfig.config.transforms[answers.codemod.selection],
99-
);
100-
}
101-
} else if (
102-
selectedConfig.config.presets &&
103-
selectedConfig.config.presets[answers.codemod.selection]
104-
) {
105-
transforms.push(
106-
selectedConfig.config.presets[answers.codemod.selection],
107-
);
108-
}
109-
} else {
110-
/**
111-
* Otherwise, locate any config files in parent directories
112-
*/
113-
const configFilePath = await findUp([
114-
'hypermod.config.js',
115-
'hypermod.config.mjs',
116-
'hypermod.config.cjs',
117-
'hypermod.config.ts',
118-
'hypermod.config.tsx',
119-
'src/hypermod.config.js',
120-
'src/hypermod.config.mjs',
121-
'src/hypermod.config.cjs',
122-
'src/hypermod.config.ts',
123-
'src/hypermod.config.tsx',
124-
'codemods/hypermod.config.js',
125-
'codemods/hypermod.config.mjs',
126-
'codemods/hypermod.config.cjs',
127-
'codemods/hypermod.config.ts',
128-
'codemods/hypermod.config.tsx',
129-
'codeshift.config.js',
130-
'codeshift.config.mjs',
131-
'codeshift.config.cjs',
132-
'codeshift.config.ts',
133-
'codeshift.config.tsx',
134-
'src/codeshift.config.js',
135-
'src/codeshift.config.mjs',
136-
'src/codeshift.config.cjs',
137-
'src/codeshift.config.ts',
138-
'src/codeshift.config.tsx',
139-
'codemods/codeshift.config.js',
140-
'codemods/codeshift.config.mjs',
141-
'codemods/codeshift.config.cjs',
142-
'codemods/codeshift.config.ts',
143-
'codemods/codeshift.config.tsx',
144-
]);
145-
146-
if (!configFilePath) {
147-
throw new InvalidUserInputError(
148-
'No transform provided, please specify a transform with either the --transform or --packages flags',
149-
);
150-
}
151-
152-
console.log(
153-
chalk.green('Found local hypermod.config file at:'),
154-
configFilePath,
155-
);
156-
157-
const config = await fetchConfigAtPath(configFilePath);
158-
const answers = await inquirer.prompt([getConfigPrompt(config)]);
159-
160-
if (config.transforms && config.transforms[answers.codemod]) {
161-
Object.entries(config.transforms)
162-
.filter(([key]) => semver.satisfies(key, `>=${answers.codemod}`))
163-
.forEach(([, codemod]) =>
164-
transforms.push(`${configFilePath}@${codemod}`),
165-
);
166-
} else if (config.presets && config.presets[answers.codemod]) {
167-
transforms.push(`${configFilePath}#${answers.codemod}`);
168-
}
169-
}
62+
const localTransforms = await resolveLocalTransforms(flags);
63+
transforms.push(...localTransforms);
17064
}
17165

66+
// If a direct path to a transform is provided, use it
17267
if (flags.transform) {
17368
if (flags.transform.includes(',')) {
17469
flags.transform.split(',').forEach(t => transforms.push(t.trim()));
@@ -177,91 +72,32 @@ export default async function main(
17772
}
17873
}
17974

75+
// If a package name is provided, fetch the community and remote configs
76+
// and merge them to get the transforms
18077
if (flags.packages) {
181-
const pkgs = flags.packages.split(',').filter(pkg => !!pkg);
78+
const pkgs = flags.packages!.split(',').filter(pkg => !!pkg);
18279

18380
for (const pkg of pkgs) {
184-
const shouldPrependAtSymbol = pkg.startsWith('@') ? '@' : '';
185-
const pkgName =
186-
shouldPrependAtSymbol + pkg.split(/[@#]/).filter(str => !!str)[0];
187-
188-
const rawTransformIds = pkg.split(/(?=[@#])/).filter(str => !!str);
189-
rawTransformIds.shift();
190-
191-
const transformIds = rawTransformIds
192-
.filter(id => id.startsWith('@'))
193-
.map(id => id.substring(1))
194-
.sort((idA, idB) => {
195-
if (semver.lt(idA, idB)) return -1;
196-
if (semver.gt(idA, idB)) return 1;
197-
return 0;
198-
});
199-
200-
const presetIds = rawTransformIds
201-
.filter(id => id.startsWith('#'))
202-
.map(id => id.substring(1));
81+
/**
82+
* If the package name starts with "hm-", it is a Hypermod transform.
83+
* We need to fetch the transform from the Hypermod API and add it to the transforms array.
84+
*/
85+
if (pkg.startsWith('hm-')) {
86+
const hmTransform = await resolveAppTransforms(pkg, CLI_DIR);
87+
transforms.push(hmTransform);
88+
continue;
89+
}
20390

204-
const { community, remote } = await fetchPackages(
205-
pkgName,
91+
/**
92+
* We need to fetch the transform from the npm registry and add it to the transforms array.
93+
*/
94+
const npmTransforms = await resolveNpmTransforms(
95+
flags,
96+
pkg,
20697
packageManager,
20798
);
20899

209-
const config = mergeConfigs(community, remote);
210-
211-
// Validate transforms/presets
212-
transformIds.forEach(id => {
213-
if (!semver.valid(semver.coerce(id.substring(1)))) {
214-
throw new InvalidUserInputError(
215-
`Invalid version provided to the --packages flag. Unable to resolve version "${id}" for package "${pkgName}". Please try: "[scope]/[package]@[version]" for example @mylib/[email protected]`,
216-
);
217-
}
218-
219-
if (!config.transforms || !config.transforms[id]) {
220-
throw new InvalidUserInputError(
221-
`Invalid version provided to the --packages flag. Unable to resolve version "${id}" for package "${pkgName}"`,
222-
);
223-
}
224-
});
225-
226-
presetIds.forEach(id => {
227-
if (!config.presets || !config.presets[id]) {
228-
throw new InvalidUserInputError(
229-
`Invalid preset provided to the --packages flag. Unable to resolve preset "${id}" for package "${pkgName}"`,
230-
);
231-
}
232-
});
233-
234-
if (presetIds.length === 0 && transformIds.length === 0) {
235-
const res: { codemod: string } = await inquirer.prompt([
236-
getConfigPrompt(config),
237-
]);
238-
239-
if (semver.valid(semver.coerce(res.codemod))) {
240-
transformIds.push(res.codemod);
241-
} else {
242-
presetIds.push(res.codemod);
243-
}
244-
}
245-
246-
// Get transform file paths
247-
if (config.transforms) {
248-
if (flags.sequence) {
249-
Object.entries(config.transforms)
250-
.filter(([key]) => semver.satisfies(key, `>=${transformIds[0]}`))
251-
.forEach(([, path]) => transforms.push(path));
252-
} else {
253-
Object.entries(config.transforms)
254-
.filter(([id]) => transformIds.includes(id))
255-
.forEach(([, path]) => transforms.push(path));
256-
}
257-
}
258-
259-
// Get preset file paths
260-
if (config.presets) {
261-
Object.entries(config.presets)
262-
.filter(([id]) => presetIds.includes(id))
263-
.forEach(([, path]) => transforms.push(path));
264-
}
100+
transforms.push(...npmTransforms);
265101
}
266102
}
267103

0 commit comments

Comments
 (0)