Skip to content

Commit d5c8cd9

Browse files
committed
fix(schemas): support splitByTags with indexFiles:false
When schemas are split by tag and there is no root barrel, operation files (pets/pets.ts) and the consolidated <schemas>/index.faker.ts file both previously emitted schema imports that didn't resolve. Operation files imported '../model/pet' when the file lived at '../model/pets/pet.ts'; the faker factory file imported types from '.' when no root index.ts existed. The root cause was that generateImportsForBuilder (used by every mode writer) and writeFakerSchemaMocks both assumed a flat schemas directory or a root barrel. Neither consulted the schema-to-tag map computed by writeSchemasTagsSplit. Compute the schema-to-tag map once in writeSpecs and thread it through: - WriteModeProps: new optional schemaTagMap field - generateImportsForBuilder: optional schemaTagMap param; the indexFiles:false branch routes each import into its tag subdir (or keeps shared schemas at the schemas root) - single-mode / split-mode / tags-mode / split-tags-mode: forward schemaTagMap at all 13 generateImportsForBuilder call sites - writeFakerSchemaMocks: same per-schema routing for the consolidated index.faker.ts file under indexFiles:false Removes the validation guard that previously rejected the combination, since the underlying import resolution now handles it correctly. - packages/core/src/types.ts: add schemaTagMap? to WriteModeProps - packages/core/src/writers/generate-imports-for-builder.ts: optional schemaTagMap param + tag-aware routing in indexFiles:false branch - packages/core/src/writers/{single,split,tags,split-tags}-mode.ts: destructure schemaTagMap from WriteModeProps, forward to call sites - packages/orval/src/write-specs.ts: hoist schemaTagMap computation to writeSpecs scope; thread to writeFakerSchemaMocks and writeMode; per-schema routing in writeFakerSchemaMocks under indexFiles:false - packages/orval/src/utils/options.ts: remove the guard - packages/orval/src/utils/options.test.ts: remove guard tests - packages/core/src/writers/generate-imports-for-builder.test.ts: add splitByTags: false to two orval-labs#3618 schemasImportPath test cases that now require it (those test cases predate splitByTags becoming required in NormalizedSchemaOptions) - docs/content/docs/reference/configuration/output.mdx: drop the indexFiles:true requirement note under splitByTags - tests/configs/axios.config.ts: new splitByTagsFakerSchemasNoIndex config exercising the previously-broken combination - tests/__snapshots__/axios/split-by-tags-faker-schemas-no-index/: snapshots for the new config - tests/api-generation.spec.ts: 3 regression tests pinning per-tag import paths for operation files, faker factory file, and tag isolation
1 parent daea728 commit d5c8cd9

29 files changed

Lines changed: 641 additions & 145 deletions

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,10 +224,6 @@ import type { Error } from '../error';
224224
within their tag directories automatically.
225225
- Schema-to-tag mapping is transitive: if `Pet` imports `Dog` which imports
226226
`Dachshund`, all three land in the same directory.
227-
- Requires `indexFiles: true` (the default). The operation writers and the
228-
consolidated `<schemas>/index.faker.ts` file currently route schema imports
229-
through the schemas root barrel, which is not generated when `indexFiles`
230-
is false.
231227

232228
### Validation of `importPath`
233229

packages/core/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,14 @@ export interface WriteModeProps {
17501750
header: string;
17511751
needSchema: boolean;
17521752
generateSchemasInline?: () => string;
1753+
// Schema-to-tag map computed by `writeSpecs` when `schemas.splitByTags` is
1754+
// enabled. Mode writers forward it to `generateImportsForBuilder` so the
1755+
// `indexFiles: false` branch can route each schema import into its tag
1756+
// subdirectory instead of assuming a flat layout. `undefined` when
1757+
// `splitByTags` is disabled, in which case routing falls back to the flat
1758+
// layout. The `'.'` sentinel marks schemas referenced by 0 or 2+ tags
1759+
// (shared, kept at the schemas root).
1760+
schemaTagMap?: Map<string, string>;
17531761
}
17541762

