|
| 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 }); |
0 commit comments