Skip to content

Commit 68a0ea2

Browse files
feat(mock): emit msw handlers and faker factories as separate files in split modes (#3730)
* feat(mock): emit msw handlers and faker factories as separate files in split modes In split and tags-split modes the get<Op>ResponseMock factories were duplicated in <name>.msw.ts and <name>.faker.ts. Now the faker file is the single home of the factories and the msw file imports and re-exports them. MSW-only output is unchanged unless the new operationResponses: false flag is set on the msw generator entry (handlers only, undefined fallbacks). * fix(mock): address review feedback on import extension, uniqueBy delimiter and docs
1 parent 3112104 commit 68a0ea2

132 files changed

Lines changed: 2859 additions & 12777 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,7 @@ export default defineConfig({
619619
|--------|------|---------|-------------|
620620
| `type` | `'msw'` | required | Discriminator for MSW handler generation. |
621621
| `path` | `String` | `undefined` | Output directory for this generator's mock files. Overrides the shared `mock.path` when set. When provided in `single` or `tags` mode, mock code is written to separate files (relative to `path`) instead of being inlined into the implementation file. |
622+
| `operationResponses` | `Boolean` | `true` | Emit `get<Op>ResponseMock` factories in the MSW output. Set to `false` to generate handlers only, response fallbacks become `undefined`. No effect when a Faker generator also emits the factories, the handlers then import them from the `.faker` file. Honored in `split` and `tags-split` modes. |
622623
| `delay` | `Number \| Function \| false` | `false` | Response delay in ms. |
623624
| `delayFunctionLazyExecute` | `Boolean` | `false` | Execute delay function at runtime instead of at build time. |
624625
| `baseUrl` | `String` | `''` | Base URL for the generated MSW handlers. |
@@ -631,6 +632,8 @@ export default defineConfig({
631632

632633
The Faker generator emits the same `get<Op>ResponseMock` factories MSW would emit, but without any `msw` dependency or HTTP handler code. Useful for tests or stories that only need fake response data.
633634

635+
In `split` and `tags-split` modes, configuring Faker alongside MSW moves the `get<Op>ResponseMock` factories to the `.faker` file. The `.msw` file only contains the handlers and imports (and re-exports) the factories instead of duplicating them. If the Faker generator is configured with `operationResponses: false` it emits no factories, so there is nothing to move and the `.msw` file keeps them inline.
636+
634637
| Option | Type | Default | Description |
635638
|--------|------|---------|-------------|
636639
| `type` | `'faker'` | required | Discriminator for Faker-only output. |

packages/core/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,10 @@ export interface CommonMockOptions {
514514

515515
export interface MswMockOptions extends CommonMockOptions {
516516
type: typeof OutputMockType.MSW;
517+
// Emit faker responses in MSW handler output, defaults to true. Disable to
518+
// generate handlers only which require passing in mock responses, falls back
519+
// to `undefined` if no mock response passed to handler.
520+
operationResponses?: boolean;
517521
// Base URL prefix for the generated MSW route matchers
518522
baseUrl?: string;
519523
// Response delay before MSW handlers resolve (false disables delay)

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,116 @@ describe('generateImportsForBuilder', () => {
199199
});
200200
});
201201

202+
describe('imports with explicit importPath', () => {
203+
it('should group imports with the same importPath into a single dependency', () => {
204+
const output = createMockOutput({ indexFiles: false });
205+
const imports: GeneratorImport[] = [
206+
{
207+
name: 'getPetResponseMock',
208+
values: true,
209+
importPath: './pets.faker',
210+
},
211+
{
212+
name: 'getUserResponseMock',
213+
values: true,
214+
importPath: './pets.faker',
215+
},
216+
];
217+
218+
const result = generateImportsForBuilder(output, imports, '../models');
219+
220+
expect(result).toEqual([
221+
{
222+
exports: [
223+
{
224+
name: 'getPetResponseMock',
225+
values: true,
226+
importPath: './pets.faker',
227+
},
228+
{
229+
name: 'getUserResponseMock',
230+
values: true,
231+
importPath: './pets.faker',
232+
},
233+
],
234+
dependency: './pets.faker',
235+
},
236+
]);
237+
});
238+
239+
it('should separate imports with different importPaths into different dependencies', () => {
240+
const output = createMockOutput({ indexFiles: false });
241+
const imports: GeneratorImport[] = [
242+
{
243+
name: 'getPetResponseMock',
244+
values: true,
245+
importPath: './pets.faker',
246+
},
247+
{
248+
name: 'getHealthResponseMock',
249+
values: true,
250+
importPath: './health.faker',
251+
},
252+
];
253+
254+
const result = generateImportsForBuilder(output, imports, '../models');
255+
256+
expect(result).toEqual([
257+
{
258+
exports: [
259+
{
260+
name: 'getPetResponseMock',
261+
values: true,
262+
importPath: './pets.faker',
263+
},
264+
],
265+
dependency: './pets.faker',
266+
},
267+
{
268+
exports: [
269+
{
270+
name: 'getHealthResponseMock',
271+
values: true,
272+
importPath: './health.faker',
273+
},
274+
],
275+
dependency: './health.faker',
276+
},
277+
]);
278+
});
279+
280+
it('should deduplicate imports with the same name and importPath', () => {
281+
const output = createMockOutput({ indexFiles: false });
282+
const imports: GeneratorImport[] = [
283+
{
284+
name: 'getPetResponseMock',
285+
values: true,
286+
importPath: './pets.faker',
287+
},
288+
{
289+
name: 'getPetResponseMock',
290+
values: true,
291+
importPath: './pets.faker',
292+
},
293+
];
294+
295+
const result = generateImportsForBuilder(output, imports, '../models');
296+
297+
expect(result).toEqual([
298+
{
299+
exports: [
300+
{
301+
name: 'getPetResponseMock',
302+
values: true,
303+
importPath: './pets.faker',
304+
},
305+
],
306+
dependency: './pets.faker',
307+
},
308+
]);
309+
});
310+
});
311+
202312
describe('with importPath (package import specifier)', () => {
203313
it('should use package import path with indexFiles', () => {
204314
const output = createMockOutput({

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,26 @@ export function generateImportsForBuilder(
134134
);
135135
}
136136

137-
const otherImports = uniqueBy(
137+
const otherImportsMap = new Map<string, GeneratorImport[]>();
138+
for (const imp of uniqueBy(
138139
imports.filter(
139140
(i): i is GeneratorImport & { importPath: string } => !!i.importPath,
140141
),
141-
(x) => x.name + x.importPath,
142-
).map<GeneratorDependency>((i) => {
143-
return {
144-
exports: [i],
145-
dependency: i.importPath,
146-
};
147-
});
142+
(x) => `${x.name}|${x.importPath}`,
143+
)) {
144+
const existing = otherImportsMap.get(imp.importPath);
145+
if (existing) {
146+
existing.push(imp);
147+
} else {
148+
otherImportsMap.set(imp.importPath, [imp]);
149+
}
150+
}
151+
const otherImports = [...otherImportsMap.entries()].map<GeneratorDependency>(
152+
([dependency, exports]) => ({
153+
exports,
154+
dependency,
155+
}),
156+
);
148157

149158
return [...schemaImports, ...schemaFactoryDeps, ...otherImports];
150159
}

0 commit comments

Comments
 (0)