|
| 1 | +import type { Nuxt, NuxtConfig, ViteConfig } from '@nuxt/schema' |
| 2 | +import type { InlineConfig as VitestConfig } from 'vitest' |
| 3 | +import { defineConfig, mergeConfig } from 'vite' |
| 4 | +import type { InlineConfig } from 'vite' |
| 5 | +import vuePlugin from '@vitejs/plugin-vue' |
| 6 | +import viteJsxPlugin from '@vitejs/plugin-vue-jsx' |
| 7 | +import { defu } from 'defu' |
| 8 | + |
| 9 | +interface GetVitestConfigOptions { |
| 10 | + nuxt: Nuxt |
| 11 | + viteConfig: InlineConfig |
| 12 | +} |
| 13 | + |
| 14 | +// https://github.com/nuxt/framework/issues/6496 |
| 15 | +async function startNuxtAndGetViteConfig( |
| 16 | + rootDir = process.cwd(), |
| 17 | + overrides?: Partial<NuxtConfig> |
| 18 | +) { |
| 19 | + const { loadNuxt, buildNuxt } = await import('@nuxt/kit') |
| 20 | + const nuxt = await loadNuxt({ |
| 21 | + cwd: rootDir, |
| 22 | + dev: false, |
| 23 | + overrides: defu( |
| 24 | + { |
| 25 | + ssr: false, |
| 26 | + test: true, |
| 27 | + modules: ['nuxt-vitest'], |
| 28 | + }, |
| 29 | + overrides |
| 30 | + ), |
| 31 | + }) |
| 32 | + |
| 33 | + if ( |
| 34 | + !nuxt.options._installedModules.find(i => i?.meta?.name === 'nuxt-vitest') |
| 35 | + ) { |
| 36 | + throw new Error( |
| 37 | + 'Failed to load nuxt-vitest module. You may need to add it to your nuxt.config.' |
| 38 | + ) |
| 39 | + } |
| 40 | + |
| 41 | + const promise = new Promise<GetVitestConfigOptions>((resolve, reject) => { |
| 42 | + nuxt.hook('vite:extendConfig', (viteConfig, { isClient }) => { |
| 43 | + if (isClient) { |
| 44 | + resolve({ nuxt, viteConfig }) |
| 45 | + throw new Error('_stop_') |
| 46 | + } |
| 47 | + }) |
| 48 | + buildNuxt(nuxt).catch(err => { |
| 49 | + if (!err.toString().includes('_stop_')) { |
| 50 | + reject(err) |
| 51 | + } |
| 52 | + }) |
| 53 | + }).finally(() => nuxt.close()) |
| 54 | + |
| 55 | + return promise |
| 56 | +} |
| 57 | + |
| 58 | +const vuePlugins = { |
| 59 | + 'vite:vue': [vuePlugin, 'vue'], |
| 60 | + 'vite:vue-jsx': [viteJsxPlugin, 'vueJsx'], |
| 61 | +} as const |
| 62 | + |
| 63 | +export async function getVitestConfigFromNuxt( |
| 64 | + options?: GetVitestConfigOptions, |
| 65 | + overrides?: NuxtConfig |
| 66 | +): Promise<InlineConfig & { test: VitestConfig }> { |
| 67 | + const { rootDir = process.cwd(), ..._overrides } = overrides || {} |
| 68 | + if (!options) options = await startNuxtAndGetViteConfig(rootDir, _overrides) |
| 69 | + options.viteConfig.plugins = options.viteConfig.plugins || [] |
| 70 | + options.viteConfig.plugins = options.viteConfig.plugins.filter( |
| 71 | + p => (p as any)?.name !== 'nuxt:import-protection' |
| 72 | + ) |
| 73 | + |
| 74 | + for (const name in vuePlugins) { |
| 75 | + if (!options.viteConfig.plugins?.some(p => (p as any)?.name === name)) { |
| 76 | + const [plugin, key] = vuePlugins[name as keyof typeof vuePlugins] |
| 77 | + options.viteConfig.plugins.unshift( |
| 78 | + // @ts-expect-error mismatching component options |
| 79 | + plugin((options.viteConfig as ViteConfig)[key]) |
| 80 | + ) |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + return { |
| 85 | + ...options.viteConfig, |
| 86 | + define: { |
| 87 | + ...options.viteConfig.define, |
| 88 | + ['process.env.NODE_ENV']: 'process.env.NODE_ENV', |
| 89 | + }, |
| 90 | + server: { |
| 91 | + ...options.viteConfig.server, |
| 92 | + middlewareMode: false, |
| 93 | + }, |
| 94 | + plugins: [ |
| 95 | + ...options.viteConfig.plugins, |
| 96 | + { |
| 97 | + name: 'disable-auto-execute', |
| 98 | + enforce: 'pre', |
| 99 | + transform(code, id) { |
| 100 | + if (id.match(/nuxt3?\/.*\/entry\./)) { |
| 101 | + return code.replace( |
| 102 | + /(?<!vueAppPromise = )entry\(\)\.catch/, |
| 103 | + 'Promise.resolve().catch' |
| 104 | + ) |
| 105 | + } |
| 106 | + }, |
| 107 | + }, |
| 108 | + ], |
| 109 | + test: { |
| 110 | + ...options.viteConfig.test, |
| 111 | + dir: process.cwd(), |
| 112 | + environmentOptions: { |
| 113 | + ...options.viteConfig.test?.environmentOptions, |
| 114 | + nuxt: { |
| 115 | + rootId: options.nuxt.options.app.rootId || undefined, |
| 116 | + ...options.viteConfig.test?.environmentOptions?.nuxt, |
| 117 | + mock: { |
| 118 | + intersectionObserver: true, |
| 119 | + indexedDb: false, |
| 120 | + ...options.viteConfig.test?.environmentOptions?.nuxt?.mock, |
| 121 | + }, |
| 122 | + }, |
| 123 | + nuxtRuntimeConfig: options.nuxt.options.runtimeConfig, |
| 124 | + nuxtRouteRules: defu( |
| 125 | + {}, |
| 126 | + options.nuxt.options.routeRules, |
| 127 | + options.nuxt.options.nitro?.routeRules |
| 128 | + ), |
| 129 | + }, |
| 130 | + environmentMatchGlobs: [ |
| 131 | + ['**/*.nuxt.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', 'nuxt'], |
| 132 | + ['{test,tests}/nuxt/**.*', 'nuxt'], |
| 133 | + ...(options.viteConfig.test?.environmentMatchGlobs || []), |
| 134 | + ], |
| 135 | + deps: { |
| 136 | + ...options.viteConfig.test?.deps, |
| 137 | + inline: [ |
| 138 | + // vite-node defaults |
| 139 | + /\/node_modules\/(.*\/)?(nuxt|nuxt3)\//, |
| 140 | + /^#/, |
| 141 | + // additional deps |
| 142 | + 'vitest-environment-nuxt', |
| 143 | + ...(options.nuxt.options.build.transpile.filter( |
| 144 | + r => typeof r === 'string' || r instanceof RegExp |
| 145 | + ) as Array<string | RegExp>), |
| 146 | + ...(typeof options.viteConfig.test?.deps?.inline !== 'boolean' |
| 147 | + ? typeof options.viteConfig.test?.deps?.inline |
| 148 | + : []), |
| 149 | + ], |
| 150 | + }, |
| 151 | + }, |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +export function defineVitestConfig(config: InlineConfig = {}) { |
| 156 | + return defineConfig(async () => { |
| 157 | + // When Nuxt module calls `startVitest`, we don't need to call `getVitestConfigFromNuxt` again |
| 158 | + if (process.env.__NUXT_VITEST_RESOLVED__) return config |
| 159 | + |
| 160 | + const overrides = config.test?.environmentOptions?.nuxt?.overrides || {} |
| 161 | + overrides.rootDir = config.test?.environmentOptions?.nuxt?.rootDir |
| 162 | + |
| 163 | + return mergeConfig( |
| 164 | + await getVitestConfigFromNuxt(undefined, overrides), |
| 165 | + config |
| 166 | + ) |
| 167 | + }) |
| 168 | +} |
| 169 | + |
| 170 | +declare module 'vitest' { |
| 171 | + interface EnvironmentOptions { |
| 172 | + nuxt?: { |
| 173 | + rootDir?: string |
| 174 | + /** |
| 175 | + * The starting URL for your Nuxt window environment |
| 176 | + * @default {http://localhost:3000} |
| 177 | + */ |
| 178 | + url?: string |
| 179 | + overrides?: NuxtConfig |
| 180 | + /** |
| 181 | + * The id of the root div to which the app should be mounted. You should also set `app.rootId` to the same value. |
| 182 | + * @default {nuxt-test} |
| 183 | + */ |
| 184 | + rootId?: string |
| 185 | + /** |
| 186 | + * The name of the DOM environment to use. |
| 187 | + * |
| 188 | + * It also needs to be installed as a dev dependency in your project. |
| 189 | + * @default {happy-dom} |
| 190 | + */ |
| 191 | + domEnvironment?: 'happy-dom' | 'jsdom' |
| 192 | + |
| 193 | + mock?: { |
| 194 | + intersectionObserver?: boolean |
| 195 | + indexedDb?: boolean |
| 196 | + } |
| 197 | + } |
| 198 | + } |
| 199 | +} |
0 commit comments