Skip to content

Commit c01dfca

Browse files
authored
fix(core): recover schema-factory imports in single-mode mock writer (#3648)
* fix(core): recover schema-factory imports in single-mode mock writer (#3627) single-mode.ts was the only writer missing the collectRecoveredSchemaFactoryImports + mergeGeneratorImports recovery pattern added to split/tags/split-tags in 66af3e2. On wide specs with faker schemas:true, shared-array import aggregation could strip get<X>Mock() factory imports, producing uncompilable single-mode mock output. Inline branch: added per-mockOutput finalization, recovery scan, and merge before importsMock. Also passes the finalized implementation to builder.importsMock (was passing raw mockOutput.implementation — matches the other three writers). De-inlined branch: reordered so finalization happens before importsMockForBuilder (was computed before, making recovery impossible), and added the same recovery + merge pipeline. * test(core): assert recovered import statement in single-mode de-inlined test toContain('getPetMock') passes trivially since the implementation body already calls getPetMock(). Switch to a regex that matches the import statement, proving the import was actually recovered.
1 parent 697be27 commit c01dfca

2 files changed

Lines changed: 196 additions & 11 deletions

File tree

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

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import {
99
createSplitModeOutput,
1010
createSplitModeProps,
1111
} from '../test-utils/split-modes';
12-
import { type GeneratorDependency, OutputMockType, OutputMode } from '../types';
12+
import {
13+
type GeneratorDependency,
14+
type GeneratorSchema,
15+
OutputMockType,
16+
OutputMode,
17+
} from '../types';
1318
import { writeSingleMode } from './single-mode';
1419

1520
describe('writeSingleMode — separated mocks import inline schemas from the target file', () => {
@@ -81,3 +86,128 @@ describe('writeSingleMode — separated mocks import inline schemas from the tar
8186
);
8287
});
8388
});
89+
90+
// Regression coverage for https://github.com/orval-labs/orval/issues/3627
91+
//
92+
// On wide specs with `faker schemas: true`, shared-array import aggregation
93+
// can strip `get<X>Mock()` factory imports from `mockOutput.imports`.
94+
// split-mode, tags-mode, and split-tags-mode all recover these by scanning
95+
// the finalized mock implementation. single-mode was the only writer missing
96+
// this recovery — both the inline branch and the de-inlined branch.
97+
98+
const petSchema: GeneratorSchema = {
99+
name: 'Pet',
100+
model: 'export type Pet = { id: number };',
101+
imports: [],
102+
schema: { type: 'object', properties: { id: { type: 'integer' } } },
103+
};
104+
105+
const createRecoveryProps = (target: string) => {
106+
const baseProps = createSplitModeProps(target);
107+
return {
108+
...baseProps,
109+
builder: {
110+
...baseProps.builder,
111+
schemas: [petSchema],
112+
operations: {
113+
listPets: createSplitModeOperation({
114+
mockOutputs: [
115+
{
116+
type: OutputMockType.FAKER,
117+
implementation: {
118+
function:
119+
'export const getPetResponseMock = () => ({ ...getPetMock() });',
120+
handler: '',
121+
handlerName: '',
122+
},
123+
imports: [],
124+
},
125+
],
126+
}),
127+
},
128+
} as typeof baseProps.builder,
129+
output: createSplitModeOutput(target, {
130+
mode: OutputMode.SINGLE,
131+
indexFiles: true,
132+
schemas: path.join(path.dirname(target), 'model'),
133+
mock: {
134+
indexMockFiles: false,
135+
generators: [{ type: OutputMockType.FAKER, schemas: true }],
136+
},
137+
}),
138+
};
139+
};
140+
141+
describe('writeSingleMode — recovers schema-factory imports stripped by aggregation (inline mocks)', () => {
142+
let tmpDir: string;
143+
144+
beforeEach(() => {
145+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orval-single-mode-'));
146+
});
147+
148+
afterEach(() => {
149+
fs.removeSync(tmpDir);
150+
});
151+
152+
it('recovers getPetMock() missing from mockOutput.imports', async () => {
153+
const target = path.join(tmpDir, 'petstore.ts');
154+
const importsMockCalls: Array<{ imports: readonly GeneratorDependency[] }> =
155+
[];
156+
const props = createRecoveryProps(target);
157+
158+
props.builder.importsMock = ({
159+
imports,
160+
}: {
161+
imports: readonly GeneratorDependency[];
162+
}) => {
163+
importsMockCalls.push({ imports });
164+
return '';
165+
};
166+
167+
await writeSingleMode({ ...props, needSchema: false });
168+
169+
expect(importsMockCalls.length).toBeGreaterThan(0);
170+
const allExportNames = importsMockCalls.flatMap((call) =>
171+
call.imports.flatMap((dep) => dep.exports.map((entry) => entry.name)),
172+
);
173+
expect(allExportNames).toContain('getPetMock');
174+
});
175+
});
176+
177+
describe('writeSingleMode — recovers schema-factory imports stripped by aggregation (de-inlined mocks)', () => {
178+
let tmpDir: string;
179+
180+
beforeEach(() => {
181+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orval-single-mode-'));
182+
});
183+
184+
afterEach(() => {
185+
fs.removeSync(tmpDir);
186+
});
187+
188+
it('recovers getPetMock() in the generated .faker.ts file', async () => {
189+
const target = path.join(tmpDir, 'petstore.ts');
190+
const props = createRecoveryProps(target);
191+
192+
props.output.mock.path = path.join(tmpDir, 'mocks');
193+
props.builder.importsMock = ({
194+
imports,
195+
}: {
196+
imports: readonly GeneratorDependency[];
197+
}) =>
198+
imports
199+
.map(
200+
({ dependency, exports }: GeneratorDependency) =>
201+
`import { ${exports.map((entry) => entry.name).join(', ')} } from '${dependency}';`,
202+
)
203+
.join('\n');
204+
205+
await writeSingleMode({ ...props, needSchema: false });
206+
207+
const mockContent = await fs.readFile(
208+
path.join(tmpDir, 'mocks', 'petstore.faker.ts'),
209+
'utf8',
210+
);
211+
expect(mockContent).toMatch(/import\s*\{[^}]*getPetMock[^}]*\}\s*from/);
212+
});
213+
});

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

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import {
2020
filterLocalStrictMockTypeImports,
2121
} from './finalize-mock-implementation';
2222
import { generateImportsForBuilder } from './generate-imports-for-builder';
23+
import {
24+
collectRecoveredSchemaFactoryImports,
25+
mergeGeneratorImports,
26+
} from './mock-imports';
2327
import { collapseInlineMockOutputs } from './mock-outputs';
2428
import {
2529
getMockDir,
@@ -177,8 +181,29 @@ export async function writeSingleMode({
177181
output,
178182
mockOutput,
179183
);
184+
const finalizedMockImplementation = builder.finalizeMockImplementation
185+
? builder.finalizeMockImplementation(
186+
mockOutput.implementation,
187+
finalizeMockOptions,
188+
)
189+
: mockOutput.implementation;
190+
const usesSchemaFactories =
191+
!!entry &&
192+
!isFunction(entry) &&
193+
entry.type === OutputMockType.FAKER &&
194+
entry.schemas === true;
195+
const recoveredSchemaFactoryImports =
196+
usesSchemaFactories && output.schemas
197+
? collectRecoveredSchemaFactoryImports(
198+
finalizedMockImplementation,
199+
builder.schemas.filter((s) => s.schema).map((s) => s.name),
200+
)
201+
: [];
180202
const filteredMockImports = filterLocalStrictMockTypeImports(
181-
mockOutput.imports.filter(
203+
mergeGeneratorImports(
204+
mockOutput.imports,
205+
recoveredSchemaFactoryImports,
206+
).filter(
182207
(impMock) =>
183208
!normalizedImports.some(
184209
(imp) =>
@@ -201,7 +226,7 @@ export async function writeSingleMode({
201226
'.',
202227
);
203228
data += builder.importsMock({
204-
implementation: mockOutput.implementation,
229+
implementation: finalizedMockImplementation,
205230
imports: importsMockForBuilder,
206231
projectName,
207232
hasSchemaDir: !!output.schemas,
@@ -306,27 +331,57 @@ export async function writeSingleMode({
306331
schemaCustomImportPath ??
307332
resolveMockSchemasPath(mockFilePath, schemasTarget);
308333

334+
const finalizeMockOptions = getFinalizeMockImplementationOptions(
335+
output,
336+
mockOutput,
337+
);
338+
339+
const finalizedMockImplementation = builder.finalizeMockImplementation
340+
? builder.finalizeMockImplementation(
341+
mockOutput.implementation,
342+
finalizeMockOptions,
343+
)
344+
: mockOutput.implementation;
345+
346+
const usesSchemaFactories =
347+
!isFunction(rawEntry) &&
348+
rawEntry.type === OutputMockType.FAKER &&
349+
rawEntry.schemas === true;
350+
const recoveredSchemaFactoryImports =
351+
usesSchemaFactories && output.schemas
352+
? collectRecoveredSchemaFactoryImports(
353+
finalizedMockImplementation,
354+
builder.schemas.filter((s) => s.schema).map((s) => s.name),
355+
)
356+
: [];
357+
309358
const importsMockForBuilder =
310359
schemasPath || mockDir !== dirname
311360
? generateImportsForBuilder(
312361
output,
313-
mockOutput.imports,
362+
filterLocalStrictMockTypeImports(
363+
mergeGeneratorImports(
364+
mockOutput.imports,
365+
recoveredSchemaFactoryImports,
366+
),
367+
finalizeMockOptions.strictSchemaTypeNames,
368+
),
314369
mockRelativeSchemasPath,
315370
schemaTagMap,
316371
)
317372
: generateImportsForBuilder(
318373
output,
319-
mockOutput.imports.filter((imp) => !!imp.importPath),
374+
filterLocalStrictMockTypeImports(
375+
mergeGeneratorImports(
376+
mockOutput.imports,
377+
recoveredSchemaFactoryImports,
378+
),
379+
finalizeMockOptions.strictSchemaTypeNames,
380+
).filter((imp) => !!imp.importPath),
320381
'.',
321382
);
322383

323384
let mockData = header;
324-
const finalizedMockImplementation = builder.finalizeMockImplementation
325-
? builder.finalizeMockImplementation(
326-
mockOutput.implementation,
327-
getFinalizeMockImplementationOptions(output, mockOutput),
328-
)
329-
: mockOutput.implementation;
330385
mockData += builder.importsMock({
331386
implementation: finalizedMockImplementation,
332387
imports: importsMockForBuilder,

0 commit comments

Comments
 (0)