Skip to content

Commit 553fa74

Browse files
author
Szymon.Poltorak
committed
feat(): create common vitest config setup
1 parent 8040d3e commit 553fa74

File tree

3 files changed

+338
-0
lines changed

3 files changed

+338
-0
lines changed

tools/README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
## Vitest config factory and setup presets
2+
3+
This folder contains utilities to centralize and standardize Vitest configuration across the monorepo.
4+
5+
### Files
6+
7+
- `vitest-config-factory.ts`: builds typed Vitest configs with sensible defaults.
8+
- `vitest-setup-presets.ts`: reusable groups of `setupFiles` paths by test kind.
9+
10+
### Goals
11+
12+
- Reduce duplication across `vitest.*.config.ts` files.
13+
- Keep per-package intent clear with minimal overrides.
14+
- Provide safe defaults and easy extension points.
15+
16+
### Quick start
17+
18+
Use the factory in your suite configs (project root is required):
19+
20+
```ts
21+
/// <reference types="vitest" />
22+
import { createE2eConfig, createIntConfig, createUnitConfig } from '../../tools/vitest-config-factory.js';
23+
24+
export default createUnitConfig('core', {
25+
projectRoot: new URL('../../', import.meta.url),
26+
});
27+
```
28+
29+
Creators:
30+
31+
- `createUnitConfig(projectKey, { projectRoot, ...options })`
32+
- `createIntConfig(projectKey, { projectRoot, ...options })`
33+
- `createE2eConfig(projectKey, { projectRoot, ...options })`
34+
35+
`projectKey` is used for cache and coverage directories.
36+
37+
### Defaults
38+
39+
Common to all kinds:
40+
41+
- `reporters: ['basic']`, `globals: true`, `environment: 'node'`
42+
- `alias: tsconfigPathAliases()`
43+
- `pool: 'threads'` with `singleThread: true`
44+
- Cache directories resolved from `projectRoot` (absolute paths)
45+
46+
Coverage:
47+
48+
- Unit/Int: enabled by default, reports to `<projectRoot>/coverage/<project>/<kind>-tests`
49+
- E2E: disabled by default
50+
- Default exclude: `['mocks/**', '**/types.ts']`
51+
52+
Global setup:
53+
54+
- Unit/Int: `['<projectRoot>/global-setup.ts']` by default
55+
- E2E: none by default (set per-suite if needed)
56+
57+
Include patterns:
58+
59+
- Unit: `src/**/*.unit.test.*`
60+
- Int: `src/**/*.int.test.*`
61+
- E2E: `tests/**/*.e2e.test.*`
62+
63+
### setupFiles strategy
64+
65+
Baseline `setupFiles` are injected automatically by kind:
66+
67+
- Unit baseline: `console.mock.ts`, `reset.mocks.ts`
68+
- Int baseline: `console.mock.ts`, `reset.mocks.ts`
69+
- E2E baseline: `reset.mocks.ts`
70+
71+
Extend with additional files using `options.setupFiles` — they append after the baseline (paths are project-root-relative):
72+
73+
```ts
74+
export default createUnitConfig('core', {
75+
projectRoot: new URL('../../', import.meta.url),
76+
setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts'],
77+
});
78+
```
79+
80+
Replace entirely using `overrideSetupFiles: true` (paths are project-root-relative):
81+
82+
```ts
83+
export default createUnitConfig('core', {
84+
projectRoot: new URL('../../', import.meta.url),
85+
overrideSetupFiles: true,
86+
setupFiles: ['testing/test-setup/src/lib/cliui.mock.ts', 'testing/test-setup/src/lib/fs.mock.ts'],
87+
});
88+
```
89+
90+
### Using presets directly
91+
92+
`vitest-setup-presets.ts` exposes grouped arrays you can compose if needed:
93+
94+
```ts
95+
import { setupPresets } from '../../tools/vitest-setup-presets.js';
96+
97+
export default createIntConfig('core', {
98+
projectRoot: new URL('../../', import.meta.url),
99+
setupFiles: [...setupPresets.int.portalClient],
100+
});
101+
```
102+
103+
Preset keys:
104+
105+
- `setupPresets.unit.{base,cliui,fs,git,portalClient,matchersCore,matcherPath}`
106+
- `setupPresets.int.{base,cliui,fs,git,portalClient,matcherPath,chromePath}`
107+
- `setupPresets.e2e.{base}`
108+
109+
### Options reference
110+
111+
`CreateVitestConfigOptions` (required + optional):
112+
113+
- `projectKey` (string): coverage/cache naming.
114+
- `kind` ('unit' | 'int' | 'e2e'): test kind.
115+
- `projectRoot` (string | URL): absolute root for all paths.
116+
- `include?: string[]`: override default include globs.
117+
- `setupFiles?: string[]`: extra setup files (appended to baseline; project-root-relative).
118+
- `overrideSetupFiles?: boolean`: skip baseline and use only provided list.
119+
- `globalSetup?: string[]`: override default global setup (project-root-relative).
120+
- `coverage?: { enabled?, exclude?, reportsSubdir? }`
121+
- `testTimeout?: number`: e.g., for E2E.
122+
- `typecheckInclude?: string[]`: include patterns for Vitest typecheck.
123+
- `cacheKey?: string`: custom cache dir suffix.
124+
125+
### Path and URL resolution
126+
127+
- The factory requires `projectRoot` (string path or `URL`).
128+
- Internally, it converts `projectRoot` into a `URL` and resolves all paths with `new URL(relativePath, projectRoot).pathname` to produce absolute filesystem paths.
129+
- Affected fields:
130+
- `cacheDir`, `test.cache.dir`
131+
- `coverage.reportsDirectory`
132+
- default `globalSetup`
133+
- baseline `setupFiles` from presets and any extras you pass
134+
- Expected inputs:
135+
- `setupFiles` and `globalSetup` you pass should be project-root-relative strings.
136+
- No `../../` paths are needed in configs; moving the factory won’t break resolution.
137+
138+
### Merging behavior (arrays and overrides)
139+
140+
- `setupFiles`:
141+
- Baseline files (by kind) are injected automatically.
142+
- Extras in `options.setupFiles` are appended after the baseline.
143+
- Set `overrideSetupFiles: true` to replace the list entirely.
144+
- `coverage.exclude`:
145+
- Defaults to `['mocks/**', '**/types.ts']`.
146+
- If you provide excludes, they are appended to the defaults.
147+
- `include`, `globalSetup`, `testTimeout`, `typecheck.include`:
148+
- If provided, they override the defaults for that suite.
149+
150+
### Notes
151+
152+
- Imports use `.js` extensions to work under ESM.
153+
- No de-duplication of `setupFiles`. Avoid adding duplicates.
154+
- You can opt-in to coverage for E2E by passing `coverage.enabled: true`.

