@openrewrite/rewrite 8.69.0-20251206-160244 → 8.69.0-20251207-092603

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 (36) hide show
  1. package/dist/cli/cli-utils.d.ts +12 -2
  2. package/dist/cli/cli-utils.d.ts.map +1 -1
  3. package/dist/cli/cli-utils.js +53 -13
  4. package/dist/cli/cli-utils.js.map +1 -1
  5. package/dist/cli/rewrite.d.ts.map +1 -1
  6. package/dist/cli/rewrite.js +230 -82
  7. package/dist/cli/rewrite.js.map +1 -1
  8. package/dist/cli/validate-parsing-recipe.d.ts +23 -0
  9. package/dist/cli/validate-parsing-recipe.d.ts.map +1 -0
  10. package/dist/cli/validate-parsing-recipe.js +149 -0
  11. package/dist/cli/validate-parsing-recipe.js.map +1 -0
  12. package/dist/javascript/format.d.ts.map +1 -1
  13. package/dist/javascript/format.js +7 -2
  14. package/dist/javascript/format.js.map +1 -1
  15. package/dist/javascript/parser.d.ts.map +1 -1
  16. package/dist/javascript/parser.js +4 -2
  17. package/dist/javascript/parser.js.map +1 -1
  18. package/dist/javascript/tabs-and-indents-visitor.d.ts.map +1 -1
  19. package/dist/javascript/tabs-and-indents-visitor.js +32 -4
  20. package/dist/javascript/tabs-and-indents-visitor.js.map +1 -1
  21. package/dist/json/parser.js +35 -20
  22. package/dist/json/parser.js.map +1 -1
  23. package/dist/run.d.ts +8 -1
  24. package/dist/run.d.ts.map +1 -1
  25. package/dist/run.js +78 -21
  26. package/dist/run.js.map +1 -1
  27. package/dist/version.txt +1 -1
  28. package/package.json +1 -1
  29. package/src/cli/cli-utils.ts +28 -9
  30. package/src/cli/rewrite.ts +244 -85
  31. package/src/cli/validate-parsing-recipe.ts +114 -0
  32. package/src/javascript/format.ts +7 -2
  33. package/src/javascript/parser.ts +6 -2
  34. package/src/javascript/tabs-and-indents-visitor.ts +33 -3
  35. package/src/json/parser.ts +35 -20
  36. package/src/run.ts +61 -25
@@ -59,35 +59,50 @@ class ParseJsonReader extends ParserSourceReader {
59
59
  }
