@openrewrite/rewrite 8.68.0-20251204-151952 → 8.68.0-20251204-174357

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.
@@ -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
- // Use relativeTo directory if available (for tests), otherwise use the directory from input path
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
- const lockDir = this.relativeTo || dir;
195
- const lockResult = await this.tryReadLockFile(lockDir);
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
- // Default to true if not specified (only search direct dependencies)
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'