Skip to content

Commit 66af3e2

Browse files
fix(mock): strict faker schema mock types for enums, nested spreads, and binary (#3607)
* fix(mock): strict faker schema mock types for enums, nested spreads, and binary Classify strict {Schema}Mock aliases by schema shape. Cast nested factory spreads to base mock types. Add petstore regression spec for #3590. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): resolve lint errors in strict mock type helpers Co-authored-by: Cursor <cursoragent@cursor.com> * chore(tests): drop accidental issue-3572 snapshots from PR Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): import nested strict mock types in tag faker files Import {Schema}Mock from index.faker when operation fakers spread schema factories with strict casts. Skip imports that duplicate local response mocks. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): prevent import duplication and recover tag faker schema imports (#3590) Use appendImportsDelta to avoid stack overflows with schemas: true. Recover missing get*Mock imports in tags-split faker files. Guard MSW generation when response.imports is undefined. Add pet-themed regression specs and tests for each failure mode. Co-authored-by: Cursor <cursoragent@cursor.com> * test(mock): fix type-aware lint errors in #3590 unit tests Use OpenApiSchemaObject and createTestContextSpec override options. Cast undefined response imports via unknown for the runtime guard test. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): restore enum and oneOf split imports after delta merge (#3590) Merge returned mock imports only when the shared array was not mutated in place. Propagate oneOf split helper types into shared import aggregation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): restore schema enum and nested split mock imports (#3590) Merge getMockScalar result imports in generateFakerForSchemas. Collect nested oneOf split helper types and filter local strict mock imports in single mode. Co-authored-by: Cursor <cursoragent@cursor.com> * test(mock): update issue-3590 snapshot for inline array item import PetDetailSettingsItem is referenced by strict mock factories and must be imported in index.faker.ts. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): scope schema-factory recovery and strict mock import fixes (#3590) Filter local strict mock type imports in split mode. Scope schema-factory recovery to the current mock generator. Recognize strict factory signatures when collecting split imports. Classify scalar composed schemas as aliases. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): type composed schema branches for type-aware lint (#3590) Cast oneOf/anyOf/allOf branches so the branch callback is not implicit any. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(mock): collect delegated factory imports from combine results (#3606) Push resolved imports from resolveMockValue into combineImports instead of scraping the shared imports array, which #3606 no longer mutates. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4345b53 commit 66af3e2

97 files changed

Lines changed: 3099 additions & 68 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.

packages/core/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,11 +1349,14 @@ export type GeneratorOperations = Record<string, GeneratorOperation>;
13491349
// A single generator's accumulated mock output, keyed by the generator's
13501350
// `OutputMockType`. Writers iterate over `GeneratorTarget.mockOutputs` to
13511351
// emit one file per entry (e.g. `<file>.msw.ts` and `<file>.faker.ts`).
1352+
export type StrictMockSchemaKind = 'object' | 'alias' | 'binary';
1353+
13521354
export interface GeneratorMockOutput {
13531355
type: OutputMockType;
13541356
implementation: string;
13551357
imports: GeneratorImport[];
13561358
strictMockSchemaTypeNames?: string[];
1359+
strictMockSchemaKinds?: Record<string, StrictMockSchemaKind>;
13571360
}
13581361

13591362
export interface GeneratorMockOutputFull {
@@ -1365,6 +1368,7 @@ export interface GeneratorMockOutputFull {
13651368
};
13661369
imports: GeneratorImport[];
13671370
strictMockSchemaTypeNames?: string[];
1371+
strictMockSchemaKinds?: Record<string, StrictMockSchemaKind>;
13681372
}
13691373

13701374
export interface GeneratorTarget {
@@ -1533,6 +1537,7 @@ export interface ClientMockGeneratorBuilder {
15331537
imports: GeneratorImport[];
15341538
implementation: ClientMockGeneratorImplementation;
15351539
strictMockSchemaTypeNames?: string[];
1540+
strictMockSchemaKinds?: Record<string, StrictMockSchemaKind>;
15361541
}
15371542

15381543
export type ClientMockBuilder = (
@@ -1698,6 +1703,7 @@ export type ResReqTypesValue = ScalarValue & {
16981703
export interface FinalizeMockImplementationOptions {
16991704
mockOptions?: Pick<MockOptions, 'required' | 'nonNullable'>;
17001705
strictSchemaTypeNames?: readonly string[];
1706+
strictMockSchemaKinds?: Readonly<Record<string, StrictMockSchemaKind>>;
17011707
}
17021708

17031709
export interface WriteSpecBuilder {

packages/core/src/writers/finalize-mock-implementation.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type {
22
FinalizeMockImplementationOptions,
3+
GeneratorImport,
34
GeneratorMockOutput,
45
NormalizedOutputOptions,
6+
StrictMockSchemaKind,
57
} from '../types';
68

79
type MockOutputWithStrictNames = Pick<
810
GeneratorMockOutput,
9-
'strictMockSchemaTypeNames'
11+
'strictMockSchemaTypeNames' | 'strictMockSchemaKinds'
1012
>;
1113

1214
export function getFinalizeMockImplementationOptions(
@@ -25,10 +27,46 @@ export function getFinalizeMockImplementationOptions(
2527
),
2628
),
2729
];
30+
const strictMockSchemaKinds = outputs.reduce<
31+
Record<string, StrictMockSchemaKind>
32+
>((acc, mockOutput) => {
33+
if (!mockOutput.strictMockSchemaKinds) {
34+
return acc;
35+
}
36+
for (const [name, kind] of Object.entries(
37+
mockOutput.strictMockSchemaKinds,
38+
)) {
39+
acc[name] ??= kind;
40+
}
41+
return acc;
42+
}, {});
2843

2944
return {
3045
mockOptions: output.override.mock,
3146
strictSchemaTypeNames:
3247
strictSchemaTypeNames.length > 0 ? strictSchemaTypeNames : undefined,
48+
strictMockSchemaKinds:
49+
Object.keys(strictMockSchemaKinds).length > 0
50+
? strictMockSchemaKinds
51+
: undefined,
3352
};
3453
}
54+
55+
/** Drop schema-factory `{Schema}Mock` type imports that are declared locally. */
56+
export function filterLocalStrictMockTypeImports(
57+
imports: readonly GeneratorImport[],
58+
strictSchemaTypeNames?: readonly string[],
59+
): GeneratorImport[] {
60+
if (!strictSchemaTypeNames?.length) {
61+
return [...imports];
62+
}
63+
64+
const localMockTypeNames = new Set(
65+
strictSchemaTypeNames.map((name) => `${name}Mock`),
66+
);
67+
68+
return imports.filter(
69+
(imp) =>
70+
!(imp.schemaFactory && !imp.values && localMockTypeNames.has(imp.name)),
71+
);
72+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import {
4+
buildKnownSchemaFactoryImportSets,
5+
collectRecoveredSchemaFactoryImports,
6+
collectSchemaFactoryImportsFromImplementation,
7+
mergeGeneratorImports,
8+
} from './mock-imports';
9+
10+
describe('buildKnownSchemaFactoryImportSets', () => {
11+
it('maps schema names to consolidated faker symbols', () => {
12+
const known = buildKnownSchemaFactoryImportSets([
13+
'Pet',
14+
'PetDetailResponse',
15+
]);
16+
17+
expect(known.factoryNames.has('getPetMock')).toBe(true);
18+
expect(known.typeNames.has('PetMock')).toBe(true);
19+
expect(known.factoryNames.has('getPetDetailResponseMock')).toBe(true);
20+
});
21+
});
22+
23+
describe('collectSchemaFactoryImportsFromImplementation', () => {
24+
it('collects factory and mock type imports from strict delegation casts', () => {
25+
const implementation = `export const getFooResponseMock = (): FooMock => ({ bar: { ...getBarMock() as BarMock } });`;
26+
27+
expect(
28+
collectSchemaFactoryImportsFromImplementation(implementation),
29+
).toEqual([
30+
{ name: 'getBarMock', values: true, schemaFactory: true },
31+
{ name: 'BarMock', values: false, schemaFactory: true },
32+
]);
33+
});
34+
35+
it('ignores split response helpers when filtered by known component schemas (#3590)', () => {
36+
const implementation = [
37+
'export const getStorePetResponseMock = (): PetDetailResponseMock => ({',
38+
' pet: { ...getPetMock() as PetMock },',
39+
' variant: getStorePetResponsePetDetailResponseItemMock(),',
40+
'});',
41+
].join('\n');
42+
43+
const known = buildKnownSchemaFactoryImportSets([
44+
'Pet',
45+
'PetDetailResponse',
46+
]);
47+
48+
expect(
49+
collectSchemaFactoryImportsFromImplementation(implementation, known),
50+
).toEqual([
51+
{ name: 'getPetMock', values: true, schemaFactory: true },
52+
{ name: 'PetMock', values: false, schemaFactory: true },
53+
]);
54+
});
55+
});
56+
57+
describe('collectRecoveredSchemaFactoryImports', () => {
58+
it('uses component schema names to recover consolidated faker imports', () => {
59+
const implementation = [
60+
'export const getStorePetResponseMock = (): PetDetailResponseMock => ({',
61+
' pet: { ...getPetMock() as PetMock },',
62+
'});',
63+
].join('\n');
64+
65+
expect(
66+
collectRecoveredSchemaFactoryImports(implementation, ['Pet']),
67+
).toEqual([
68+
{ name: 'getPetMock', values: true, schemaFactory: true },
69+
{ name: 'PetMock', values: false, schemaFactory: true },
70+
]);
71+
});
72+
});
73+
74+
describe('mergeGeneratorImports', () => {
75+
it('prefers value imports over type-only duplicates', () => {
76+
expect(
77+
mergeGeneratorImports(
78+
[{ name: 'PetMock', values: false, schemaFactory: true }],
79+
[{ name: 'PetMock', values: true, schemaFactory: true }],
80+
),
81+
).toEqual([{ name: 'PetMock', values: true, schemaFactory: true }]);
82+
});
83+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type { GeneratorImport } from '../types';
2+
import { pascal } from '../utils';
3+
4+
export interface KnownSchemaFactoryImportSets {
5+
factoryNames: ReadonlySet<string>;
6+
typeNames: ReadonlySet<string>;
7+
}
8+
9+
/** Maps `components/schemas` names to consolidated index.faker import symbols. */
10+
export function buildKnownSchemaFactoryImportSets(
11+
schemaNames: readonly string[],
12+
): KnownSchemaFactoryImportSets {
13+
const factoryNames = new Set<string>();
14+
const typeNames = new Set<string>();
15+
16+
for (const name of schemaNames) {
17+
const typeName = pascal(name);
18+
factoryNames.add(`get${typeName}Mock`);
19+
typeNames.add(`${typeName}Mock`);
20+
}
21+
22+
return { factoryNames, typeNames };
23+
}
24+
25+
/**
26+
* Recover schema-factory imports referenced in generated mock bodies but
27+
* missing from the collected import list (e.g. after shared-array import
28+
* aggregation on large specs). Scans for `get<Schema>Mock()` calls and
29+
* `as <Schema>Mock` casts emitted by strict schema delegation (#3590).
30+
*
31+
* When `knownSets` is provided, only symbols that exist in the consolidated
32+
* schemas faker file are recovered — this avoids importing one-off split
33+
* response helper factories that live in the tag file itself.
34+
*/
35+
export function collectSchemaFactoryImportsFromImplementation(
36+
implementation: string,
37+
knownSets?: KnownSchemaFactoryImportSets,
38+
): GeneratorImport[] {
39+
const imports: GeneratorImport[] = [];
40+
const seen = new Set<string>();
41+
42+
for (const match of implementation.matchAll(/\b(get[A-Za-z0-9]+Mock)\(\)/g)) {
43+
const factoryName = match[1];
44+
if (knownSets && !knownSets.factoryNames.has(factoryName)) {
45+
continue;
46+
}
47+
const key = `value::${factoryName}`;
48+
if (seen.has(key)) continue;
49+
seen.add(key);
50+
imports.push({
51+
name: factoryName,
52+
values: true,
53+
schemaFactory: true,
54+
});
55+
}
56+
57+
for (const match of implementation.matchAll(/\bas ([A-Za-z0-9]+Mock)\b/g)) {
58+
const typeName = match[1];
59+
if (knownSets && !knownSets.typeNames.has(typeName)) {
60+
continue;
61+
}
62+
const key = `type::${typeName}`;
63+
if (seen.has(key)) continue;
64+
seen.add(key);
65+
imports.push({
66+
name: typeName,
67+
values: false,
68+
schemaFactory: true,
69+
});
70+
}
71+
72+
return imports;
73+
}
74+
75+
export function mergeGeneratorImports(
76+
...groups: readonly (readonly GeneratorImport[])[]
77+
): GeneratorImport[] {
78+
const merged = new Map<string, GeneratorImport>();
79+
80+
for (const group of groups) {
81+
for (const imp of group) {
82+
const key = `${imp.name}::${imp.alias ?? ''}`;
83+
const existing = merged.get(key);
84+
if (!existing) {
85+
merged.set(key, imp);
86+
continue;
87+
}
88+
if (!existing.values && imp.values) {
89+
merged.set(key, imp);
90+
}
91+
}
92+
}
93+
94+
return [...merged.values()];
95+
}
96+
97+
/** Recover missing index.faker imports when `schemas: true` is enabled. */
98+
export function collectRecoveredSchemaFactoryImports(
99+
implementation: string,
100+
componentSchemaNames: readonly string[],
101+
): GeneratorImport[] {
102+
return collectSchemaFactoryImportsFromImplementation(
103+
implementation,
104+
buildKnownSchemaFactoryImportSets(componentSchemaNames),
105+
);
106+
}

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import {
1515
import { getMockFileExtensionByTypeName } from '../utils/file-extensions';
1616
import { escapeRegExp } from '../utils/string';
1717
import { writeGeneratedFile } from './file';
18-
import { getFinalizeMockImplementationOptions } from './finalize-mock-implementation';
18+
import {
19+
getFinalizeMockImplementationOptions,
20+
filterLocalStrictMockTypeImports,
21+
} from './finalize-mock-implementation';
1922
import { generateImportsForBuilder } from './generate-imports-for-builder';
2023
import { collapseInlineMockOutputs } from './mock-outputs';
2124
import {
@@ -168,13 +171,20 @@ export async function writeSingleMode({
168171
const entry = output.mock.generators.find(
169172
(g) => !isFunction(g) && g.type === mockOutput.type,
170173
);
171-
const filteredMockImports = mockOutput.imports.filter(
172-
(impMock) =>
173-
!normalizedImports.some(
174-
(imp) =>
175-
imp.name === impMock.name &&
176-
(imp.alias ?? '') === (impMock.alias ?? ''),
177-
),
174+
const finalizeMockOptions = getFinalizeMockImplementationOptions(
175+
output,
176+
mockOutput,
177+
);
178+
const filteredMockImports = filterLocalStrictMockTypeImports(
179+
mockOutput.imports.filter(
180+
(impMock) =>
181+
!normalizedImports.some(
182+
(imp) =>
183+
imp.name === impMock.name &&
184+
(imp.alias ?? '') === (impMock.alias ?? ''),
185+
),
186+
),
187+
finalizeMockOptions.strictSchemaTypeNames,
178188
);
179189
const importsMockForBuilder = schemasPath
180190
? generateImportsForBuilder(

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ import {
1414
} from '../utils';
1515
import { getMockFileExtensionByTypeName } from '../utils/file-extensions';
1616
import { writeGeneratedFile } from './file';
17-
import { getFinalizeMockImplementationOptions } from './finalize-mock-implementation';
17+
import {
18+
filterLocalStrictMockTypeImports,
19+
getFinalizeMockImplementationOptions,
20+
} from './finalize-mock-implementation';
1821
import { generateImportsForBuilder } from './generate-imports-for-builder';
22+
import {
23+
collectRecoveredSchemaFactoryImports,
24+
mergeGeneratorImports,
25+
} from './mock-imports';
1926
import { getMockDir, resolveMockSchemasPath } from './mock-utils';
2027
import { generateTarget } from './target';
2128
import { getOrvalGeneratedTypes, getTypedResponse } from './types';
@@ -211,18 +218,42 @@ export async function writeSplitMode({
211218
schemaCustomImportPath ??
212219
resolveMockSchemasPath(mockFilePath, schemasTarget);
213220

214-
const importsMockForBuilder = generateImportsForBuilder(
221+
const finalizeMockOptions = getFinalizeMockImplementationOptions(
215222
output,
216-
mockOutput.imports,
217-
mockRelativeSchemasPath,
223+
mockOutput,
218224
);
219-
let mockData = header;
225+
220226
const finalizedMockImplementation = builder.finalizeMockImplementation
221227
? builder.finalizeMockImplementation(
222228
mockOutput.implementation,
223-
getFinalizeMockImplementationOptions(output, mockOutput),
229+
finalizeMockOptions,
224230
)
225231
: mockOutput.implementation;
232+
233+
const usesSchemaFactories =
234+
!isFunction(rawEntry) &&
235+
rawEntry.type === OutputMockType.FAKER &&
236+
rawEntry.schemas === true;
237+
const recoveredSchemaFactoryImports =
238+
usesSchemaFactories && output.schemas
239+
? collectRecoveredSchemaFactoryImports(
240+
finalizedMockImplementation,
241+
builder.schemas.filter((s) => s.schema).map((s) => s.name),
242+
)
243+
: [];
244+
245+
const importsMockForBuilder = generateImportsForBuilder(
246+
output,
247+
filterLocalStrictMockTypeImports(
248+
mergeGeneratorImports(
249+
mockOutput.imports,
250+
recoveredSchemaFactoryImports,
251+
),
252+
finalizeMockOptions.strictSchemaTypeNames,
253+
),
254+
mockRelativeSchemasPath,
255+
);
256+
let mockData = header;
226257
mockData += builder.importsMock({
227258
implementation: finalizedMockImplementation,
228259
imports: importsMockForBuilder,

0 commit comments

Comments
 (0)