@openrewrite/rewrite 8.63.3 → 8.63.4

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.
Files changed (55) hide show
  1. package/dist/java/rpc.d.ts +2 -2
  2. package/dist/java/rpc.d.ts.map +1 -1
  3. package/dist/java/rpc.js +10 -4
  4. package/dist/java/rpc.js.map +1 -1
  5. package/dist/java/type.d.ts +1 -1
  6. package/dist/java/type.d.ts.map +1 -1
  7. package/dist/java/type.js +3 -3
  8. package/dist/java/type.js.map +1 -1
  9. package/dist/javascript/assertions.d.ts +1 -1
  10. package/dist/javascript/assertions.d.ts.map +1 -1
  11. package/dist/javascript/assertions.js +35 -65
  12. package/dist/javascript/assertions.js.map +1 -1
  13. package/dist/javascript/comparator.d.ts +2 -2
  14. package/dist/javascript/comparator.d.ts.map +1 -1
  15. package/dist/javascript/comparator.js.map +1 -1
  16. package/dist/javascript/dependency-workspace.d.ts +44 -0
  17. package/dist/javascript/dependency-workspace.d.ts.map +1 -0
  18. package/dist/javascript/dependency-workspace.js +335 -0
  19. package/dist/javascript/dependency-workspace.js.map +1 -0
  20. package/dist/javascript/parser.d.ts.map +1 -1
  21. package/dist/javascript/parser.js +5 -2
  22. package/dist/javascript/parser.js.map +1 -1
  23. package/dist/javascript/preconditions.js +2 -2
  24. package/dist/javascript/preconditions.js.map +1 -1
  25. package/dist/javascript/templating.d.ts +110 -5
  26. package/dist/javascript/templating.d.ts.map +1 -1
  27. package/dist/javascript/templating.js +412 -38
  28. package/dist/javascript/templating.js.map +1 -1
  29. package/dist/javascript/type-mapping.js +2 -2
  30. package/dist/javascript/type-mapping.js.map +1 -1
  31. package/dist/rpc/queue.d.ts +1 -0
  32. package/dist/rpc/queue.d.ts.map +1 -1
  33. package/dist/rpc/queue.js +11 -1
  34. package/dist/rpc/queue.js.map +1 -1
  35. package/dist/rpc/server.d.ts.map +1 -1
  36. package/dist/rpc/server.js +5 -0
  37. package/dist/rpc/server.js.map +1 -1
  38. package/dist/test/rewrite-test.d.ts +1 -1
  39. package/dist/test/rewrite-test.d.ts.map +1 -1
  40. package/dist/test/rewrite-test.js +27 -5
  41. package/dist/test/rewrite-test.js.map +1 -1
  42. package/dist/version.txt +1 -1
  43. package/package.json +1 -1
  44. package/src/java/rpc.ts +4 -4
  45. package/src/java/type.ts +3 -3
  46. package/src/javascript/assertions.ts +14 -21
  47. package/src/javascript/comparator.ts +2 -2
  48. package/src/javascript/dependency-workspace.ts +317 -0
  49. package/src/javascript/parser.ts +6 -3
  50. package/src/javascript/preconditions.ts +2 -2
  51. package/src/javascript/templating.ts +535 -44
  52. package/src/javascript/type-mapping.ts +2 -2
  53. package/src/rpc/queue.ts +11 -1
  54. package/src/rpc/server.ts +5 -0
  55. package/src/test/rewrite-test.ts +11 -3
@@ -20,33 +20,26 @@ import ts from 'typescript';
20
20
  import {json, Json} from "../json";
21
21
  import * as fs from "fs";
22
22
  import * as path from "path";
23
- import {execSync} from "child_process";
23
+ import {DependencyWorkspace} from "./dependency-workspace";
24
24
 
25
25
  const sourceFileCache: Map<string, ts.SourceFile> = new Map();
26
26
 