tools/vitest-config-factory.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/// <reference types="vitest" />
2+
import { pathToFileURL } from 'node:url';
3+
import { defineConfig, mergeConfig } from 'vite';
4+
import type { UserConfig as ViteUserConfig } from 'vite';
5+
import type { CoverageOptions } from 'vitest';
6+
import { setupPresets } from './vitest-setup-presets.js';
7+
import { tsconfigPathAliases } from './vitest-tsconfig-path-aliases.js';
8+
9+
export type TestKind = 'unit' | 'int' | 'e2e';
10+
11+
export interface CreateVitestConfigOptions {
12+
projectKey: string;
13+
kind: TestKind;
14+
projectRoot: string | URL;
15+
include?: string[];
16+
setupFiles?: string[];
17+
/** If true, the factory will not inject the baseline setupFiles for the given kind. */
18+
overrideSetupFiles?: boolean;
19+
globalSetup?: string[];
20+
coverage?: {
21+
enabled?: boolean;
22+
exclude?: string[];
23+
reportsSubdir?: string;
24+
};
25+
testTimeout?: number;
26+
typecheckInclude?: string[];
27+
cacheKey?: string;
28+
}
29+
30+
export function createVitestConfig(
31+
options: CreateVitestConfigOptions,
32+
): ViteUserConfig {
33+
const projectRootUrl: URL =
34+
typeof options.projectRoot === 'string'
35+
? pathToFileURL(
36+
options.projectRoot.endsWith('/')
37+
? options.projectRoot
38+
: options.projectRoot + '/',
39+
)
40+
: options.projectRoot;
41+
const cacheDirName = options.cacheKey ?? options.projectKey;
42+
const reportsSubdir =
43+
options.coverage?.reportsSubdir ?? `${options.kind}-tests`;
44+
const coverageEnabled = options.coverage?.enabled ?? options.kind !== 'e2e';
45+
const defaultGlobalSetup =
46+
options.kind === 'e2e'
47+
? undefined
48+
: [new URL('global-setup.ts', projectRootUrl).pathname];
49+
50+
type VitestAwareUserConfig = ViteUserConfig & { test?: unknown };
51+
const baselineSetupByKind: Record<TestKind, readonly string[]> = {
52+
unit: setupPresets.unit.base,
53+
int: setupPresets.int.base,
54+
e2e: setupPresets.e2e.base,
55+
} as const;
56+
57+
const resolveFromRoot = (relativePath: string): string =>
58+
new URL(relativePath, projectRootUrl).pathname;
59+
const mapToAbsolute = (
60+
paths: readonly string[] | undefined,
61+
): string[] | undefined =>
62+
paths == null ? paths : paths.map(resolveFromRoot);
63+
64+
const defaultExclude = ['mocks/**', '**/types.ts'];
65+
66+
const baselineSetupAbs = mapToAbsolute(baselineSetupByKind[options.kind])!;
67+
const extraSetupAbs = mapToAbsolute(options.setupFiles) ?? [];
68+
const finalSetupFiles = options.overrideSetupFiles
69+
? extraSetupAbs
70+
: extraSetupAbs.length > 0
71+
? [...baselineSetupAbs, ...extraSetupAbs]
72+
: undefined; // let base keep baseline when no extras
73+
74+
const baseConfig: VitestAwareUserConfig = {
75+
cacheDir: new URL(`node_modules/.vite/${cacheDirName}`, projectRootUrl)
76+
.pathname,
77+
test: {
78+
reporters: ['basic'],
79+
globals: true,
80+
cache: { dir: new URL('node_modules/.vitest', projectRootUrl).pathname },
81+
alias: tsconfigPathAliases(),
82+
pool: 'threads',
83+
poolOptions: { threads: { singleThread: true } },
84+
environment: 'node',
85+
include:
86+
options.kind === 'unit'
87+
? ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']
88+
: options.kind === 'int'
89+
? ['src/**/*.int.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']
90+
: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
91+
globalSetup: defaultGlobalSetup,
92+
setupFiles: baselineSetupAbs,
93+
...(coverageEnabled
94+
? {
95+
coverage: {
96+
reporter: ['text', 'lcov'],
97+
reportsDirectory: new URL(
98+
`coverage/${options.projectKey}/${reportsSubdir}`,
99+
projectRootUrl,
100+
).pathname,
101+
exclude: defaultExclude,
102+
} as CoverageOptions,
103+
}
104+
: {}),
105+
},
106+
};
107+
108+
const overrideConfig: VitestAwareUserConfig = {
109+
test: {
110+
...(options.include ? { include: options.include } : {}),
111+
...(options.globalSetup
112+
? { globalSetup: mapToAbsolute(options.globalSetup) }
113+
: {}),
114+
...(finalSetupFiles ? { setupFiles: finalSetupFiles } : {}),
115+
...(options.typecheckInclude
116+
? { typecheck: { include: options.typecheckInclude } }
117+
: {}),
118+
...(options.testTimeout != null
119+
? { testTimeout: options.testTimeout }
120+
: {}),
121+
...(coverageEnabled && options.coverage?.exclude
122+
? {
123+
coverage: {
124+
exclude: [...defaultExclude, ...options.coverage.exclude],
125+
} as CoverageOptions,
126+
}
127+
: {}),
128+
},
129+
};
130+
131+
const merged = mergeConfig(
132+
baseConfig as ViteUserConfig,
133+
overrideConfig as ViteUserConfig,
134+
);
135+
return defineConfig(merged);
136+
}
137+
138+
export const createUnitConfig = (
139+
projectKey: string,
140+
rest: Omit<CreateVitestConfigOptions, 'projectKey' | 'kind'>,
141+
): ViteUserConfig => createVitestConfig({ projectKey, kind: 'unit', ...rest });
142+
143+
export const createIntConfig = (
144+
projectKey: string,
145+
rest: Omit<CreateVitestConfigOptions, 'projectKey' | 'kind'>,
146+
): ViteUserConfig => createVitestConfig({ projectKey, kind: 'int', ...rest });
147+
148+
export const createE2eConfig = (
149+
projectKey: string,
150+
rest: Omit<CreateVitestConfigOptions, 'projectKey' | 'kind'>,
151+
): ViteUserConfig => createVitestConfig({ projectKey, kind: 'e2e', ...rest });

