Skip to content

Commit ef151a0

Browse files
pullfrog[bot]mrlubos
authored andcommitted
fix: use ConfigError, guard null status, skip empty output dirs
1 parent e69d79f commit ef151a0

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

packages/shared/src/config/output/__tests__/postprocess.test.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import fs from 'node:fs';
2+
13
import { sync } from 'cross-spawn';
24
import { vi } from 'vitest';
35

6+
import { ConfigError } from '../../../error';
47
import { postprocessOutput } from '../postprocess';
58

69
vi.mock('cross-spawn');
10+
vi.mock('node:fs');
711

812
const mockSync = vi.mocked(sync);
13+
const mockExistsSync = vi.mocked(fs.existsSync);
14+
const mockReaddirSync = vi.mocked(fs.readdirSync);
915

1016
const baseConfig = {
1117
path: '/output',
@@ -17,13 +23,39 @@ const noopPostProcessors = {};
1723
describe('postprocessOutput', () => {
1824
beforeEach(() => {
1925
vi.clearAllMocks();
26+
mockExistsSync.mockReturnValue(true);
27+
mockReaddirSync.mockReturnValue(['index.ts'] as any);
2028
});
2129

2230
it('should not call sync when postProcess is empty', () => {
2331
postprocessOutput(baseConfig, noopPostProcessors, '');
2432
expect(mockSync).not.toHaveBeenCalled();
2533
});
2634

35+
it('should not call sync when output directory does not exist', () => {
36+
mockExistsSync.mockReturnValue(false);
37+
38+
postprocessOutput(
39+
{ ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'prettier' }] },
40+
noopPostProcessors,
41+
'',
42+
);
43+
44+
expect(mockSync).not.toHaveBeenCalled();
45+
});
46+
47+
it('should not call sync when output directory is empty', () => {
48+
mockReaddirSync.mockReturnValue([] as any);
49+
50+
postprocessOutput(
51+
{ ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'prettier' }] },
52+
noopPostProcessors,
53+
'',
54+
);
55+
56+
expect(mockSync).not.toHaveBeenCalled();
57+
});
58+
2759
it('should call sync with command and resolved args', () => {
2860
mockSync.mockReturnValue({ error: undefined, status: 0 } as any);
2961

@@ -48,7 +80,20 @@ describe('postprocessOutput', () => {
4880
expect(mockSync).toHaveBeenCalledWith('prettier', ['/my/output', '--write']);
4981
});
5082

51-
it('should throw when the process fails to spawn (e.g., ENOENT)', () => {
83+
it('should throw ConfigError when the process fails to spawn (e.g., ENOENT)', () => {
84+
const spawnError = new Error('spawnSync oxfmt ENOENT');
85+
mockSync.mockReturnValue({ error: spawnError, status: null } as any);
86+
87+
expect(() =>
88+
postprocessOutput(
89+
{ ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'oxfmt' }] },
90+
noopPostProcessors,
91+
'',
92+
),
93+
).toThrow(ConfigError);
94+
});
95+
96+
it('should include the error message when the process fails to spawn', () => {
5297
const spawnError = new Error('spawnSync oxfmt ENOENT');
5398
mockSync.mockReturnValue({ error: spawnError, status: null } as any);
5499

@@ -77,7 +122,19 @@ describe('postprocessOutput', () => {
77122
).toThrow('Post-processor "My Formatter" failed to run: spawnSync my-formatter ENOENT');
78123
});
79124

80-
it('should throw when the process exits with a non-zero status code', () => {
125+
it('should throw ConfigError when the process exits with a non-zero status code', () => {
126+
mockSync.mockReturnValue({ error: undefined, status: 1, stderr: Buffer.from('') } as any);
127+
128+
expect(() =>
129+
postprocessOutput(
130+
{ ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'prettier' }] },
131+
noopPostProcessors,
132+
'',
133+
),
134+
).toThrow(ConfigError);
135+
});
136+
137+
it('should include exit code in error message', () => {
81138
mockSync.mockReturnValue({ error: undefined, status: 1, stderr: Buffer.from('') } as any);
82139

83140
expect(() =>
@@ -105,6 +162,18 @@ describe('postprocessOutput', () => {
105162
).toThrow('Post-processor "biome" exited with code 2:\nerror: file not found');
106163
});
107164

165+
it('should not throw when the process is killed by a signal (null status)', () => {
166+
mockSync.mockReturnValue({ error: undefined, signal: 'SIGTERM', status: null } as any);
167+
168+
expect(() =>
169+
postprocessOutput(
170+
{ ...baseConfig, postProcess: [{ args: ['{{path}}'], command: 'prettier' }] },
171+
noopPostProcessors,
172+
'',
173+
),
174+
).not.toThrow();
175+
});
176+
108177
it('should skip unknown string preset processors', () => {
109178
postprocessOutput({ ...baseConfig, postProcess: ['unknown-preset'] }, noopPostProcessors, '');
110179
expect(mockSync).not.toHaveBeenCalled();

packages/shared/src/config/output/postprocess.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import fs from 'node:fs';
2+
13
import colors from 'ansi-colors';
24
import { sync } from 'cross-spawn';
35

6+
import { ConfigError } from '../../error';
7+
48
type Output = {
59
/**
610
* The absolute path to the output folder.
@@ -50,6 +54,15 @@ export function postprocessOutput(
5054
postProcessors: Record<string, PostProcessor>,
5155
jobPrefix: string,
5256
): void {
57+
if (!config.postProcess.length) {
58+
return;
59+
}
60+
61+
// skip post-processing when the output directory doesn't exist or is empty
62+
if (!fs.existsSync(config.path) || fs.readdirSync(config.path).length === 0) {
63+
return;
64+
}
65+
5366
for (const processor of config.postProcess) {
5467
const resolved = typeof processor === 'string' ? postProcessors[processor] : processor;
5568

@@ -63,16 +76,16 @@ export function postprocessOutput(
6376
const result = sync(resolved.command, args);
6477

6578
if (result.error) {
66-
throw new Error(`Post-processor "${name}" failed to run: ${result.error.message}`);
79+
throw new ConfigError(`Post-processor "${name}" failed to run: ${result.error.message}`);
6780
}
6881

69-
if (result.status !== 0) {
82+
if (result.status !== null && result.status !== 0) {
7083
let message = `Post-processor "${name}" exited with code ${result.status}`;
7184
const stderr = result.stderr?.toString().trim();
7285
if (stderr) {
7386
message += `:\n${stderr}`;
7487
}
75-
throw new Error(message);
88+
throw new ConfigError(message);
7689
}
7790
}
7891
}

0 commit comments

Comments
 (0)