27
- export function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]): Generator<SourceSpec<any>, void, unknown> {
27
+ export async function* npm(relativeTo: string, ...sourceSpecs: SourceSpec<any>[]): AsyncGenerator<SourceSpec<any>, void, unknown> {
28
28
  for (const spec of sourceSpecs) {
29
29
  if (spec.path === 'package.json') {
30
- // Write package.json to disk so npm install can be run
31
- fs.mkdirSync(relativeTo, {recursive: true});
32
- const packageJsonPath = path.join(relativeTo, 'package.json');
33
-
34
- // Check if package.json already exists with the same content
35
- let needsInstall = true;
36
- if (fs.existsSync(packageJsonPath)) {
37
- const existingContent = fs.readFileSync(packageJsonPath, 'utf-8');
38
- if (existingContent === spec.before) {
39
- needsInstall = false;
40
- }
41
- }
42
-
43
- if (needsInstall) {
44
- fs.writeFileSync(packageJsonPath, spec.before!);
45
- execSync('npm install', {
46
- cwd: relativeTo,
47
- stdio: 'inherit' // Show npm output for debugging
48
- });
30
+ // Parse package.json to extract dependencies
31
+ const packageJsonContent = JSON.parse(spec.before!);
32
+ const dependencies = {
33
+ ...packageJsonContent.dependencies,
34
+ ...packageJsonContent.devDependencies
35
+ };
36
+
37
+ // Use DependencyWorkspace to create workspace in relativeTo directory
38
+ // This will check if it's already valid and skip npm install if so
39
+ if (Object.keys(dependencies).length > 0) {
40
+ await DependencyWorkspace.getOrCreateWorkspace(dependencies, relativeTo);
49
41
  }
42
+
50
43
  yield spec;
51
44
  }
52
45
  }
@@ -27,7 +27,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
27
27
  /**
28
28
  * Flag indicating whether the trees match so far
29
29
  */
30
- private match: boolean = true;
30
+ protected match: boolean = true;
31
31
 
32
32
  /**
33
33
  * Creates a new comparator visitor.
@@ -63,7 +63,7 @@ export class JavaScriptComparatorVisitor extends JavaScriptVisitor<J> {
63
63
  /**
64
64
  * Aborts the visit operation by setting the match flag to false.
65
65
  */
66
- private abort(): void {
66
+ protected abort(): void {
67
67
  this.match = false;
68
68
  }
69
69
 
@@ -0,0 +1,317 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import * as os from 'os';
19
+ import * as crypto from 'crypto';
20
+ import {execSync} from 'child_process';
21
+
22
+ /**
23
+ * Manages workspace directories for TypeScript compilation with dependencies.
24
+ * Creates temporary workspaces with package.json and installed node_modules
25
+ * to enable proper type attribution for templates.
26
+ */
27
+ export class DependencyWorkspace {
28
+ private static readonly WORKSPACE_BASE = path.join(os.tmpdir(), 'openrewrite-js-workspaces');
29
+ private static readonly cache = new Map<string, string>();
30
+
31
+ /**
32
+ * Gets or creates a workspace directory for the given dependencies.
33
+ * Workspaces are cached by dependency hash to avoid repeated npm installs.
34
+ *
35
+ * @param dependencies NPM dependencies (package name to version mapping)
36
+ * @param targetDir Optional target directory. If provided, creates workspace in this directory
37
+ * instead of a hash-based temp directory. Caller is responsible for directory lifecycle.
38
+ * @returns Path to the workspace directory
39
+ */
40
+ static async getOrCreateWorkspace(dependencies: Record<string, string>, targetDir?: string): Promise<string> {
41
+ if (targetDir) {
42
+ // Use provided directory - check if it's already valid
43
+ if (this.isWorkspaceValid(targetDir, dependencies)) {
44
+ return targetDir;
45
+ }
46
+
47
+ // Create/update workspace in target directory
48
+ fs.mkdirSync(targetDir, {recursive: true});
49
+
50
+ try {
51
+ const packageJson = {
52
+ name: "openrewrite-template-workspace",
53
+ version: "1.0.0",
54
+ private: true,
55
+ dependencies: dependencies
56
+ };
57
+
58
+ fs.writeFileSync(
59
+ path.join(targetDir, 'package.json'),
60
+ JSON.stringify(packageJson, null, 2)
61
+ );
62
+
63
+ // Run npm install
64
+ execSync('npm install --silent', {
65
+ cwd: targetDir,
66
+ stdio: 'pipe' // Suppress output
67
+ });
68
+
69
+ return targetDir;
70
+ } catch (error) {
71
+ throw new Error(`Failed to create dependency workspace: ${error}`);
72
+ }
73
+ }
74
+
75
+ // Use hash-based cached workspace
76
+ const hash = this.hashDependencies(dependencies);
77
+
78
+ // Check cache
79
+ const cached = this.cache.get(hash);
80
+ if (cached && fs.existsSync(cached) && this.isWorkspaceValid(cached, dependencies)) {
81
+ return cached;
82
+ }
83
+
84
+ // Final workspace location
85
+ const workspaceDir = path.join(this.WORKSPACE_BASE, hash);
86
+
87
+ // Check if valid workspace already exists on disk (cross-VM reuse)
88
+ if (fs.existsSync(workspaceDir) && this.isWorkspaceValid(workspaceDir, dependencies)) {
89
+ this.cache.set(hash, workspaceDir);
90
+ return workspaceDir;
91
+ }
92
+
93
+ // Ensure base directory exists
94
+ if (!fs.existsSync(this.WORKSPACE_BASE)) {
95
+ fs.mkdirSync(this.WORKSPACE_BASE, {recursive: true});
96
+ }
97
+
98
+ // Create workspace in temporary location to ensure atomicity
99
+ // This prevents reusing partially created workspaces from crashes
100
+ // and handles concurrency with other Node processes
101
+ const tempSuffix = `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
102
+ const tempWorkspaceDir = path.join(this.WORKSPACE_BASE, hash + tempSuffix);
103
+
104
+ try {
105
+ // Create temporary workspace directory
106
+ fs.mkdirSync(tempWorkspaceDir, {recursive: true});
107
+
108
+ // Create package.json
109
+ const packageJson = {
110
+ name: "openrewrite-template-workspace",
111
+ version: "1.0.0",
112
+ private: true,
113
+ dependencies: dependencies
114
+ };
115
+
116
+ fs.writeFileSync(
117
+ path.join(tempWorkspaceDir, 'package.json'),
118
+ JSON.stringify(packageJson, null, 2)
119
+ );
120
+
121
+ // Run npm install
122
+ execSync('npm install --silent', {
123
+ cwd: tempWorkspaceDir,
124
+ stdio: 'pipe' // Suppress output
125
+ });
126
+
127
+ // Atomically move to final location with retry logic for concurrency
128
+ let moved = false;
129
+ let retries = 3;
130
+
131
+ while (!moved && retries > 0) {
132
+ try {
133
+ // Attempt atomic rename (works on POSIX, fails on Windows if target exists)
134
+ fs.renameSync(tempWorkspaceDir, workspaceDir);
135
+ moved = true;
136
+ } catch (error: any) {
137
+ // Handle concurrent creation by another process
138
+ if (error.code === 'EEXIST' || error.code === 'ENOTEMPTY' || error.code === 'EISDIR' ||
139
+ (error.code === 'EPERM' && fs.existsSync(workspaceDir))) {
140
+ // Target exists - check if it's valid
141
+ if (this.isWorkspaceValid(workspaceDir, dependencies)) {
142
+ // Another process created a valid workspace - use theirs
143
+ moved = true; // Don't try again
144
+ } else {
145
+ // Invalid workspace exists - try to remove and retry
146
+ try {
147
+ fs.rmSync(workspaceDir, {recursive: true, force: true});
148
+ retries--;
149
+ } catch (removeError) {
150
+ // Another process might be using it, give up
151
+ retries = 0;
152
+ }
153
+ }
154
+ } else if (error.code === 'EXDEV') {
155
+ // Cross-device link - fallback to copy+remove (not atomic, but rare)
156
+ try {
157
+ fs.cpSync(tempWorkspaceDir, workspaceDir, {recursive: true});
158
+ moved = true;
159
+ } catch (copyError) {
160
+ // Check if another process created it while we were copying
161
+ if (this.isWorkspaceValid(workspaceDir, dependencies)) {
162
+ moved = true;
163
+ } else {
164
+ throw error;
165
+ }
166
+ }
167
+ } else {
168
+ // Unexpected error
169
+ throw error;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Clean up temp directory
175
+ try {
176
+ if (fs.existsSync(tempWorkspaceDir)) {
177
+ fs.rmSync(tempWorkspaceDir, {recursive: true, force: true});
178
+ }
179
+ } catch {
180
+ // Ignore cleanup errors
181
+ }
182
+
183
+ // Verify final workspace is valid (might be from another process)
184
+ if (!this.isWorkspaceValid(workspaceDir, dependencies)) {
185
+ throw new Error('Failed to create valid workspace due to concurrent modifications');
186
+ }
187
+
188
+ // Cache the workspace
189
+ this.cache.set(hash, workspaceDir);
190
+
191
+ return workspaceDir;
192
+ } catch (error) {
193
+ // Clean up temporary workspace on failure
194
+ try {
195
+ if (fs.existsSync(tempWorkspaceDir)) {
196
+ fs.rmSync(tempWorkspaceDir, {recursive: true, force: true});
197
+ }
198
+ } catch {
199
+ // Ignore cleanup errors
200
+ }
201
+ throw new Error(`Failed to create dependency workspace: ${error}`);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Generates a hash from dependencies for caching.
207
+ */
208
+ private static hashDependencies(dependencies: Record<string, string>): string {
209
+ // Sort keys for consistent hashing
210
+ const sorted = Object.keys(dependencies).sort();
211
+ const content = sorted.map(key => `${key}:${dependencies[key]}`).join(',');
212
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
213
+ }
214
+
215
+ /**
216
+ * Checks if a workspace is valid (has node_modules and matching package.json).
217
+ *
218
+ * @param workspaceDir Directory to check
219
+ * @param expectedDependencies Optional dependencies to check against package.json
220
+ */
221
+ private static isWorkspaceValid(workspaceDir: string, expectedDependencies?: Record<string, string>): boolean {
222
+ const nodeModules = path.join(workspaceDir, 'node_modules');
223
+ const packageJsonPath = path.join(workspaceDir, 'package.json');
224
+
225
+ if (!fs.existsSync(nodeModules) || !fs.existsSync(packageJsonPath)) {
226
+ return false;
227
+ }
228
+
229
+ // If dependencies provided, check if they match
230
+ if (expectedDependencies) {
231
+ try {
232
+ const packageJsonContent = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
233
+ const existingDeps = packageJsonContent.dependencies || {};
234
+
235
+ // Check if all expected dependencies match
236
+ const expectedKeys = Object.keys(expectedDependencies).sort();
237
+ const existingKeys = Object.keys(existingDeps).sort();
238
+
239
+ if (expectedKeys.length !== existingKeys.length) {
240
+ return false;
241
+ }
242
+
243
+ for (let i = 0; i < expectedKeys.length; i++) {
244
+ if (expectedKeys[i] !== existingKeys[i] ||
245
+ expectedDependencies[expectedKeys[i]] !== existingDeps[existingKeys[i]]) {
246
+ return false;
247
+ }
248
+ }
249
+ } catch (error) {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ return true;
255
+ }
256
+
257
+ /**
258
+ * Cleans up old workspace directories.
259
+ * Removes workspaces older than the specified age.
260
+ * Also removes all temporary directories (*.tmp-*) regardless of age,
261
+ * as these indicate incomplete/crashed operations.
262
+ *
263
+ * @param maxAgeMs Maximum age in milliseconds (default: 24 hours)
264
+ */
265
+ static cleanupOldWorkspaces(maxAgeMs: number = 24 * 60 * 60 * 1000): void {
266
+ if (!fs.existsSync(this.WORKSPACE_BASE)) {
267
+ return;
268
+ }
269
+
270
+ const now = Date.now();
271
+ const entries = fs.readdirSync(this.WORKSPACE_BASE, {withFileTypes: true});
272
+
273
+ for (const entry of entries) {
274
+ if (!entry.isDirectory()) {
275
+ continue;
276
+ }
277
+
278
+ const workspaceDir = path.join(this.WORKSPACE_BASE, entry.name);
279
+
280
+ // Always clean up temporary directories (incomplete operations)
281
+ if (entry.name.includes('.tmp-')) {
282
+ try {
283
+ fs.rmSync(workspaceDir, {recursive: true, force: true});
284
+ } catch (error) {
285
+ // Ignore errors, might be in use by another process
286
+ }
287
+ continue;
288
+ }
289
+
290
+ // Clean up old regular workspaces
291
+ try {
292
+ const stats = fs.statSync(workspaceDir);
293
+ const age = now - stats.mtimeMs;
294
+
295
+ if (age > maxAgeMs) {
296
+ fs.rmSync(workspaceDir, {recursive: true, force: true});
297
+ // Remove from cache
298
+ for (const [hash, dir] of this.cache.entries()) {
299
+ if (dir === workspaceDir) {
300
+ this.cache.delete(hash);
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ } catch (error) {
306
+ // Ignore errors, workspace might be in use
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Clears all cached workspaces.
313
+ */
314
+ static clearCache(): void {
315
+ this.cache.clear();
316
+ }
317
+ }
@@ -609,11 +609,12 @@ export class JavaScriptParserVisitor {
609
609
  visitNumericLiteral(node: ts.NumericLiteral): J.Literal {
610
610
  // Parse the numeric value from the text
611
611
  const text = node.text;
612
- let value: number | bigint;
612
+ let value: number | bigint | string;
613
613
 
614
614
  // Check if it's a BigInt literal (ends with 'n')
615
615
  if (text.endsWith('n')) {
616
- value = BigInt(text.slice(0, -1));
616
+ // TODO consider adding `JS.Literal`
617
+ value = text.slice(0, -1);
617
618
  } else if (text.includes('.') || text.toLowerCase().includes('e')) {
618
619
  // Floating point number
619
620
  value = parseFloat(text);
@@ -708,7 +709,8 @@ export class JavaScriptParserVisitor {
708
709
  visitBigIntLiteral(node: ts.BigIntLiteral): J.Literal {
709
710
  // Parse BigInt value, removing the 'n' suffix
710
711
  const text = node.text;
711
- const value = BigInt(text.slice(0, -1));
712
+ // TODO consider adding `JS.Literal`
713
+ const value = text.slice(0, -1);
712
714
  return this.mapLiteral(node, value);
713
715
  }
714
716
 
@@ -717,6 +719,7 @@ export class JavaScriptParserVisitor {
717
719
  }
718
720
 
719
721
  visitRegularExpressionLiteral(node: ts.RegularExpressionLiteral): J.Literal {
722
+ // TODO consider adding `JS.Literal`
720
723
  return this.mapLiteral(node, node.text); // FIXME value not in AST
721
724
  }
722
725
 
@@ -27,14 +27,14 @@ export function hasSourcePath(filePattern: string): Promise<RpcRecipe> | TreeVis
27
27
  }
28
28
 
29
29
  export function usesMethod(methodPattern: string, matchOverrides: boolean = false): Promise<RpcRecipe> | TreeVisitor<any, ExecutionContext> {
30
- return RewriteRpc.get() ? RewriteRpc.get()!.prepareRecipe("org.openrewrite.java.search.FindMethods", {
30
+ return RewriteRpc.get() ? RewriteRpc.get()!.prepareRecipe("org.openrewrite.java.search.HasMethod", {
31
31
  methodPattern,
32
32
  matchOverrides
33
33
  }) : new UsesMethod(methodPattern);
34
34
  }
35
35
 
36
36
  export function usesType(fullyQualifiedType: string): Promise<RpcRecipe> | TreeVisitor<any, ExecutionContext> {
37
- return RewriteRpc.get() ? RewriteRpc.get()!.prepareRecipe("org.openrewrite.java.search.FindTypes", {
37
+ return RewriteRpc.get() ? RewriteRpc.get()!.prepareRecipe("org.openrewrite.java.search.HasType", {
38
38
  fullyQualifiedType,
39
39
  checkAssignability: false
40
40
  }) : new UsesType(fullyQualifiedType);