17551763
export interface GeneratorApiOperations {

packages/core/src/writers/generate-imports-for-builder.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ describe('generateImportsForBuilder', () => {
335335
path: '/libs/models',
336336
type: 'typescript',
337337
importPath: '@acme/models',
338+
splitByTags: false,
338339
},
339340
mock: {
340341
indexMockFiles: false,
@@ -373,6 +374,7 @@ describe('generateImportsForBuilder', () => {
373374
path: '/libs/models',
374375
type: 'typescript',
375376
importPath: '@acme/models',
377+
splitByTags: false,
376378
},
377379
mock: {
378380
indexMockFiles: false,

packages/core/src/writers/generate-imports-for-builder.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export function generateImportsForBuilder(
2020
output: NormalizedOutputOptions,
2121
imports: readonly GeneratorImport[],
2222
relativeSchemasPath: string,
23+
// Schema→tag map computed by `writeSpecs` when `schemas.splitByTags` is
24+
// enabled. Used only in the `indexFiles: false` branch to insert each
25+
// schema's tag subdirectory into the import path. `'.'` is the sentinel
26+
// for shared schemas (referenced by 0 or 2+ tags).
27+
schemaTagMap?: Map<string, string>,
2328
): GeneratorDependency[] {
2429
const isPackageImport =
2530
isObject(output.schemas) && !!output.schemas.importPath;
@@ -93,9 +98,17 @@ export function generateImportsForBuilder(
9398
const importExtension = isPackageImport
9499
? ''
95100
: getImportExtension(output.fileExtension, output.tsconfig);
101+
// When schemas are split by tag, route each import into its tag
102+
// subdirectory. Schemas referenced by 0 or 2+ tags land at the schemas
103+
// root (sentinel `'.'`); their path is unchanged from the flat layout.
104+
// The lookup uses the original schema name (`schemaName` preferred),
105+
// not the TS identifier, because `buildSchemaTagMap` keys on the
106+
// original name.
107+
const tagDir = schemaTagMap?.get(baseName);
108+
const tagSegment = tagDir && tagDir !== '.' ? `${tagDir}/` : '';
96109
const dependency = upath.joinSafe(
97110
relativeSchemasPath,
98-
`${normalizedName}${suffix}${importExtension}`,
111+
`${tagSegment}${normalizedName}${suffix}${importExtension}`,
99112
);
100113

101114
if (!importsByDependency.has(dependency)) {

packages/core/src/writers/single-mode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export async function writeSingleMode({
3636
header,
3737
needSchema,
3838
generateSchemasInline,
39+
schemaTagMap,
3940
}: WriteModeProps): Promise<string[]> {
4041
try {
4142
const {
@@ -143,6 +144,7 @@ export async function writeSingleMode({
143144
output,
144145
normalizedImports,
145146
relativeSchemasPath,
147+
schemaTagMap,
146148
)
147149
: generateImportsForBuilder(
148150
output,
@@ -191,6 +193,7 @@ export async function writeSingleMode({
191193
output,
192194
filteredMockImports,
193195
relativeSchemasPath,
196+
schemaTagMap,
194197
)
195198
: generateImportsForBuilder(
196199
output,
@@ -309,6 +312,7 @@ export async function writeSingleMode({
309312
output,
310313
mockOutput.imports,
311314
mockRelativeSchemasPath,
315+
schemaTagMap,
312316
)
313317
: generateImportsForBuilder(
314318
output,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export async function writeSplitMode({
3434
header,
3535
needSchema,
3636
generateSchemasInline,
37+
schemaTagMap,
3738
}: WriteModeProps): Promise<string[]> {
3839
try {
3940
const {
@@ -99,6 +100,7 @@ export async function writeSplitMode({
99100
output,
100101
imports,
101102
relativeSchemasPath,
103+
schemaTagMap,
102104
);
103105

104106
implementationData += builder.imports({
@@ -252,6 +254,7 @@ export async function writeSplitMode({
252254
finalizeMockOptions.strictSchemaTypeNames,
253255
),
254256
mockRelativeSchemasPath,
257+
schemaTagMap,
255258
);
256259
let mockData = header;
257260
mockData += builder.importsMock({

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function writeSplitTagsMode({
3535
header,
3636
needSchema,
3737
generateSchemasInline,
38+
schemaTagMap,
3839
}: WriteModeProps): Promise<string[]> {
3940
const { filename, dirname, extension } = getFileInfo(output.target, {
4041
backupFilename: conventionName(
@@ -138,6 +139,7 @@ export async function writeSplitTagsMode({
138139
output,
139140
adjustedImports,
140141
relativeSchemasPath,
142+
schemaTagMap,
141143
);
142144

143145
implementationData += builder.imports({
@@ -298,6 +300,7 @@ export async function writeSplitTagsMode({
298300
finalizeMockOptions.strictSchemaTypeNames,
299301
),
300302
mockRelativeSchemasPath,
303+
schemaTagMap,
301304
);
302305

303306
let mockData = header;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export async function writeTagsMode({
4242
header,
4343
needSchema,
4444
generateSchemasInline,
45+
schemaTagMap,
4546
}: WriteModeProps): Promise<string[]> {
4647
const {
4748
path: targetPath,
@@ -168,6 +169,7 @@ export async function writeTagsMode({
168169
output,
169170
normalizedImports,
170171
schemasPathRelative,
172+
schemaTagMap,
171173
);
172174

173175
data += builder.imports({
@@ -202,6 +204,7 @@ export async function writeTagsMode({
202204
),
203205
),
204206
schemasPathRelative,
207+
schemaTagMap,
205208
);
206209

207210
data += builder.importsMock({
@@ -359,6 +362,7 @@ export async function writeTagsMode({
359362
finalizeMockOptions.strictSchemaTypeNames,
360363
),
361364
mockRelativeSchemasPath,
365+
schemaTagMap,
362366
);
363367

364368
let mockData = header;

packages/orval/src/utils/options.test.ts

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,104 +1364,4 @@ describe('normalizeOptions', () => {
13641364
}
13651365
});
13661366
});
1367-
1368-
describe('schemas.splitByTags with indexFiles', () => {
1369-
it('rejects splitByTags + indexFiles:false', async () => {
1370-
const workspace = await createTempWorkspace();
1371-
1372-
try {
1373-
await expect(
1374-
normalizeOptions(
1375-
{
1376-
input: {
1377-
target: {
1378-
openapi: '3.1.0',
1379-
info: { title: 'Test', version: '1.0.0' },
1380-
paths: {},
1381-
},
1382-
},
1383-
output: {
1384-
target: './generated.ts',
1385-
schemas: {
1386-
path: './models',
1387-
type: 'typescript',
1388-
splitByTags: true,
1389-
},
1390-
indexFiles: false,
1391-
},
1392-
},
1393-
workspace,
1394-
),
1395-
).rejects.toThrow(/splitByTags.*requires.*indexFiles.*true/s);
1396-
} finally {
1397-
await rm(workspace, { recursive: true, force: true });
1398-
}
1399-
});
1400-
1401-
it('accepts splitByTags + indexFiles:true (default)', async () => {
1402-
const workspace = await createTempWorkspace();
1403-
1404-
try {
1405-
const normalized = await normalizeOptions(
1406-
{
1407-
input: {
1408-
target: {
1409-
openapi: '3.1.0',
1410-
info: { title: 'Test', version: '1.0.0' },
1411-
paths: {},
1412-
},
1413-
},
1414-
output: {
1415-
target: './generated.ts',
1416-
schemas: {
1417-
path: './models',
1418-
type: 'typescript',
1419-
splitByTags: true,
1420-
},
1421-
},
1422-
},
1423-
workspace,
1424-
);
1425-
1426-
expect(normalized.output.schemas).toMatchObject({
1427-
splitByTags: true,
1428-
});
1429-
// `indexFiles` defaults to true; splitByTags should be accepted.
1430-
expect(normalized.output.indexFiles).toBe(true);
1431-
} finally {
1432-
await rm(workspace, { recursive: true, force: true });
1433-
}
1434-
});
1435-
1436-
it('does not reject indexFiles:false when splitByTags is not set', async () => {
1437-
const workspace = await createTempWorkspace();
1438-
1439-
try {
1440-
const normalized = await normalizeOptions(
1441-
{
1442-
input: {
1443-
target: {
1444-
openapi: '3.1.0',
1445-
info: { title: 'Test', version: '1.0.0' },
1446-
paths: {},
1447-
},
1448-
},
1449-
output: {
1450-
target: './generated.ts',
1451-
schemas: {
1452-
path: './models',
1453-
type: 'typescript',
1454-
},
1455-
indexFiles: false,
1456-
},
1457-
},
1458-
workspace,
1459-
);
1460-
1461-
expect(normalized.output.indexFiles).toBe(false);
1462-
} finally {
1463-
await rm(workspace, { recursive: true, force: true });
1464-
}
1465-
});
1466-
});
14671367
});

packages/orval/src/utils/options.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -750,26 +750,6 @@ export async function normalizeOptions(
750750
);
751751
}
752752

753-
// `schemas.splitByTags` routes schema files into per-tag subdirectories and
754-
// (when `indexFiles: false`) skips the root barrel. Operation files and the
755-
// consolidated `<schemas>/index.faker.ts` file both currently assume either
756-
// a flat schemas directory or a root barrel, so without `indexFiles: true`
757-
// they emit imports that don't resolve (e.g. `../model/pet` when the file
758-
// lives at `../model/pets/pet.ts`). Reject the combination until the import
759-
// resolvers are taught to honor `splitByTags` directly.
760-
if (
761-
isObject(normalizedOptions.output.schemas) &&
762-
normalizedOptions.output.schemas.splitByTags &&
763-
!normalizedOptions.output.indexFiles
764-
) {
765-
throw new Error(
766-
styleText(
767-
'red',
768-
`\`schemas.splitByTags\` currently requires \`output.indexFiles: true\`. With \`indexFiles: false\`, operation files and the consolidated faker factory file emit schema imports that don't resolve against the per-tag layout. Set \`indexFiles: true\` (the default) or remove \`splitByTags\`.`,
769-
),
770-
);
771-
}
772-
773753
return normalizedOptions;
774754
}
775755

0 commit comments

Comments
 (0)