Skip to content

Commit ca3d6c5

Browse files
committed
feat(core): add tagsSplitDeduplication option for barrel + shared type extraction
Add a new `tagsSplitDeduplication` output option (default: false) that extracts shared infrastructure types (e.g. HTTPStatusCode*) emitted by client header builders into a `common-types.ts` file, then generates a barrel `index.ts` with named re-exports for public types. When enabled (with `indexFiles: true`): - Shared types are collected across all per-tag files and deduplicated - Extracted to `<commonTypesFileName>.ts` (default: 'common-types') - Per-tag files import shared types from the common file - Barrel uses named re-exports for exported types + `export *` for tags When disabled (default): behavior is identical to before — shared types are inlined per-tag, no barrel, no extraction. The header builder API is backward compatible: `ClientHeaderBuilder` return type widens from `string` to `string | HeaderResult`. Existing builders returning `string` continue to work unchanged. Currently only `generateFetchHeader` emits `sharedTypes` (HTTPStatusCode* family). The extraction mechanism is generic — other header builders can adopt it incrementally. Remove stale barrel snapshots from the previous naive implementation (gated on `indexFiles` alone). The barrel now requires both `indexFiles` and `tagsSplitDeduplication` to be `true`. Related design discussion: #3553 Closes #3553
1 parent e768015 commit ca3d6c5

122 files changed

Lines changed: 2532 additions & 136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/content/docs/reference/configuration/output.mdx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,70 @@ import { getPetMock } from '@acme/data-layer/sdk/fakers';
618618

619619
Generate `index.ts` files for schemas.
620620

