Skip to content

Commit 14f2894

Browse files
authored
Merge pull request #3438 from hey-api/copilot/fix-output-header-config
2 parents 2faab85 + c66d565 commit 14f2894

7 files changed

Lines changed: 150 additions & 6 deletions

File tree

.changeset/famous-pianos-attack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": patch
3+
---
4+
5+
**output**: fix: apply `output.header` to bundled files

packages/openapi-python/src/generate/__tests__/client.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
import path from 'node:path';
22

3+
/**
4+
* Replicates the outputHeaderToPrefix logic from generate/client.ts for testing.
5+
*/
6+
function outputHeaderToPrefix(header: unknown): string {
7+
if (header == null || typeof header === 'function') return '';
8+
const lines =
9+
typeof header === 'string'
10+
? header.split(/\r?\n/)
11+
: (header as string[]).flatMap((line) => line.split(/\r?\n/));
12+
const content = lines.join('\n');
13+
return content ? `${content}\n\n` : '';
14+
}
15+
16+
describe('outputHeaderToPrefix logic', () => {
17+
it('returns default comment for string header', () => {
18+
const result = outputHeaderToPrefix('# This file is auto-generated by @hey-api/openapi-python');
19+
expect(result).toBe('# This file is auto-generated by @hey-api/openapi-python\n\n');
20+
});
21+
22+
it('returns joined lines for array header', () => {
23+
const result = outputHeaderToPrefix([
24+
'# This file is auto-generated by @hey-api/openapi-python',
25+
'# type: ignore',
26+
]);
27+
expect(result).toBe(
28+
'# This file is auto-generated by @hey-api/openapi-python\n# type: ignore\n\n',
29+
);
30+
});
31+
32+
it('returns empty string for null header', () => {
33+
expect(outputHeaderToPrefix(null)).toBe('');
34+
});
35+
36+
it('returns empty string for undefined header', () => {
37+
expect(outputHeaderToPrefix(undefined)).toBe('');
38+
});
39+
40+
it('returns empty string for function header', () => {
41+
expect(outputHeaderToPrefix(() => '# dynamic')).toBe('');
42+
});
43+
44+
it('handles string with embedded newlines', () => {
45+
const result = outputHeaderToPrefix('# line1\n# line2');
46+
expect(result).toBe('# line1\n# line2\n\n');
47+
});
48+
});
49+
350
describe('isDevMode logic', () => {
451
const scenarios: ReadonlyArray<{
552
description: string;

packages/openapi-python/src/generate/client.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44

55
import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core';
6-
import type { DefinePlugin } from '@hey-api/shared';
6+
import type { DefinePlugin, OutputHeader } from '@hey-api/shared';
77
import { ensureDirSync } from '@hey-api/shared';
88

99
import type { Config } from '../config/types';
@@ -53,6 +53,21 @@ function getClientBundlePaths(pluginName: string): {
5353
};
5454
}
5555

56+
/**
57+
* Converts an {@link OutputHeader} value to a string prefix for file content.
58+
* Returns an empty string when the header is null, undefined, or a function
59+
* (functions require a render context which is not available for bundled files).
60+
*/
61+
function outputHeaderToPrefix(header: OutputHeader): string {
62+
if (header == null || typeof header === 'function') return '';
63+
const lines =
64+
typeof header === 'string'
65+
? header.split(/\r?\n/)
66+
: header.flatMap((line) => line.split(/\r?\n/));
67+
const content = lines.join('\n');
68+
return content ? `${content}\n\n` : '';
69+
}
70+
5671
/**
5772
* Returns absolute path to the client folder. This is hard-coded for now.
5873
*/
@@ -114,11 +129,13 @@ function renameFile({
114129

115130
function replaceImports({
116131
filePath,
132+
header,
117133
isDevMode,
118134
meta,
119135
renamed,
120136
}: {
121137
filePath: string;
138+
header?: string;
122139
isDevMode?: boolean;
123140
meta: ProjectRenderMeta;
124141
renamed: Map<string, string>;
@@ -148,9 +165,9 @@ function replaceImports({
148165
return replacedMatch;
149166
});
150167

151-
const header = '# This file is auto-generated by @hey-api/openapi-python\n\n';
168+
const fileHeader = header ?? '';
152169

153-
content = `${header}${content}`;
170+
content = `${fileHeader}${content}`;
154171

155172
fs.writeFileSync(filePath, content, 'utf8');
156173
}
@@ -159,18 +176,21 @@ function replaceImports({
159176
* Creates a `client` folder containing the same modules as the client package.
160177
*/
161178
export function generateClientBundle({
179+
header,
162180
meta,
163181
outputPath,
164182
plugin,
165183
project,
166184
}: {
185+
header?: OutputHeader;
167186
meta: ProjectRenderMeta;
168187
outputPath: string;
169188
plugin: DefinePlugin<Client.Config & { name: string }>['Config'];
170189
project?: IProject;
171190
}): Map<string, string> | undefined {
172191
const renamed = new Map<string, string>();
173192
const devMode = isDevMode();
193+
const headerPrefix = outputHeaderToPrefix(header);
174194

175195
// copy Hey API clients to output
176196
const isHeyApiClientPlugin = plugin.name.startsWith('@hey-api/client-');
@@ -222,6 +242,7 @@ export function generateClientBundle({
222242
for (const file of clientFiles) {
223243
replaceImports({
224244
filePath: path.resolve(clientOutputPath, file),
245+
header: headerPrefix,
225246
isDevMode: devMode,
226247
meta,
227248
renamed,

packages/openapi-python/src/generate/output.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function generateOutput(context: Context): Promise<void> {
2424
// not proud of this one
2525
// @ts-expect-error
2626
config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({
27+
header: config.output.header,
2728
meta: {
2829
importFileExtension: config.output.importFileExtension,
2930
},

packages/openapi-ts/src/generate/__tests__/client.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,52 @@
11
import path from 'node:path';
22

3+
/**
4+
* Replicates the outputHeaderToPrefix logic from generate/client.ts for testing.
5+
*/
6+
function outputHeaderToPrefix(header: unknown): string {
7+
if (header == null || typeof header === 'function') return '';
8+
const lines =
9+
typeof header === 'string'
10+
? header.split(/\r?\n/)
11+
: (header as string[]).flatMap((line) => line.split(/\r?\n/));
12+
const content = lines.join('\n');
13+
return content ? `${content}\n\n` : '';
14+
}
15+
16+
describe('outputHeaderToPrefix logic', () => {
17+
it('returns default comment for string header', () => {
18+
const result = outputHeaderToPrefix('// This file is auto-generated by @hey-api/openapi-ts');
19+
expect(result).toBe('// This file is auto-generated by @hey-api/openapi-ts\n\n');
20+
});
21+
22+
it('returns joined lines for array header', () => {
23+
const result = outputHeaderToPrefix([
24+
'// This file is auto-generated by @hey-api/openapi-ts',
25+
'// @ts-nocheck',
26+
]);
27+
expect(result).toBe(
28+
'// This file is auto-generated by @hey-api/openapi-ts\n// @ts-nocheck\n\n',
29+
);
30+
});
31+
32+
it('returns empty string for null header', () => {
33+
expect(outputHeaderToPrefix(null)).toBe('');
34+
});
35+
36+
it('returns empty string for undefined header', () => {
37+
expect(outputHeaderToPrefix(undefined)).toBe('');
38+
});
39+
40+
it('returns empty string for function header', () => {
41+
expect(outputHeaderToPrefix(() => '// dynamic')).toBe('');
42+
});
43+
44+
it('handles string with embedded newlines', () => {
45+
const result = outputHeaderToPrefix('// line1\n// line2');
46+
expect(result).toBe('// line1\n// line2\n\n');
47+
});
48+
});
49+
350
describe('isDevMode logic', () => {
451
const scenarios: ReadonlyArray<{
552
description: string;

packages/openapi-ts/src/generate/client.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44

55
import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core';
6-
import type { DefinePlugin } from '@hey-api/shared';
6+
import type { DefinePlugin, OutputHeader } from '@hey-api/shared';
77
import { ensureDirSync } from '@hey-api/shared';
88

99
import type { Config } from '../config/types';
@@ -53,6 +53,21 @@ function getClientBundlePaths(pluginName: string): {
5353
};
5454
}
5555

56+
/**
57+
* Converts an {@link OutputHeader} value to a string prefix for file content.
58+
* Returns an empty string when the header is null, undefined, or a function
59+
* (functions require a render context which is not available for bundled files).
60+
*/
61+
function outputHeaderToPrefix(header: OutputHeader): string {
62+
if (header == null || typeof header === 'function') return '';
63+
const lines =
64+
typeof header === 'string'
65+
? header.split(/\r?\n/)
66+
: header.flatMap((line) => line.split(/\r?\n/));
67+
const content = lines.join('\n');
68+
return content ? `${content}\n\n` : '';
69+
}
70+
5671
/**
5772
* Returns absolute path to the client folder. This is hard-coded for now.
5873
*/
@@ -114,11 +129,13 @@ function renameFile({
114129

115130
function replaceImports({
116131
filePath,
132+
header,
117133
isDevMode,
118134
meta,
119135
renamed,
120136
}: {
121137
filePath: string;
138+
header?: string;
122139
isDevMode?: boolean;
123140
meta: ProjectRenderMeta;
124141
renamed: Map<string, string>;
@@ -148,9 +165,9 @@ function replaceImports({
148165
return replacedMatch;
149166
});
150167

151-
const header = '// This file is auto-generated by @hey-api/openapi-ts\n\n';
168+
const fileHeader = header ?? '';
152169

153-
content = `${header}${content}`;
170+
content = `${fileHeader}${content}`;
154171

155172
fs.writeFileSync(filePath, content, 'utf8');
156173
}
@@ -159,18 +176,21 @@ function replaceImports({
159176
* Creates a `client` folder containing the same modules as the client package.
160177
*/
161178
export function generateClientBundle({
179+
header,
162180
meta,
163181
outputPath,
164182
plugin,
165183
project,
166184
}: {
185+
header?: OutputHeader;
167186
meta: ProjectRenderMeta;
168187
outputPath: string;
169188
plugin: DefinePlugin<Client.Config & { name: string }>['Config'];
170189
project?: IProject;
171190
}): Map<string, string> | undefined {
172191
const renamed = new Map<string, string>();
173192
const devMode = isDevMode();
193+
const headerPrefix = outputHeaderToPrefix(header);
174194

175195
// copy Hey API clients to output
176196
const isHeyApiClientPlugin = plugin.name.startsWith('@hey-api/client-');
@@ -211,6 +231,7 @@ export function generateClientBundle({
211231
for (const file of coreFiles) {
212232
replaceImports({
213233
filePath: path.resolve(coreOutputPath, file),
234+
header: headerPrefix,
214235
isDevMode: devMode,
215236
meta,
216237
renamed,
@@ -221,6 +242,7 @@ export function generateClientBundle({
221242
for (const file of clientFiles) {
222243
replaceImports({
223244
filePath: path.resolve(clientOutputPath, file),
245+
header: headerPrefix,
224246
isDevMode: devMode,
225247
meta,
226248
renamed,

packages/openapi-ts/src/generate/output.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function generateOutput(context: Context): Promise<void> {
2424
// not proud of this one
2525
// @ts-expect-error
2626
config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({
27+
header: config.output.header,
2728
meta: {
2829
importFileExtension: config.output.importFileExtension,
2930
},

0 commit comments

Comments
 (0)