Skip to content

Commit 5d53dfa

Browse files
authored
feat(core): allow mock data generators only (#3398)
* feat(mock): add generateHandlers option to suppress MSW handler output When `generateHandlers: false` is set in mock options, only faker data generator functions are produced — no MSW request handlers, no aggregated handler array, and no `msw` dependency in imports. This allows users who only need mock data factories to avoid the MSW dependency entirely. * fix(core): handle empty handlerName in writers when generateHandlers is false Guard handlerName accumulation in target and target-tags writers to avoid producing junk `()` entries when handlers are disabled. Also update the split-tags index file to use barrel exports when there is no aggregated handler function to reference. * test(mock): add coverage for generateHandlers: false option Verify that handler and handlerName are empty, mock data function is still generated, msw imports are excluded, and faker locale imports work correctly when generateHandlers is disabled. * docs(mock): document generateHandlers option Add generateHandlers to the configuration reference table and a new "Data Generators Only" section to the MSW guide explaining the use case and output differences. * fix(core): avoid leading comma when accumulating handlerNames in target-tags
1 parent 729c5b8 commit 5d53dfa

9 files changed

Lines changed: 164 additions & 27 deletions

File tree

docs/content/docs/guides/msw.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,31 @@ const server = setupServer(...handlers);
245245
export { server };
246246
```
247247

248+
## Data Generators Only
249+
250+
If you only need mock data factories without MSW request handlers, set `generateHandlers` to `false`:
251+
252+
```ts title="orval.config.ts"
253+
export default defineConfig({
254+
petstore: {
255+
output: {
256+
mock: {
257+
type: 'msw',
258+
generateHandlers: false,
259+
},
260+
},
261+
},
262+
});
263+
```
264+
265+
This generates only the `get<OperationId>ResponseMock` functions (powered by Faker.js) — no MSW handlers, no aggregated handler array, and no `msw` dependency in the output. Your project only needs `@faker-js/faker` as a dependency.
266+
267+
This is useful when you want mock data for:
268+
- Unit tests with custom assertion libraries
269+
- Storybook stories
270+
- Seed scripts
271+
- Any test setup that doesn't use MSW
272+
248273
## MSW Best Practices
249274

250275
The generated code follows [MSW best practices](https://mswjs.io/docs/best-practices):

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export default defineConfig({
377377
| Option | Type | Default | Description |
378378
|--------|------|---------|-------------|
379379
| `type` | `'msw'` | `'msw'` | Mock type |
380+
| `generateHandlers` | `Boolean` | `true` | Generate MSW request handlers. Set to `false` for data generators only (no `msw` dependency needed). |
380381
| `delay` | `Number \| Function \| false` | `false` | Response delay in ms |
381382
| `delayFunctionLazyExecute` | `Boolean` | `false` | Execute delay function at runtime |
382383
| `useExamples` | `Boolean` | `false` | Use OpenAPI examples |

packages/core/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ export interface GlobalMockOptions {
424424
// Preferred response content type when multiple success content types exist
425425
preferredContentType?: string;
426426
indexMockFiles?: boolean;
427+
// When false, only mock data generators are produced (no MSW request handlers or aggregated handler array)
428+
generateHandlers?: boolean;
427429
}
428430

429431
export type OverrideMockOptions = Partial<GlobalMockOptions> & {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,17 @@ export async function writeSplitTagsMode({
277277

278278
// Write mock index file after Promise.all to ensure deterministic export order.
279279
if (indexFilePath && mockOption) {
280+
const handlersDisabled = mockOption.generateHandlers === false;
280281
const indexContent = tagEntries
281282
.map(([tag]) => {
282283
const localMockPath = upath.joinSafe(
283284
'./',
284285
tag,
285286
tag + '.' + getMockFileExtensionByTypeName(mockOption),
286287
);
287-
return `export { get${pascal(tag)}Mock } from '${localMockPath}'\n`;
288+
return handlersDisabled
289+
? `export * from '${localMockPath}'\n`
290+
: `export { get${pascal(tag)}Mock } from '${localMockPath}'\n`;
288291
})
289292
.join('');
290293
await fs.appendFile(indexFilePath, indexContent);

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ function generateTargetTags(
4747
implementationMock: {
4848
function: operation.implementationMock.function,
4949
handler: operation.implementationMock.handler,
50-
handlerName: ' ' + operation.implementationMock.handlerName + '()',
50+
handlerName: operation.implementationMock.handlerName
51+
? ' ' + operation.implementationMock.handlerName + '()'
52+
: '',
5153
},
5254
};
5355

@@ -66,11 +68,14 @@ function generateTargetTags(
6668
handler:
6769
currentOperation.implementationMock.handler +
6870
operation.implementationMock.handler,
69-
handlerName:
70-
currentOperation.implementationMock.handlerName +
71-
',\n ' +
72-
operation.implementationMock.handlerName +
73-
'()',
71+
handlerName: operation.implementationMock.handlerName
72+
? currentOperation.implementationMock.handlerName +
73+
(currentOperation.implementationMock.handlerName.length > 0
74+
? ',\n '
75+
: ' ') +
76+
operation.implementationMock.handlerName +
77+
'()'
78+
: currentOperation.implementationMock.handlerName,
7479
},
7580
mutators: operation.mutator
7681
? [...(currentOperation.mutators ?? []), operation.mutator]

packages/core/src/writers/target.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ export function generateTarget(
4848
target.implementationMock.function += operation.implementationMock.function;
4949
target.implementationMock.handler += operation.implementationMock.handler;
5050

51-
const handlerNameSeparator =
52-
target.implementationMock.handlerName.length > 0 ? ',\n ' : ' ';
53-
target.implementationMock.handlerName +=
54-
handlerNameSeparator + operation.implementationMock.handlerName + '()';
51+
if (operation.implementationMock.handlerName) {
52+
const handlerNameSeparator =
53+
target.implementationMock.handlerName.length > 0 ? ',\n ' : ' ';
54+
target.implementationMock.handlerName +=
55+
handlerNameSeparator + operation.implementationMock.handlerName + '()';
56+
}
5557

5658
if (operation.mutator) {
5759
target.mutators.push(operation.mutator);

packages/mock/src/msw/index.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
import { OutputMockType } from '@orval/core';
77
import { describe, expect, it } from 'vitest';
88

9-
import { generateMSW } from './index';
9+
import { generateMSW, generateMSWImports } from './index';
1010

1111
describe('generateMSW', () => {
1212
const mockVerbOptions = {
@@ -1228,4 +1228,85 @@ describe('generateMSW', () => {
12281228
expect(petEntries.some((i) => i.alias === '__Pet')).toBe(true);
12291229
});
12301230
});
1231+
1232+
describe('generateHandlers: false', () => {
1233+
it('should return empty handler when generateHandlers is false', () => {
1234+
const result = generate({
1235+
mock: { type: OutputMockType.MSW, generateHandlers: false },
1236+
});
1237+
expect(result.implementation.handler).toBe('');
1238+
});
1239+
1240+
it('should return empty handlerName when generateHandlers is false', () => {
1241+
const result = generate({
1242+
mock: { type: OutputMockType.MSW, generateHandlers: false },
1243+
});
1244+
expect(result.implementation.handlerName).toBe('');
1245+
});
1246+
1247+
it('should still generate mock data function when generateHandlers is false', () => {
1248+
const numberVerbOptions = {
1249+
...mockVerbOptions,
1250+
response: {
1251+
...mockVerbOptions.response,
1252+
definition: { success: 'number' },
1253+
types: { success: [{ key: '200', value: 'number' }] },
1254+
contentTypes: ['application/json'],
1255+
},
1256+
} as unknown as GeneratorVerbOptions;
1257+
1258+
const result = generateMSW(numberVerbOptions, {
1259+
...baseOptions,
1260+
mock: { type: OutputMockType.MSW, generateHandlers: false },
1261+
});
1262+
expect(result.implementation.function).toContain(
1263+
'export const getGetUserResponseMock',
1264+
);
1265+
expect(result.implementation.handler).toBe('');
1266+
});
1267+
1268+
it('should not contain msw imports when generateHandlers is false', () => {
1269+
const result = generateMSWImports({
1270+
implementation:
1271+
'export const getUserResponseMock = () => faker.string.alpha()',
1272+
imports: [],
1273+
projectName: '',
1274+
hasSchemaDir: false,
1275+
isAllowSyntheticDefaultImports: true,
1276+
options: { type: OutputMockType.MSW, generateHandlers: false },
1277+
});
1278+
expect(result).not.toContain("from 'msw'");
1279+
});
1280+
1281+
it('should include faker import when generateHandlers is false', () => {
1282+
const result = generateMSWImports({
1283+
implementation:
1284+
'export const getUserResponseMock = () => faker.string.alpha()',
1285+
imports: [],
1286+
projectName: '',
1287+
hasSchemaDir: false,
1288+
isAllowSyntheticDefaultImports: true,
1289+
options: { type: OutputMockType.MSW, generateHandlers: false },
1290+
});
1291+
expect(result).toContain("from '@faker-js/faker'");
1292+
});
1293+
1294+
it('should use locale-specific faker import when generateHandlers is false with locale', () => {
1295+
const result = generateMSWImports({
1296+
implementation:
1297+
'export const getUserResponseMock = () => faker.string.alpha()',
1298+
imports: [],
1299+
projectName: '',
1300+
hasSchemaDir: false,
1301+
isAllowSyntheticDefaultImports: true,
1302+
options: {
1303+
type: OutputMockType.MSW,
1304+
generateHandlers: false,
1305+
locale: 'fr',
1306+
},
1307+
});
1308+
expect(result).toContain("from '@faker-js/faker/locale/fr'");
1309+
expect(result).not.toContain("from 'msw'");
1310+
});
1311+
});
12311312
});

packages/mock/src/msw/index.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,19 @@ import { getMockDefinition, getMockOptionsDataOverride } from './mocks';
2121
function getMSWDependencies(
2222
options?: GlobalMockOptions,
2323
): GeneratorDependency[] {
24-
const hasDelay = options?.delay !== false;
2524
const locale = options?.locale;
2625

26+
const fakerDependency: GeneratorDependency = {
27+
exports: [{ name: 'faker', values: true }],
28+
dependency: locale ? `@faker-js/faker/locale/${locale}` : '@faker-js/faker',
29+
};
30+
31+
if (options?.generateHandlers === false) {
32+
return [fakerDependency];
33+
}
34+
35+
const hasDelay = options?.delay !== false;
36+
2737
const exports = [
2838
{ name: 'http', values: true },
2939
{ name: 'HttpResponse', values: true },
@@ -34,15 +44,7 @@ function getMSWDependencies(
3444
exports.push({ name: 'delay', values: true });
3545
}
3646

37-
return [
38-
{ exports, dependency: 'msw' },
39-
{
40-
exports: [{ name: 'faker', values: true }],
41-
dependency: locale
42-
? `@faker-js/faker/locale/${locale}`
43-
: '@faker-js/faker',
44-
},
45-
];
47+
return [{ exports, dependency: 'msw' }, fakerDependency];
4648
}
4749

4850
export const generateMSWImports: GenerateMockImports = ({
@@ -421,11 +423,15 @@ export function generateMSW(
421423
}
422424
}
423425

426+
const handlersDisabled =
427+
isObject(generatorOptions.mock) &&
428+
generatorOptions.mock.generateHandlers === false;
429+
424430
return {
425431
implementation: {
426432
function: mockImplementations.join('\n'),
427-
handlerName: handlerName,
428-
handler: handlerImplementations.join('\n'),
433+
handlerName: handlersDisabled ? '' : handlerName,
434+
handler: handlersDisabled ? '' : handlerImplementations.join('\n'),
429435
},
430436
imports: imports,
431437
};

packages/orval/src/client.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ export const generateClientHeader: GeneratorClientHeader = ({
127127
clientImplementation,
128128
}) => {
129129
const { header } = getGeneratorClient(outputClient, output);
130+
const handlersDisabled =
131+
output.mock &&
132+
!isFunction(output.mock) &&
133+
output.mock.generateHandlers === false;
134+
130135
return {
131136
implementation: header
132137
? header({
@@ -143,7 +148,9 @@ export const generateClientHeader: GeneratorClientHeader = ({
143148
clientImplementation,
144149
})
145150
: '',
146-
implementationMock: `export const ${titles.implementationMock} = () => [\n`,
151+
implementationMock: handlersDisabled
152+
? ''
153+
: `export const ${titles.implementationMock} = () => [\n`,
147154
};
148155
};
149156

@@ -157,10 +164,15 @@ export const generateClientFooter: GeneratorClientFooter = ({
157164
}) => {
158165
const { footer } = getGeneratorClient(outputClient, output);
159166

167+
const handlersDisabled =
168+
output.mock &&
169+
!isFunction(output.mock) &&
170+
output.mock.generateHandlers === false;
171+
160172
if (!footer) {
161173
return {
162174
implementation: '',
163-
implementationMock: `\n]\n`,
175+
implementationMock: handlersDisabled ? '' : `\n]\n`,
164176
};
165177
}
166178

@@ -193,7 +205,7 @@ export const generateClientFooter: GeneratorClientFooter = ({
193205

194206
return {
195207
implementation,
196-
implementationMock: `]\n`,
208+
implementationMock: handlersDisabled ? '' : `]\n`,
197209
};
198210
};
199211

0 commit comments

Comments
 (0)