Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: load nuxt app within setupFiles #260

Merged
merged 1 commit into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@
"@testing-library/vue": "8.0.1",
"@types/estree": "1.0.5",
"@types/jsdom": "21.1.6",
"@vitejs/plugin-vue": "4.5.1",
"@vitejs/plugin-vue-jsx": "3.1.0",
"@vitest/ui": "0.33.0",
"@vue/test-utils": "2.4.3",
"changelogen": "0.5.5",
Expand All @@ -89,8 +87,6 @@
"peerDependencies": {
"@jest/globals": "^29.5.0",
"@testing-library/vue": "^7.0.0 || ^8.0.1",
"@vitejs/plugin-vue": "*",
"@vitejs/plugin-vue-jsx": "*",
"@vitest/ui": "0.33.0",
"@vue/test-utils": "^2.4.2",
"h3": "*",
Expand Down
6 changes: 0 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 22 additions & 25 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { Nuxt, NuxtConfig, ViteConfig } from '@nuxt/schema'
import type { Nuxt, NuxtConfig } from '@nuxt/schema'
import type { InlineConfig as VitestConfig } from 'vitest'
import { defineConfig } from 'vite'
import type { InlineConfig } from 'vite'
import vuePlugin from '@vitejs/plugin-vue'
import viteJsxPlugin from '@vitejs/plugin-vue-jsx'
import { defu } from 'defu'
import { createResolver } from '@nuxt/kit'

interface GetVitestConfigOptions {
nuxt: Nuxt
Expand Down Expand Up @@ -39,7 +38,7 @@ async function startNuxtAndGetViteConfig(
}

const promise = new Promise<GetVitestConfigOptions>((resolve, reject) => {
nuxt.hook('vite:extendConfig', (viteConfig, { isClient }) => {
nuxt.hook('vite:configResolved', (viteConfig, { isClient }) => {
if (isClient) {
resolve({ nuxt, viteConfig })
throw new Error('_stop_')
Expand All @@ -55,35 +54,24 @@ async function startNuxtAndGetViteConfig(
return promise
}

const vuePlugins = {
'vite:vue': [vuePlugin, 'vue'],
'vite:vue-jsx': [viteJsxPlugin, 'vueJsx'],
} as const

export async function getVitestConfigFromNuxt(
options?: GetVitestConfigOptions,
overrides?: NuxtConfig
): Promise<InlineConfig & { test: VitestConfig }> {
const { rootDir = process.cwd(), ..._overrides } = overrides || {}
if (!options) options = await startNuxtAndGetViteConfig(rootDir, {
test: true,
..._overrides
})
options.viteConfig.plugins = options.viteConfig.plugins || []
options.viteConfig.plugins = options.viteConfig.plugins.filter(
p => (p as any)?.name !== 'nuxt:import-protection'
)

for (const name in vuePlugins) {
if (!options.viteConfig.plugins?.some(p => (p as any)?.name === name)) {
const [plugin, key] = vuePlugins[name as keyof typeof vuePlugins]
options.viteConfig.plugins.unshift(
// @ts-expect-error mismatching component options
plugin((options.viteConfig as ViteConfig)[key])
)
}
if (!options) {
options = await startNuxtAndGetViteConfig(rootDir, {
test: true,
..._overrides
})
}

options.viteConfig.plugins ||= []
options.viteConfig.plugins = options.viteConfig.plugins?.filter(
p => (p as any)?.name !== 'nuxt:import-protection'
)

const resolvedConfig = defu(
// overrides
{
Expand Down Expand Up @@ -111,6 +99,8 @@ export async function getVitestConfigFromNuxt(
/^#/,
// additional deps
'@nuxt/test-utils',
'@nuxt/test-utils-nightly',
'@nuxt/test-utils-edge',
'vitest-environment-nuxt',
...(options.nuxt.options.build.transpile.filter(
r => typeof r === 'string' || r instanceof RegExp
Expand Down Expand Up @@ -157,6 +147,13 @@ export async function getVitestConfigFromNuxt(
// TODO: fix this by separating nuxt/node vitest configs
// typescript currently checks this to determine if it can access the filesystem: https://github.com/microsoft/TypeScript/blob/d4fbc9b57d9aa7d02faac9b1e9bb7b37c687f6e9/src/compiler/core.ts#L2738-L2749
delete resolvedConfig.define!['process.browser']

if (!Array.isArray(resolvedConfig.test.setupFiles)) {
resolvedConfig.test.setupFiles = [resolvedConfig.test.setupFiles].filter(Boolean) as string[]
}

const resolver = createResolver(import.meta.url)
resolvedConfig.test.setupFiles.unshift(resolver.resolve('./runtime/entry'))

return resolvedConfig
}
Expand Down
6 changes: 3 additions & 3 deletions src/environments/vitest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export default <Environment>{
jsdom: { url },
}))

win.__NUXT_VITEST_ENVIRONMENT__ = true

win.__NUXT__ = {
serverRendered: false,
config: {
Expand Down Expand Up @@ -137,14 +139,12 @@ export default <Environment>{
registry.add(`${manifestOutputPath}/meta/test.json`)
registry.add(`${manifestOutputPath}/meta/dev.json`)

await import('#app/entry').then(r => r.default())

return {
// called after all tests with this env have been run
teardown() {
teardown()
keys.forEach(key => delete global[key])
originals.forEach((v, k) => (global[k] = v))
teardown()
},
}
},
Expand Down
1 change: 1 addition & 0 deletions src/environments/vitest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type NuxtBuiltinEnvironment = 'happy-dom' | 'jsdom'
export interface NuxtWindow extends Window {
__app: App
__registry: Set<string>
__NUXT_VITEST_ENVIRONMENT__?: boolean
__NUXT__: any
$fetch: any
fetch: any
Expand Down
33 changes: 16 additions & 17 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { pathToFileURL } from 'node:url'
import { defineNuxtModule, logger, resolvePath } from '@nuxt/kit'
import { addVitePlugin, createResolver, defineNuxtModule, logger, resolvePath } from '@nuxt/kit'
import type { File, Reporter, Vitest, UserConfig as VitestConfig } from 'vitest'
import { mergeConfig } from 'vite'
import type { InlineConfig as ViteConfig } from 'vite'
import { getVitestConfigFromNuxt } from './config'
import { getPort } from 'get-port-please'
import { h } from 'vue'
import { debounce } from 'perfect-debounce'
import { isCI } from 'std-env'
import { defu } from 'defu'

import { getVitestConfigFromNuxt } from './config'
import { setupImportMocking } from './module/mock'
import { NuxtRootStubPlugin } from './module/plugins/entry'

export interface NuxtVitestOptions {
startOnBoot?: boolean
Expand Down Expand Up @@ -37,6 +39,12 @@ export default defineNuxtModule<NuxtVitestOptions>({
setupImportMocking()
}

const resolver = createResolver(import.meta.url)
addVitePlugin(NuxtRootStubPlugin.vite({
entry: await resolvePath('#app/entry', { alias: nuxt.options.alias }),
rootStubPath: await resolvePath(resolver.resolve('./runtime/nuxt-root')),
}))

if (!nuxt.options.dev) return

if (nuxt.options.test && nuxt.options.app.rootId === '__nuxt') {
Expand All @@ -49,7 +57,7 @@ export default defineNuxtModule<NuxtVitestOptions>({
const rawViteConfigPromise = new Promise<ViteConfig>(resolve => {
// Wrap with app:resolve to ensure we got the final vite config
nuxt.hook('app:resolve', () => {
nuxt.hook('vite:extendConfig', (config, { isClient }) => {
nuxt.hook('vite:configResolved', (config, { isClient }) => {
if (isClient) resolve(config)
})
})
Expand All @@ -69,16 +77,14 @@ export default defineNuxtModule<NuxtVitestOptions>({
async function start() {
const rawViteConfig = mergeConfig({}, await rawViteConfigPromise)

const viteConfig = await getVitestConfigFromNuxt({ nuxt, viteConfig: rawViteConfig })
const viteConfig = await getVitestConfigFromNuxt({ nuxt, viteConfig: defu({ test: options.vitestConfig }, rawViteConfig) })

viteConfig.plugins = (viteConfig.plugins || []).filter((p: any) => {
return !vitePluginBlocklist.includes(p?.name)
})

process.env.__NUXT_VITEST_RESOLVED__ = 'true'
const { startVitest } = (await import(
pathToFileURL(await resolvePath('vitest/node')).href
)) as typeof import('vitest/node')
const { startVitest } = (await import(pathToFileURL(await resolvePath('vitest/node')).href)) as typeof import('vitest/node')

const customReporter: Reporter = {
onInit(_ctx) {
Expand All @@ -97,10 +103,9 @@ export default defineNuxtModule<NuxtVitestOptions>({
const watchMode = !process.env.NUXT_VITEST_DEV_TEST && !isCI

// For testing dev mode in CI, maybe expose an option to user later
const vitestConfig: VitestConfig = watchMode
const overrides: VitestConfig = watchMode
? {
passWithNoTests: true,
...options.vitestConfig,
reporters: options.logToConsole
? [
...toArray(options.vitestConfig?.reporters ?? ['default']),
Expand All @@ -114,16 +119,10 @@ export default defineNuxtModule<NuxtVitestOptions>({
port: PORT,
},
}
: {
...options.vitestConfig,
watch: false,
}

// TODO: Investigate segfault when loading config file in Nuxt
viteConfig.configFile = false
: { watch: false }

// Start Vitest
const promise = startVitest('test', [], vitestConfig, viteConfig)
const promise = startVitest('test', [], defu(overrides, viteConfig.test), viteConfig)
promise.catch(() => process.exit(1))

if (watchMode) {
Expand Down
29 changes: 29 additions & 0 deletions src/module/plugins/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { readFileSync } from 'node:fs'
import { createUnplugin } from "unplugin"

const PLUGIN_NAME = 'nuxt:vitest:nuxt-root-stub'

interface NuxtRootStubPluginOptions {
entry: string
rootStubPath: string
}

export const NuxtRootStubPlugin = createUnplugin((options: NuxtRootStubPluginOptions) => {
return {
name: PLUGIN_NAME,
enforce: 'pre',
vite: {
async resolveId(id) {
if (id.endsWith('nuxt-vitest-app-entry')) {
return id
}
},
async load(id) {
if (id.endsWith('nuxt-vitest-app-entry')) {
const entryContents = readFileSync(options.entry, 'utf-8')
return entryContents.replace('#build/root-component.mjs', options.rootStubPath)
}
}
},
}
})
48 changes: 20 additions & 28 deletions src/module/plugins/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { Plugin } from 'vite'
import { normalize, resolve } from 'node:path'
import { createUnplugin } from 'unplugin'
import { resolvePath } from '@nuxt/kit'

export interface MockPluginContext {
imports: Import[]
Expand Down Expand Up @@ -106,7 +107,7 @@
const name = importName.value
const importItem = ctx.imports.find(_ => name === (_.as || _.name))
if (!importItem) {
console.log({ imports: ctx.imports })

Check warning on line 110 in src/module/plugins/mock.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
return this.error(`Cannot find import "${name}" to mock`)
}

Expand Down Expand Up @@ -193,30 +194,20 @@
([from, mocks]) => {
importPathsList.add(from)
const lines = [
`vi.mock(${JSON.stringify(
from
)}, async (importOriginal) => {`,
` const mocks = global.${HELPER_MOCK_HOIST}`,
` if (!mocks[${JSON.stringify(
from
)}]) { mocks[${JSON.stringify(
from
)}] = { ...await importOriginal(${JSON.stringify(
from
)}) } }`,
`vi.mock(${JSON.stringify(from)}, async (importOriginal) => {`,
` const mocks = globalThis.${HELPER_MOCK_HOIST}`,
` if (!mocks[${JSON.stringify(from)}]) {`,
` mocks[${JSON.stringify(from)}] = { ...await importOriginal(${JSON.stringify(from)}) }`,
` }`,
]
for (const mock of mocks) {
if (mock.import.name === 'default') {
lines.push(
` mocks[${JSON.stringify(from)}]["default"] = await (${
mock.factory
})();`
` mocks[${JSON.stringify(from)}]["default"] = await (${mock.factory})();`
)
} else {
lines.push(
` mocks[${JSON.stringify(from)}][${JSON.stringify(
mock.name
)}] = await (${mock.factory})();`
` mocks[${JSON.stringify(from)}][${JSON.stringify(mock.name)}] = await (${mock.factory})();`
)
}
}
Expand Down Expand Up @@ -245,7 +236,7 @@
if (!mockLines.length) return

s.prepend(`vi.hoisted(() => {
if(!global.${HELPER_MOCK_HOIST}){
if(!globalThis.${HELPER_MOCK_HOIST}){
vi.stubGlobal(${JSON.stringify(HELPER_MOCK_HOIST)}, {})
}
});\n`)
Expand Down Expand Up @@ -274,28 +265,29 @@
vite: {
transform,
// Place Vitest's mock plugin after all Nuxt plugins
configResolved(config) {
async configResolved(config) {
const firstSetupFile = Array.isArray(config.test?.setupFiles)
? config.test!.setupFiles[0]
? config.test!.setupFiles.find(p => !p.includes('runtime/entry'))
: config.test?.setupFiles

if (firstSetupFile) {
resolvedFirstSetupFile = normalize(resolve(firstSetupFile))
resolvedFirstSetupFile = await resolvePath(normalize(resolve(firstSetupFile)))
}

const plugins = (config.plugins || []) as Plugin[]

// `vite:mocks` was a typo in Vitest before v0.34.0
const mockPluginIndex = plugins.findIndex(
i => i.name === 'vite:mocks' || i.name === 'vitest:mocks'
)
const vitestPlugins = plugins.filter(p => p.name === 'vite:mocks' || p.name.startsWith('vitest:'))
const lastNuxt = findLastIndex(
plugins,
i => i.name?.startsWith('nuxt:')
)
if (mockPluginIndex !== -1 && lastNuxt !== -1) {
if (mockPluginIndex < lastNuxt) {
const [mockPlugin] = plugins.splice(mockPluginIndex, 1)
plugins.splice(lastNuxt, 0, mockPlugin)
if (lastNuxt === -1) return
for (const plugin of vitestPlugins) {
const index = plugins.indexOf(plugin)
if (index < lastNuxt) {
plugins.splice(index, 1)
plugins.splice(lastNuxt, 0, plugin)
}
}
},
Expand Down
15 changes: 15 additions & 0 deletions src/runtime/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
if (
typeof window !== 'undefined' &&
// @ts-expect-error undefined property
window.__NUXT_VITEST_ENVIRONMENT__
) {
// @ts-expect-error alias to allow us to transform the entrypoint
await import('#app/nuxt-vitest-app-entry').then(r => r.default())
// We must manually call `page:finish` to snc route after navigation
// as there is no `<NuxtPage>` instantiated by default.
const nuxtApp = useNuxtApp()
await nuxtApp.callHook('page:finish')
useRouter().afterEach(() => nuxtApp.callHook('page:finish'))
}

export {}
Loading
Loading