Skip to content

Commit d9be625

Browse files
committed
JavaScript: Fix one more annoying formatter bug
1 parent de857e4 commit d9be625

4 files changed

Lines changed: 99 additions & 6 deletions

File tree

rewrite-javascript/rewrite/src/cli/cli-utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,15 +452,20 @@ export function isAcceptedFile(filePath: string): boolean {
452452
return false;
453453
}
454454

455+
export type ProgressCallback = (current: number, total: number, filePath: string) => void;
456+
455457
/**
456458
* Parse source files using appropriate parsers
457459
*/
458460
export async function parseFiles(
459461
filePaths: string[],
460462
projectRoot: string,
461-
verbose: boolean = false
463+
verbose: boolean = false,
464+
onProgress?: ProgressCallback
462465
): Promise<SourceFile[]> {
463466
const parsed: SourceFile[] = [];
467+
const total = filePaths.length;
468+
let current = 0;
464469

465470
// Group files by type
466471
const jsFiles: string[] = [];
@@ -487,6 +492,8 @@ export async function parseFiles(
487492
}
488493
const jsParser = new JavaScriptParser({relativeTo: projectRoot});
489494
for await (const sf of jsParser.parse(...jsFiles)) {
495+
current++;
496+
onProgress?.(current, total, sf.sourcePath);
490497
parsed.push(sf);
491498
}
492499
}
@@ -498,6 +505,8 @@ export async function parseFiles(
498505
}
499506
const pkgParser = new PackageJsonParser({relativeTo: projectRoot});
500507
for await (const sf of pkgParser.parse(...packageJsonFiles)) {
508+
current++;
509+
onProgress?.(current, total, sf.sourcePath);
501510
parsed.push(sf);
502511
}
503512
}
@@ -509,6 +518,8 @@ export async function parseFiles(
509518
}
510519
const jsonParser = new JsonParser({relativeTo: projectRoot});
511520
for await (const sf of jsonParser.parse(...jsonFiles)) {
521+
current++;
522+
onProgress?.(current, total, sf.sourcePath);
512523
parsed.push(sf);
513524
}
514525
}

rewrite-javascript/rewrite/src/cli/rewrite.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,44 @@ import {
3434
parseRecipeSpec
3535
} from './cli-utils';
3636

37+
const isTTY = process.stdout.isTTY ?? false;
38+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
39+
40+
class Spinner {
41+
private frameIndex = 0;
42+
private interval: NodeJS.Timeout | null = null;
43+
private message = '';
44+
45+
start(message: string): void {
46+
if (!isTTY) return;
47+
this.message = message;
48+
this.render();
49+
this.interval = setInterval(() => this.render(), 80);
50+
}
51+
52+
update(message: string): void {
53+
this.message = message;
54+
if (!isTTY) return;
55+
this.render();
56+
}
57+
58+
private render(): void {
59+
const frame = SPINNER_FRAMES[this.frameIndex];
60+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
61+
process.stdout.write(`\r\x1b[K${frame} ${this.message}`);
62+
}
63+
64+
stop(): void {
65+
if (this.interval) {
66+
clearInterval(this.interval);
67+
this.interval = null;
68+
}
69+
if (isTTY) {
70+
process.stdout.write('\r\x1b[K');
71+
}
72+
}
73+
}
74+
3775
// Import language modules to register printers
3876
import '../text';
3977
import '../json';
@@ -127,9 +165,13 @@ async function main() {
127165
console.log(`Running recipe: ${recipe.name}`);
128166
}
129167

130-
// Discover source files
168+
const spinner = new Spinner();
131169
const projectRoot = process.cwd();
170+
171+
// Discover source files
172+
spinner.start('Discovering source files...');
132173
const sourceFiles = await discoverFiles(projectRoot, opts.verbose);
174+
spinner.stop();
133175

