@openrewrite/rewrite 8.68.0-20251204-145030 → 8.68.0-20251204-171340
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli-utils.d.ts +69 -0
- package/dist/cli/cli-utils.d.ts.map +1 -0
- package/dist/cli/cli-utils.js +580 -0
- package/dist/cli/cli-utils.js.map +1 -0
- package/dist/cli/rewrite.d.ts +6 -0
- package/dist/cli/rewrite.d.ts.map +1 -0
- package/dist/cli/rewrite.js +196 -0
- package/dist/cli/rewrite.js.map +1 -0
- package/dist/javascript/package-json-parser.d.ts +13 -0
- package/dist/javascript/package-json-parser.d.ts.map +1 -1
- package/dist/javascript/package-json-parser.js +43 -3
- package/dist/javascript/package-json-parser.js.map +1 -1
- package/dist/javascript/search/find-dependency.d.ts.map +1 -1
- package/dist/javascript/search/find-dependency.js +4 -3
- package/dist/javascript/search/find-dependency.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +3 -2
- package/src/cli/cli-utils.ts +554 -0
- package/src/cli/rewrite.ts +185 -0
- package/src/javascript/package-json-parser.ts +50 -3
- package/src/javascript/search/find-dependency.ts +4 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright 2025 the original author or authors.
|
|
4
|
+
* <p>
|
|
5
|
+
* Licensed under the Moderne Source Available License (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
* <p>
|
|
9
|
+
* https://docs.moderne.io/licensing/moderne-source-available-license
|
|
10
|
+
* <p>
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import {Command} from 'commander';
|
|
18
|
+
import * as fsp from 'fs/promises';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import {RecipeRegistry} from '../recipe';
|
|
21
|
+
import {ExecutionContext} from '../execution';
|
|
22
|
+
import {scheduleRun} from '../run';
|
|
23
|
+
import {TreePrinters} from '../print';
|
|
24
|
+
import {activate} from '../index';
|
|
25
|
+
import {
|
|
26
|
+
parseRecipeSpec,
|
|
27
|
+
parseRecipeOptions,
|
|
28
|
+
installRecipePackage,
|
|
29
|
+
loadLocalRecipes,
|
|
30
|
+
findRecipe,
|
|
31
|
+
discoverFiles,
|
|
32
|
+
parseFiles
|
|
33
|
+
} from './cli-utils';
|
|
34
|
+
|
|
35
|
+
// Import language modules to register printers
|
|
36
|
+
import '../text';
|
|
37
|
+
import '../json';
|
|
38
|
+
import '../java';
|
|
39
|
+
import '../javascript';
|
|
40
|
+
|
|
41
|
+
// Set stack size for complex parsing
|
|
42
|
+
require('v8').setFlagsFromString('--stack-size=8000');
|
|
43
|
+
|
|
44
|
+
const DEFAULT_RECIPE_DIR = path.join(process.env.HOME || '', '.rewrite', 'javascript', 'recipes');
|
|
45
|
+
|
|
46
|
+
interface CliOptions {
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
recipeDir?: string;
|
|
49
|
+
option: string[];
|
|
50
|
+
verbose: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const program = new Command();
|
|
55
|
+
program
|
|
56
|
+
.name('rewrite')
|
|
57
|
+
.description('Run OpenRewrite recipes on your codebase')
|
|
58
|
+
.argument('<recipe>', 'Recipe to run in format "package:recipe" (e.g., "@openrewrite/recipes-nodejs:replace-deprecated-slice")')
|
|
59
|
+
.option('-n, --dry-run', 'Show what changes would be made without applying them', false)
|
|
60
|
+
.option('-d, --recipe-dir <dir>', 'Directory for recipe installation', DEFAULT_RECIPE_DIR)
|
|
61
|
+
.option('-o, --option <option...>', 'Recipe options in format "key=value"', [])
|
|
62
|
+
.option('-v, --verbose', 'Enable verbose output', false)
|
|
63
|
+
.parse();
|
|
64
|
+
|
|
65
|
+
const recipeArg = program.args[0];
|
|
66
|
+
const opts = program.opts() as CliOptions;
|
|
67
|
+
opts.option = opts.option || [];
|
|
68
|
+
|
|
69
|
+
// Parse recipe specification
|
|
70
|
+
const recipeSpec = parseRecipeSpec(recipeArg);
|
|
71
|
+
if (!recipeSpec) {
|
|
72
|
+
console.error(`Invalid recipe format: ${recipeArg}`);
|
|
73
|
+
console.error('Expected format: "package:recipe" (e.g., "@openrewrite/recipes-nodejs:replace-deprecated-slice")');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (opts.verbose) {
|
|
78
|
+
console.log(`Package: ${recipeSpec.packageName}`);
|
|
79
|
+
console.log(`Recipe: ${recipeSpec.recipeName}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse recipe options
|
|
83
|
+
const recipeOptions = parseRecipeOptions(opts.option);
|
|
84
|
+
if (opts.verbose && Object.keys(recipeOptions).length > 0) {
|
|
85
|
+
console.log(`Options: ${JSON.stringify(recipeOptions)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Set up recipe registry
|
|
89
|
+
const registry = new RecipeRegistry();
|
|
90
|
+
|
|
91
|
+
// Register built-in recipes
|
|
92
|
+
await activate(registry);
|
|
93
|
+
|
|
94
|
+
// Load recipes from local path or install from NPM
|
|
95
|
+
try {
|
|
96
|
+
if (recipeSpec.isLocalPath) {
|
|
97
|
+
await loadLocalRecipes(recipeSpec.packageName, registry, opts.verbose);
|
|
98
|
+
} else {
|
|
99
|
+
await installRecipePackage(recipeSpec.packageName, opts.recipeDir || DEFAULT_RECIPE_DIR, registry, opts.verbose);
|
|
100
|
+
}
|
|
101
|
+
} catch (e: any) {
|
|
102
|
+
console.error(`Failed to load recipes: ${e.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find the recipe
|
|
107
|
+
const recipe = findRecipe(registry, recipeSpec.recipeName, recipeOptions);
|
|
108
|
+
if (!recipe) {
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.verbose) {
|
|
113
|
+
console.log(`Running recipe: ${recipe.name}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Discover source files
|
|
117
|
+
const projectRoot = process.cwd();
|
|
118
|
+
const sourceFiles = await discoverFiles(projectRoot, opts.verbose);
|
|
119
|
+
|
|
120
|
+
if (sourceFiles.length === 0) {
|
|
121
|
+
console.log('No source files found to process.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (opts.verbose) {
|
|
126
|
+
console.log(`Found ${sourceFiles.length} source files`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Parse all source files
|
|
130
|
+
const parsedFiles = await parseFiles(sourceFiles, projectRoot, opts.verbose);
|
|
131
|
+
|
|
132
|
+
if (parsedFiles.length === 0) {
|
|
133
|
+
console.log('No files could be parsed.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Run the recipe
|
|
138
|
+
const ctx = new ExecutionContext();
|
|
139
|
+
const {changeset} = await scheduleRun(recipe, parsedFiles, ctx);
|
|
140
|
+
|
|
141
|
+
if (changeset.length === 0) {
|
|
142
|
+
console.log('No changes to make.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`\n${changeset.length} file(s) ${opts.dryRun ? 'would be ' : ''}changed:\n`);
|
|
147
|
+
|
|
148
|
+
// Handle results
|
|
149
|
+
for (const result of changeset) {
|
|
150
|
+
if (opts.dryRun) {
|
|
151
|
+
// Print diff
|
|
152
|
+
const diff = await result.diff();
|
|
153
|
+
console.log(diff);
|
|
154
|
+
} else {
|
|
155
|
+
// Apply changes
|
|
156
|
+
if (result.after) {
|
|
157
|
+
const filePath = path.join(projectRoot, result.after.sourcePath);
|
|
158
|
+
const content = await TreePrinters.print(result.after);
|
|
159
|
+
|
|
160
|
+
// Ensure directory exists
|
|
161
|
+
await fsp.mkdir(path.dirname(filePath), {recursive: true});
|
|
162
|
+
await fsp.writeFile(filePath, content);
|
|
163
|
+
|
|
164
|
+
if (result.before) {
|
|
165
|
+
console.log(` Modified: ${result.after.sourcePath}`);
|
|
166
|
+
} else {
|
|
167
|
+
console.log(` Created: ${result.after.sourcePath}`);
|
|
168
|
+
}
|
|
169
|
+
} else if (result.before) {
|
|
170
|
+
const filePath = path.join(projectRoot, result.before.sourcePath);
|
|
171
|
+
await fsp.unlink(filePath);
|
|
172
|
+
console.log(` Deleted: ${result.before.sourcePath}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!opts.dryRun) {
|
|
178
|
+
console.log('\nChanges applied successfully.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
main().catch((err) => {
|
|
183
|
+
console.error('Fatal error:', err.message);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
@@ -187,12 +187,15 @@ export class PackageJsonParser extends Parser {
|
|
|
187
187
|
: filePath;
|
|
188
188
|
|
|
189
189
|
// Try to read lock file if dependency resolution is not skipped
|
|
190
|
-
//
|
|
190
|
+
// First try the directory containing the package.json, then walk up toward relativeTo
|
|
191
191
|
let lockContent: PackageLockContent | undefined = undefined;
|
|
192
192
|
let packageManager: PackageManager | undefined = undefined;
|
|
193
193
|
if (!this.skipDependencyResolution) {
|
|
194
|
-
|
|
195
|
-
const
|
|
194
|
+
// Resolve dir relative to relativeTo if dir is relative (e.g., '.' when package.json is at root)
|
|
195
|
+
const absoluteDir = this.relativeTo && !path.isAbsolute(dir)
|
|
196
|
+
? path.resolve(this.relativeTo, dir)
|
|
197
|
+
: dir;
|
|
198
|
+
const lockResult = await this.tryReadLockFileWithWalkUp(absoluteDir, this.relativeTo);
|
|
196
199
|
lockContent = lockResult?.content;
|
|
197
200
|
packageManager = lockResult?.packageManager;
|
|
198
201
|
}
|
|
@@ -215,6 +218,50 @@ export class PackageJsonParser extends Parser {
|
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Attempts to find and read a lock file by walking up the directory tree.
|
|
223
|
+
* Starts from the directory containing the package.json and walks up toward
|
|
224
|
+
* the root directory (or relativeTo if specified).
|
|
225
|
+
*
|
|
226
|
+
* This handles both standalone projects (lock file next to package.json) and
|
|
227
|
+
* workspace scenarios (lock file at workspace root).
|
|
228
|
+
*
|
|
229
|
+
* @param startDir The directory containing the package.json being parsed
|
|
230
|
+
* @param rootDir Optional root directory to stop walking at (e.g., relativeTo/git root)
|
|
231
|
+
* @returns Object with parsed lock file content and detected package manager, or undefined if none found
|
|
232
|
+
*/
|
|
233
|
+
private async tryReadLockFileWithWalkUp(
|
|
234
|
+
startDir: string,
|
|
235
|
+
rootDir?: string
|
|
236
|
+
): Promise<{ content: PackageLockContent; packageManager: PackageManager } | undefined> {
|
|
237
|
+
// Normalize paths for comparison
|
|
238
|
+
const normalizedRoot = rootDir ? path.resolve(rootDir) : undefined;
|
|
239
|
+
let currentDir = path.resolve(startDir);
|
|
240
|
+
|
|
241
|
+
// Walk up the directory tree looking for a lock file
|
|
242
|
+
while (true) {
|
|
243
|
+
const result = await this.tryReadLockFile(currentDir);
|
|
244
|
+
if (result) {
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If we've reached rootDir, stop walking (don't go above it)
|
|
249
|
+
if (normalizedRoot && currentDir === normalizedRoot) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check if we've reached the filesystem root
|
|
254
|
+
const parentDir = path.dirname(currentDir);
|
|
255
|
+
if (parentDir === currentDir) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
currentDir = parentDir;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
218
265
|
/**
|
|
219
266
|
* Attempts to read and parse a lock file from the given directory.
|
|
220
267
|
* Supports npm (package-lock.json), bun (bun.lock), pnpm, and yarn.
|
|
@@ -88,6 +88,9 @@ export class FindDependency extends Recipe {
|
|
|
88
88
|
|
|
89
89
|
constructor(options: FindDependencyOptions) {
|
|
90
90
|
super(options);
|
|
91
|
+
// Handle string values from RPC serialization (e.g., "false" instead of false)
|
|
92
|
+
// and default to true if not specified
|
|
93
|
+
this.onlyDirect = !(this.onlyDirect === false || (this.onlyDirect as any === "false"));
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
override instanceName(): string {
|
|
@@ -97,8 +100,7 @@ export class FindDependency extends Recipe {
|
|
|
97
100
|
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
98
101
|
const packageName = this.packageName;
|
|
99
102
|
const version = this.version;
|
|
100
|
-
|
|
101
|
-
const onlyDirect = this.onlyDirect ?? true;
|
|
103
|
+
const onlyDirect = this.onlyDirect;
|
|
102
104
|
|
|
103
105
|
// Create a picomatch matcher for the package name pattern
|
|
104
106
|
// For patterns without '/', use { contains: true } so that '*jest*' matches '@types/jest'
|