-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathapply.ts
177 lines (164 loc) · 6.43 KB
/
apply.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import chalk from 'chalk';
import type { Command } from 'commander';
import { Option } from 'commander';
import ignore from 'ignore';
import jscodeshift from 'jscodeshift';
import path from 'path';
import type { SharedCodemodOptions as Options } from '../src/utils/options.js';
import { logger } from '../utils/logger.js';
import type { CodemodConfig } from './config.js';
export function createApplyCommand(program: Command, codemods: CodemodConfig[]) {
const applyCommand = program.command('apply').description('apply the given codemod to the target file paths');
const commands = new Map<string, Command>();
// Add arguments that will be used for all codemods
for (const codemod of codemods) {
const command = applyCommand
.command(`${codemod.name}`)
.description(codemod.description)
.argument(
'<target-glob-pattern...>',
'Path to files or glob pattern. If using glob pattern, wrap in single quotes.'
)
.addOption(new Option('-d, --dry', 'dry run (no changes are made to files)').default(false))
.addOption(
new Option('-v, --verbose <level>', 'Show more information about the transform process')
.choices(['0', '1', '2'])
.default('0')
)
.addOption(
new Option(
'-l, --log-file [path]',
'Write logs to a file. If option is set but no path is provided, logs are written to ember-data-codemods.log'
)
)
.addOption(
new Option(
'-i, --ignore <ignore-glob-pattern...>',
'Ignores the given file or glob pattern. If using glob pattern, wrap in single quotes.'
)
)
.allowUnknownOption() // to passthrough jscodeshift options
.action(createApplyAction(codemod.name));
commands.set(codemod.name, command);
}
// Add arguments that are specific to the legacy-compat-builders codemod
const legacyCompatBuilders = commands.get('legacy-compat-builders');
if (!legacyCompatBuilders) {
throw new Error('No codemod found for: legacy-compat-builders');
}
legacyCompatBuilders
.addOption(
new Option(
'--store-names <store-name...>',
"Identifier name associated with the store. If overriding, it is recommended that you include 'store' in your list."
).default(['store'])
)
.addOption(
new Option(
'--method, --methods <method-name...>',
'Method name(s) to transform. By default, will transform all methods.'
).choices(['findAll', 'findRecord', 'query', 'queryRecord', 'saveRecord'])
);
}
function createApplyAction(transformName: string) {
return async (patterns: string[], options: Options & Record<string, unknown>) => {
logger.config(options);
const log = logger.for(transformName);
log.debug('Running with options:', { targetGlobPattern: patterns, ...options });
const ig = ignore().add(['**/*.d.ts', '**/node_modules/**/*', '**/dist/**/*', ...(options.ignore ?? [])]);
log.debug('Running for paths:', Bun.inspect(patterns));
if (options.dry) {
log.warn('Running in dry mode. No files will be modified.');
}
const { Codemods } = await import('../src/index.js');
if (!(transformName in Codemods)) {
throw new Error(`No codemod found for: ${transformName}`);
}
const transform = Codemods[transformName as keyof typeof Codemods];
/**
* | Result | How-to | Meaning |
* | :------ | :------ | :------- |
* | `errors` | `throw` | we attempted to transform but encountered an error |
* | `unmodified` | return `string` (unchanged) | we attempted to transform but it was unnecessary |
* | `skipped` | return `undefined` | we did not attempt to transform |
* | `ok` | return `string` (changed) | we successfully transformed |
*/
const result = {
matches: 0,
errors: 0,
unmodified: 0,
skipped: 0,
ok: 0,
};
const j = jscodeshift.withParser('ts');
for (const pattern of patterns) {
const glob = new Bun.Glob(pattern);
for await (const filepath of glob.scan('.')) {
if (ig.ignores(path.join(filepath))) {
log.warn('Skipping ignored file:', filepath);
result.skipped++;
continue;
}
log.debug('Transforming:', filepath);
result.matches++;
const file = Bun.file(filepath);
const originalSource = await file.text();
let transformedSource: string | undefined;
try {
transformedSource = transform(
{ source: originalSource, path: filepath },
{
j,
jscodeshift: j,
stats: (_name: string, _quantity?: number): void => {}, // unused
report: (_msg: string): void => {}, // unused
},
// SAFETY: This isn't safe TBH. YOLO
options as Parameters<typeof transform>[2]
);
} catch (error) {
result.errors++;
log.error({
filepath,
message: error instanceof Error ? error.message : 'Unknown error',
});
continue;
}
if (transformedSource === undefined) {
result.skipped++;
} else if (transformedSource === originalSource) {
result.unmodified++;
} else {
if (options.dry) {
log.info({
filepath,
message: 'Transformed source:\n\t' + transformedSource,
});
} else {
await Bun.write(filepath, transformedSource);
}
result.ok++;
}
}
}
if (result.matches === 0) {
log.warn('No files matched the provided glob pattern(s):', patterns);
}
if (result.errors > 0) {
log.info(chalk.red(`${result.errors} error(s). See logs above.`));
} else if (result.matches > 0) {
log.success('Zero errors! 🎉');
}
if (result.skipped > 0) {
log.info(
chalk.yellow(`${result.skipped} skipped file(s).`, chalk.gray('Transform did not run. See logs above.'))
);
}
if (result.unmodified > 0) {
log.info(`${result.unmodified} unmodified file(s).`, chalk.gray('Transform ran but no changes were made.'));
}
if (result.ok > 0) {
log.info(chalk.green(`${result.ok} transformed file(s).`));
}
};
}