134176
if (sourceFiles.length === 0) {
135177
console.log('No source files found to process.');
@@ -141,7 +183,11 @@ async function main() {
141183
}
142184

143185
// Parse all source files
144-
const parsedFiles = await parseFiles(sourceFiles, projectRoot, opts.verbose);
186+
spinner.start(`Parsing ${sourceFiles.length} files...`);
187+
const parsedFiles = await parseFiles(sourceFiles, projectRoot, opts.verbose, (current, total, filePath) => {
188+
spinner.update(`Parsing [${current}/${total}] ${filePath}`);
189+
});
190+
spinner.stop();
145191

146192
if (parsedFiles.length === 0) {
147193
console.log('No files could be parsed.');
@@ -151,8 +197,18 @@ async function main() {
151197
// Run the recipe with streaming output
152198
const ctx = new ExecutionContext();
153199
let changeCount = 0;
200+
let processedCount = 0;
201+
const totalFiles = parsedFiles.length;
202+
203+
spinner.start(`Running recipe on ${totalFiles} files...`);
154204

155205
for await (const result of scheduleRunStreaming(recipe, parsedFiles, ctx)) {
206+
processedCount++;
207+
const currentPath = result.after?.sourcePath ?? result.before?.sourcePath ?? '';
208+
spinner.update(`Processing [${processedCount}/${totalFiles}] ${currentPath}`);
209+
210+
const statusMsg = `Processing [${processedCount}/${totalFiles}] ${currentPath}`;
211+
156212
if (opts.dryRun) {
157213
// Print colorized diff immediately (skip empty diffs)
158214
const diff = await result.diff();
@@ -162,7 +218,9 @@ async function main() {
162218
);
163219
if (hasChanges) {
164220
changeCount++;
221+
spinner.stop();
165222
console.log(colorizeDiff(diff));
223+
spinner.start(statusMsg);
166224
}
167225
} else {
168226
// Apply changes immediately
@@ -175,20 +233,26 @@ async function main() {
175233
await fsp.writeFile(filePath, content);
176234

177235
changeCount++;
236+
spinner.stop();
178237
if (result.before) {
179238
console.log(` Modified: ${result.after.sourcePath}`);
180239
} else {
181240
console.log(` Created: ${result.after.sourcePath}`);
182241
}
242+
spinner.start(statusMsg);
183243
} else if (result.before) {
184244
const filePath = path.join(projectRoot, result.before.sourcePath);
185245
await fsp.unlink(filePath);
186246
changeCount++;
247+
spinner.stop();
187248
console.log(` Deleted: ${result.before.sourcePath}`);
249+
spinner.start(statusMsg);
188250
}
189251
}
190252
}
191253

254+
spinner.stop();
255+
192256
if (changeCount === 0) {
193257
console.log('No changes to make.');
194258
} else if (opts.dryRun) {

rewrite-javascript/rewrite/src/javascript/format.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,9 +1083,9 @@ export class BlankLinesVisitor<P> extends JavaScriptVisitor<P> {
10831083
const parentKind = this.cursor.parent?.value.kind;
10841084
// Skip newline only for object literals (NewClass) - they should preserve single-line formatting
10851085
if (parentKind != J.Kind.NewClass) {
1086-
if (!draft.end.whitespace.includes("\n")) {
1087-
draft.end.whitespace = draft.end.whitespace.replace(/[ \t]+$/, '') + "\n";
1088-
}
1086+
draft.end = replaceLastWhitespace(draft.end, ws =>
1087+
ws.includes("\n") ? ws : ws.replace(/[ \t]+$/, '') + "\n"
1088+
);
10891089
}
10901090
});
10911091
}

rewrite-javascript/rewrite/test/javascript/format/format.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,22 @@ buf.slice();`
596596
// @formatter:on
597597
)
598598
})
599+
600+
test('inline comment after last statement in block should stay on same line', () => {
601+
return spec.rewriteRun(
602+
// @formatter:off
603+
//language=typescript
604+
typescript(
605+
`async function getMarkerOrSkip(fixtureSubDir: string): Promise<NodeResolutionResult> {
606+
const marker = await parseAndGetMarker(fixtureSubDir);
607+
if (!marker || marker.resolvedDependencies.length === 0) {
608+
// Skip test if we couldn't resolve dependencies (CLI not available or packages not installed)
609+
return null as any; // Will be caught by the conditional skip
610+
}
611+
return marker;
612+
}`
613+
)
614+
// @formatter:on
615+
)
616+
})
599617
});

0 commit comments

Comments
 (0)