60
60
  if (Array.isArray(parsed)) {
61
61
  this.cursor++; // skip '['
62
+ const values = parsed.map((p, i) => {
63
+ const element = this.json(p) as Json.Value;
64
+ const afterWhitespace = this.whitespace();
65
+ // Check if there's a comma after this element
66
+ const hasComma = this.source[this.cursor] === ',';
67
+ if (hasComma) {
68
+ this.cursor++; // skip ','
69
+ }
70
+ return {
71
+ kind: Json.Kind.RightPadded,
72
+ element,
73
+ after: space(afterWhitespace),
74
+ markers: emptyMarkers
75
+ } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>;
76
+ });
77
+ this.cursor++; // skip ']'
62
78
  return {
63
79
  kind: Json.Kind.Array,
64
80
  ...base,
65
- values: parsed.map(p => {
66
- const value = {
67
- kind: Json.Kind.RightPadded,
68
- element: this.json(p) as Json.Value,
69
- after: space(this.whitespace()),
70
- markers: emptyMarkers
71
- } satisfies Json.RightPadded<Json.Value> as Json.RightPadded<Json.Value>;
72
- this.cursor++;
73
- return value;
74
- })
81
+ values
75
82
  } satisfies Json.Array as Json.Array;
76
83
  } else if (parsed !== null && typeof parsed === "object") {
77
84
  this.cursor++; // skip '{'
85
+ const keys = Object.keys(parsed);
86
+ const members = keys.map((key, i) => {
87
+ const element = this.member(parsed, key);
88
+ const afterWhitespace = this.whitespace();
89
+ // Check if there's a comma after this element
90
+ const hasComma = this.source[this.cursor] === ',';
91
+ if (hasComma) {
92
+ this.cursor++; // skip ','
93
+ }
94
+ return {
95
+ kind: Json.Kind.RightPadded,
96
+ element,
97
+ after: space(afterWhitespace),
98
+ markers: emptyMarkers
99
+ } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>;
100
+ });
101
+ this.cursor++; // skip '}'
78
102
  return {
79
103
  kind: Json.Kind.Object,
80
104
  ...base,
81
- members: Object.keys(parsed).map(key => {
82
- const member = {
83
- kind: Json.Kind.RightPadded,
84
- element: this.member(parsed, key),
85
- after: space(this.whitespace()),
86
- markers: emptyMarkers
87
- } satisfies Json.RightPadded<Json.Member> as Json.RightPadded<Json.Member>;
88
- this.cursor++;
89
- return member;
90
- })
105
+ members
91
106
  } satisfies Json.Object as Json.Object;
92
107
  } else if (typeof parsed === "string") {
93
108
  // Extract original source to preserve escape sequences
package/src/run.ts CHANGED
@@ -68,51 +68,87 @@ export async function scheduleRun(recipe: Recipe, before: SourceFile[], ctx: Exe
68
68
  return { changeset };
69
69
  }
70
70
 
71
+ export type ProgressCallback = (phase: 'parsing' | 'scanning' | 'processing', current: number, total: number, sourcePath: string) => void;
72
+
71
73
  /**
72
74
  * Streaming version of scheduleRun that yields results as soon as each file is processed.
73
75
  * This allows callers to print diffs immediately and free memory earlier.
74
76
  *
77
+ * Accepts either an array or an async iterable of source files. When the recipe is not
78
+ * a scanning recipe, files are processed immediately as they're yielded from the iterable,
79
+ * avoiding the need to collect all files into memory first.
80
+ *
75
81
  * For scanning recipes, the scan phase completes on all files before yielding any results.
82
+ *
83
+ * @param onProgress Optional callback for progress updates during parsing, scanning, and processing phases.
76
84
  */
77
85
  export async function* scheduleRunStreaming(
78
86
  recipe: Recipe,
79
- before: SourceFile[],
80
- ctx: ExecutionContext
87
+ before: SourceFile[] | AsyncIterable<SourceFile>,
88
+ ctx: ExecutionContext,
89
+ onProgress?: ProgressCallback
81
90
  ): AsyncGenerator<Result, void, undefined> {
82
91
  const cursor = rootCursor();
92
+ const isScanning = await hasScanningRecipe(recipe);
93
+
94
+ if (isScanning) {
95
+ // For scanning recipes, we need to collect all files first for the scan phase
96
+ const files: SourceFile[] = Array.isArray(before) ? [...before] : [];
97
+ if (!Array.isArray(before)) {
98
+ let parseCount = 0;
99
+ for await (const sf of before) {
100
+ files.push(sf);
101
+ parseCount++;
102
+ onProgress?.('parsing', parseCount, -1, sf.sourcePath); // -1 = unknown total
103
+ }
104
+ }
105
+
106
+ const totalFiles = files.length;
83
107
 
84
- // Phase 1: Run scanners on all files (if any scanning recipes exist)
85
- if (await hasScanningRecipe(recipe)) {
86
- for (const b of before) {
108
+ // Phase 1: Run scanners on all files
109
+ for (let i = 0; i < files.length; i++) {
110
+ const b = files[i];
111
+ onProgress?.('scanning', i + 1, totalFiles, b.sourcePath);
87
112
  await recurseRecipeList(recipe, b, async (recipe, b2) => {
88
113
  if (recipe instanceof ScanningRecipe) {
89
114
  return (await recipe.scanner(recipe.accumulator(cursor, ctx))).visit(b2, ctx, cursor)
90
115
  }
91
116
  });
92
117
  }
93
- }
94
118
 
95
- // Phase 2: Collect generated files
96
- const generated = (await recurseRecipeList(recipe, [] as SourceFile[], async (recipe, generated) => {
97
- if (recipe instanceof ScanningRecipe) {
98
- generated.push(...await recipe.generate(recipe.accumulator(cursor, ctx), ctx));
99
- }
100
- return generated;
101
- }))!;
102
-
103
- // Phase 3: Edit existing files and yield results immediately
104
- for (const b of before) {
105
- const editedB = await recurseRecipeList(recipe, b, async (recipe, b2) => (await recipe.editor()).visit(b2, ctx, cursor));
106
- if (editedB !== b) {
107
- yield new Result(b, editedB);
119
+ // Phase 2: Collect generated files
120
+ const generated = (await recurseRecipeList(recipe, [] as SourceFile[], async (recipe, generated) => {
121
+ if (recipe instanceof ScanningRecipe) {
122
+ generated.push(...await recipe.generate(recipe.accumulator(cursor, ctx), ctx));
123
+ }
124
+ return generated;
125
+ }))!;
126
+
127
+ // Phase 3: Edit existing files and yield results immediately
128
+ for (let i = 0; i < files.length; i++) {
129
+ const b = files[i];
130
+ onProgress?.('processing', i + 1, totalFiles, b.sourcePath);
131
+ const editedB = await recurseRecipeList(recipe, b, async (recipe, b2) => (await recipe.editor()).visit(b2, ctx, cursor));
132
+ // Always yield a result so the caller knows when each file is processed
133
+ yield new Result(b, editedB !== b ? editedB : b);
134
+ // Clear array entry to allow GC to free memory for this file
135
+ (files as any)[i] = null;
108
136
  }
109
- }
110
137
 
111
- // Phase 4: Edit generated files and yield results
112
- for (const g of generated) {
113
- const editedG = await recurseRecipeList(recipe, g, async (recipe, g2) => (await recipe.editor()).visit(g2, ctx, cursor));
114
- if (editedG) {
115
- yield new Result(undefined, editedG);
138
+ // Phase 4: Edit generated files and yield results
139
+ for (const g of generated) {
140
+ const editedG = await recurseRecipeList(recipe, g, async (recipe, g2) => (await recipe.editor()).visit(g2, ctx, cursor));
141
+ if (editedG) {
142
+ yield new Result(undefined, editedG);
143
+ }
144
+ }
145
+ } else {
146
+ // For non-scanning recipes, process files immediately as they come in
147
+ const iterable = Array.isArray(before) ? before : before;
148
+ for await (const b of iterable) {
149
+ const editedB = await recurseRecipeList(recipe, b, async (recipe, b2) => (await recipe.editor()).visit(b2, ctx, cursor));
150
+ // Always yield a result so the caller knows when each file is processed
151
+ yield new Result(b, editedB !== b ? editedB : b);
116
152
  }
117
153
  }
118
154