621+
## tagsSplitDeduplication
622+
623+
**Type:** `Boolean`
624+
**Default:** `false`
625+
626+
In `tags-split` mode, extract shared infrastructure types (e.g. `HTTPStatusCode*` emitted by the fetch client) into a single `common-types.ts` file and generate a barrel `index.ts` at the target root.
627+
628+
When enabled alongside [`indexFiles: true`](#indexfiles):
629+
630+
- Shared types that would otherwise be duplicated across per-tag files are collected and written once to `[commonTypesFileName].ts`
631+
- Each per-tag file imports shared types from the common file instead of declaring them inline
632+
- A barrel `index.ts` is generated with named re-exports for public shared types plus `export *` re-exports for each per-tag implementation file
633+
634+
```ts title="orval.config.ts"
635+
export default defineConfig({
636+
petstore: {
637+
output: {
638+
mode: 'tags-split',
639+
target: './src/api/endpoints.ts',
640+
schemas: './src/api/model',
641+
client: 'fetch',
642+
indexFiles: true,
643+
tagsSplitDeduplication: true,
644+
},
645+
},
646+
});
647+
```
648+
649+
Resulting structure:
650+
651+
```
652+
src/api/
653+
├── common-types.ts ← shared types extracted once
654+
├── index.ts ← barrel with named + wildcard re-exports
655+
├── pets/
656+
│ └── pets.ts ← import type { ... } from '../common-types'
657+
└── health/
658+
└── health.ts ← import type { ... } from '../common-types'
659+
```
660+
661+
When disabled (default), shared types are inlined per-tag and no barrel is generated — identical to previous behavior.
662+
663+
Suppressed when [`workspace`](#workspace) is set (the workspace barrel handles aggregation).
664+
665+
## commonTypesFileName
666+
667+
**Type:** `String`
668+
**Default:** `'common-types'`
669+
670+
The file name (without extension) used for the shared types file when [`tagsSplitDeduplication`](#tagssplitdeduplication) is enabled.
671+
672+
```ts title="orval.config.ts"
673+
export default defineConfig({
674+
petstore: {
675+
output: {
676+
mode: 'tags-split',
677+
indexFiles: true,
678+
tagsSplitDeduplication: true,
679+
commonTypesFileName: 'shared', // generates shared.ts
680+
},
681+
},
682+
});
683+
```
684+
621685
## docs
622686

623687
**Type:** `Boolean | Object`

docs/content/docs/zh/reference/configuration/output.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ export default defineConfig({
139139

140140
控制是否生成 index 文件。拆分模式下通常保持开启,便于统一导入。
141141

142+
## tagsSplitDeduplication
143+
144+
**类型:** `Boolean`
145+
**默认值:** `false`
146+
147+
`tags-split` 模式下,将共享的基础设施类型(如 fetch 客户端的 `HTTPStatusCode*`)提取到单独的 `common-types.ts` 文件中,并在目标根目录生成 barrel `index.ts`。需要同时开启 `indexFiles: true`
148+
149+
## commonTypesFileName
150+
151+
**类型:** `String`
152+
**默认值:** `'common-types'`
153+
154+
`tagsSplitDeduplication` 开启时,共享类型文件的文件名(不含扩展名)。
155+
142156
## docs
143157

144158
生成接口文档相关输出。只有需要把生成产物和文档系统集成时再开启。

packages/angular/src/http-client.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ const createOutput = (
147147
optionsParamRequired: false,
148148
unionAddMissingProperties: false,
149149
propertySortOrder: 'Specification',
150+
tagsSplitDeduplication: false,
151+
commonTypesFileName: 'common-types',
150152
factoryMethods: {
151153
functionNamePrefix: 'create',
152154
mode: 'single',

packages/angular/src/http-resource.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ const createOutput = (
157157
optionsParamRequired: false,
158158
unionAddMissingProperties: false,
159159
propertySortOrder: 'Specification',
160+
tagsSplitDeduplication: false,
161+
commonTypesFileName: 'common-types',
160162
factoryMethods: {
161163
functionNamePrefix: 'create',
162164
mode: 'single',
@@ -940,7 +942,7 @@ describe('angular httpResource generator', () => {
940942
routeRegistry.set('getPetById', '/api/pets/${petId}');
941943
routeRegistry.set('createPet', '/api/pets');
942944

943-
const header = generateHttpResourceHeader({
945+
const rawHeader = generateHttpResourceHeader({
944946
title: 'PetService',
945947
isRequestOptions: true,
946948
isMutator: false,
@@ -952,6 +954,9 @@ describe('angular httpResource generator', () => {
952954
clientImplementation: '',
953955
} as never);
954956

957+
const header =
958+
typeof rawHeader === 'string' ? rawHeader : rawHeader.implementation;
959+
955960
expect(header.match(/type AngularHttpParamValue =/g)).toHaveLength(1);
956961
expect(header.match(/preserveRequiredNullables = false,/g)).toHaveLength(
957962
1,
@@ -2633,7 +2638,7 @@ describe('angular httpResource generator', () => {
26332638
const verbOption = createVerbOption();
26342639
routeRegistry.set('getPetById', '/api/pets/${petId}');
26352640

2636-
return generateHttpResourceHeader({
2641+
const result = generateHttpResourceHeader({
26372642
title: 'PetService',
26382643
isRequestOptions: true,
26392644
isMutator: false,
@@ -2644,6 +2649,8 @@ describe('angular httpResource generator', () => {
26442649
verbOptions: { getPetById: verbOption },
26452650
clientImplementation: '',
26462651
} as never);
2652+
2653+
return typeof result === 'string' ? result : result.implementation;
26472654
};
26482655

26492656
it('encodes the signal path parameter when urlEncodeParameters is true', () => {

packages/core/src/test-utils/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export function createTestContextSpec({
4747
optionsParamRequired: false,
4848
propertySortOrder: PropertySortOrder.SPECIFICATION,
4949
factoryMethods: undefined,
50+
tagsSplitDeduplication: false,
51+
commonTypesFileName: 'common-types',
5052
override: {
5153
title: undefined,
5254
transformer: undefined,

packages/core/src/test-utils/split-modes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export const createSplitModeOutput = (
7575
unionAddMissingProperties: false,
7676
optionsParamRequired: false,
7777
propertySortOrder: 'Alphabetical',
78+
tagsSplitDeduplication: false,
79+
commonTypesFileName: 'common-types',
7880
override: {
7981
tags: {},
8082
operations: {},

packages/core/src/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export interface NormalizedOutputOptions {
6868
optionsParamRequired: boolean;
6969
propertySortOrder: PropertySortOrder;
7070
factoryMethods?: NormalizedFactoryMethodsOptions;
71+
tagsSplitDeduplication: boolean;
72+
commonTypesFileName: string;
7173
}
7274

7375
export interface NormalizedParamsSerializerOptions {
@@ -360,6 +362,8 @@ export interface OutputOptions {
360362
optionsParamRequired?: boolean;
361363
propertySortOrder?: PropertySortOrder;
362364
factoryMethods?: FactoryMethodsOptions;
365+
tagsSplitDeduplication?: boolean;
366+
commonTypesFileName?: string;
363367
}
364368

365369
export interface InputFiltersOptions {
@@ -1396,6 +1400,7 @@ export interface GeneratorTarget {
13961400
paramsSerializer?: GeneratorMutator[];
13971401
paramsFilter?: GeneratorMutator[];
13981402
fetchReviver?: GeneratorMutator[];
1403+
sharedTypes?: SharedTypeDeclaration[];
13991404
}
14001405

14011406
export interface GeneratorTargetFull {
@@ -1409,6 +1414,7 @@ export interface GeneratorTargetFull {
14091414
paramsSerializer?: GeneratorMutator[];
14101415
paramsFilter?: GeneratorMutator[];
14111416
fetchReviver?: GeneratorMutator[];
1417+
sharedTypes?: SharedTypeDeclaration[];
14121418
}
14131419

14141420
export interface GeneratorOperation {
@@ -1507,6 +1513,17 @@ export type ClientExtraFilesBuilder = (
15071513
context: ContextSpec,
15081514
) => Promise<ClientFileBuilder[]>;
15091515

1516+
export interface SharedTypeDeclaration {
1517+
name: string;
1518+
exported: boolean;
1519+
code: string;
1520+
}
1521+
1522+
export type HeaderResult = {
1523+
implementation: string;
1524+
sharedTypes?: SharedTypeDeclaration[];
1525+
};
1526+
15101527
export type ClientHeaderBuilder = (params: {
15111528
title: string;
15121529
isRequestOptions: boolean;
@@ -1520,7 +1537,7 @@ export type ClientHeaderBuilder = (params: {
15201537
tag?: string;
15211538
isDefaultTagBucket?: boolean;
15221539
clientImplementation: string;
1523-
}) => string;
1540+
}) => string | HeaderResult;
15241541

15251542
export type ClientFooterBuilder = (params: {
15261543
noFunction?: boolean | undefined;
@@ -1759,6 +1776,7 @@ export interface GeneratorApiOperations {
17591776
export interface GeneratorClientExtra {
17601777
implementation: string;
17611778
implementationMock: string;
1779+
sharedTypes?: SharedTypeDeclaration[];
17621780
}
17631781

17641782
export type GeneratorClientTitle = (data: {

packages/core/src/writers/split-tags-mode.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,14 @@ describe('writeSplitTagsMode — barrel index.ts at target root (#3553)', () =>
320320
fs.removeSync(tmpDir);
321321
});
322322

323-
it('writes index.ts when indexFiles is true', async () => {
323+
it('writes index.ts when indexFiles and tagsSplitDeduplication are true', async () => {
324324
const target = path.join(tmpDir, 'petstore.ts');
325325
const props = {
326326
...createSplitModeProps(target),
327327
output: createSplitModeOutput(target, {
328328
mode: OutputMode.TAGS_SPLIT,
329329
indexFiles: true,
330+
tagsSplitDeduplication: true,
330331
}),
331332
};
332333

@@ -340,13 +341,32 @@ describe('writeSplitTagsMode — barrel index.ts at target root (#3553)', () =>
340341
expect(content).toContain("export * from './pets/pets'");
341342
});
342343

344+
it('does not write index.ts when tagsSplitDeduplication is false', async () => {
345+
const target = path.join(tmpDir, 'petstore.ts');
346+
const props = {
347+
...createSplitModeProps(target),
348+
output: createSplitModeOutput(target, {
349+
mode: OutputMode.TAGS_SPLIT,
350+
indexFiles: true,
351+
tagsSplitDeduplication: false,
352+
}),
353+
};
354+
355+
const paths = await writeSplitTagsMode({ ...props, needSchema: false });
356+
357+
const indexPath = path.join(tmpDir, 'index.ts');
358+
expect(paths).not.toContain(indexPath);
359+
expect(fs.existsSync(indexPath)).toBe(false);
360+
});
361+
343362
it('does not write index.ts when indexFiles is false', async () => {
344363
const target = path.join(tmpDir, 'petstore.ts');
345364
const props = {
346365
...createSplitModeProps(target),
347366
output: createSplitModeOutput(target, {
348367
mode: OutputMode.TAGS_SPLIT,
349368
indexFiles: false,
369+
tagsSplitDeduplication: true,
350370
}),
351371
};
352372

@@ -364,6 +384,7 @@ describe('writeSplitTagsMode — barrel index.ts at target root (#3553)', () =>
364384
output: createSplitModeOutput(target, {
365385
mode: OutputMode.TAGS_SPLIT,
366386
indexFiles: true,
387+
tagsSplitDeduplication: true,
367388
}),
368389
};
369390

@@ -380,6 +401,7 @@ describe('writeSplitTagsMode — barrel index.ts at target root (#3553)', () =>
380401
output: createSplitModeOutput(target, {
381402
mode: OutputMode.TAGS_SPLIT,
382403
indexFiles: true,
404+
tagsSplitDeduplication: true,
383405
workspace: path.join(tmpDir, 'workspace'),
384406
}),
385407
};
@@ -390,4 +412,22 @@ describe('writeSplitTagsMode — barrel index.ts at target root (#3553)', () =>
390412
expect(paths).not.toContain(indexPath);
391413
expect(fs.existsSync(indexPath)).toBe(false);
392414
});
415+
416+
it('does not write common-types.ts when no shared types are present', async () => {
417+
const target = path.join(tmpDir, 'petstore.ts');
418+
const props = {
419+
...createSplitModeProps(target),
420+
output: createSplitModeOutput(target, {
421+
mode: OutputMode.TAGS_SPLIT,
422+
indexFiles: true,
423+
tagsSplitDeduplication: true,
424+
}),
425+
};
426+
427+
const paths = await writeSplitTagsMode({ ...props, needSchema: false });
428+
429+
const commonTypesPath = path.join(tmpDir, 'common-types.ts');
430+
expect(paths).not.toContain(commonTypesPath);
431+
expect(fs.existsSync(commonTypesPath)).toBe(false);
432+
});
393433
});

0 commit comments

Comments
 (0)