tools/vitest-setup-presets.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const setupPresets = {
2+
unit: {
3+
base: [
4+
'testing/test-setup/src/lib/console.mock.ts',
5+
'testing/test-setup/src/lib/reset.mocks.ts',
6+
],
7+
cliui: ['testing/test-setup/src/lib/cliui.mock.ts'],
8+
fs: ['testing/test-setup/src/lib/fs.mock.ts'],
9+
git: ['testing/test-setup/src/lib/git.mock.ts'],
10+
portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'],
11+
matchersCore: [
12+
'testing/test-setup/src/lib/extend/ui-logger.matcher.ts',
13+
'testing/test-setup/src/lib/extend/markdown-table.matcher.ts',
14+
'testing/test-setup/src/lib/extend/jest-extended.matcher.ts',
15+
],
16+
matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'],
17+
},
18+
int: {
19+
base: [
20+
'testing/test-setup/src/lib/console.mock.ts',
21+
'testing/test-setup/src/lib/reset.mocks.ts',
22+
],
23+
cliui: ['testing/test-setup/src/lib/cliui.mock.ts'],
24+
fs: ['testing/test-setup/src/lib/fs.mock.ts'],
25+
git: ['testing/test-setup/src/lib/git.mock.ts'],
26+
portalClient: ['testing/test-setup/src/lib/portal-client.mock.ts'],
27+
matcherPath: ['testing/test-setup/src/lib/extend/path.matcher.ts'],
28+
chromePath: ['testing/test-setup/src/lib/chrome-path.mock.ts'],
29+
},
30+
e2e: {
31+
base: ['testing/test-setup/src/lib/reset.mocks.ts'],
32+
},
33+
} as const;

0 commit comments

Comments
 (0)