Skip to content

Commit 12b64ec

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 c0e9770 commit 12b64ec

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
@@ -1792,6 +1792,14 @@ export interface WriteModeProps {
17921792
header: string;
17931793
needSchema: boolean;
17941794
generateSchemasInline?: () => string;
1795+
// Schema-to-tag map computed by `writeSpecs` when `schemas.splitByTags` is
1796+
// enabled. Mode writers forward it to `generateImportsForBuilder` so the
1797+
// `indexFiles: false` branch can route each schema import into its tag
1798+
// subdirectory instead of assuming a flat layout. `undefined` when
1799+
// `splitByTags` is disabled, in which case routing falls back to the flat
1800+
// layout. The `'.'` sentinel marks schemas referenced by 0 or 2+ tags
1801+
// (shared, kept at the schemas root).
1802+
schemaTagMap?: Map<string, string>;
17951803
}
17961804

17971805
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 {
@@ -108,6 +109,7 @@ export async function writeSplitMode({
108109
output,
109110
imports,
110111
relativeSchemasPath,
112+
schemaTagMap,
111113
);
112114

113115
implementationData += builder.imports({
@@ -261,6 +263,7 @@ export async function writeSplitMode({
261263
finalizeMockOptions.strictSchemaTypeNames,
262264
),
263265
mockRelativeSchemasPath,
266+
schemaTagMap,
264267
);
265268
let mockData = header;
266269
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
@@ -40,6 +40,7 @@ export async function writeSplitTagsMode({
4040
header,
4141
needSchema,
4242
generateSchemasInline,
43+
schemaTagMap,
4344
}: WriteModeProps): Promise<string[]> {
4445
const { filename, dirname, extension } = getFileInfo(output.target, {
4546
backupFilename: conventionName(
@@ -188,6 +189,7 @@ export async function writeSplitTagsMode({
188189
output,
189190
adjustedImports,
190191
relativeSchemasPath,
192+
schemaTagMap,
191193
);
192194

193195
implementationData += builder.imports({
@@ -348,6 +350,7 @@ export async function writeSplitTagsMode({
348350
finalizeMockOptions.strictSchemaTypeNames,
349351
),
350352
mockRelativeSchemasPath,
353+
schemaTagMap,
351354
);
352355

353356
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
@@ -755,26 +755,6 @@ export async function normalizeOptions(
755755
);
756756
}
757757

758-
// `schemas.splitByTags` routes schema files into per-tag subdirectories and
759-
// (when `indexFiles: false`) skips the root barrel. Operation files and the
760-
// consolidated `<schemas>/index.faker.ts` file both currently assume either
761-
// a flat schemas directory or a root barrel, so without `indexFiles: true`
762-
// they emit imports that don't resolve (e.g. `../model/pet` when the file
763-
// lives at `../model/pets/pet.ts`). Reject the combination until the import
764-
// resolvers are taught to honor `splitByTags` directly.
765-
if (
766-
isObject(normalizedOptions.output.schemas) &&
767-
normalizedOptions.output.schemas.splitByTags &&
768-
!normalizedOptions.output.indexFiles
769-
) {
770-
throw new Error(
771-
styleText(
772-
'red',
773-
`\`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\`.`,
774-
),
775-
);
776-
}
777-
778758
return normalizedOptions;
779759
}
780760

0 commit comments

Comments
 (0)