Skip to content

Commit

Permalink
refactor: move to monorepo (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jan 19, 2023
0 parents commit 982b46a
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/vitest-environment-nuxt/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Nuxt } from '@nuxt/schema'
import type { InlineConfig as VitestConfig } from 'vitest'
import { InlineConfig, mergeConfig, defineConfig } from 'vite'
import modules from './module'

interface GetVitestConfigOptions {
nuxt: Nuxt
viteConfig: InlineConfig
}

// https://github.com/nuxt/framework/issues/6496
async function getNuxtAndViteConfig(rootDir = process.cwd()) {
const { loadNuxt, buildNuxt } = await import('@nuxt/kit')
const nuxt = await loadNuxt({
cwd: rootDir,
dev: false,
ready: false,
overrides: {
ssr: false,
app: {
rootId: 'nuxt-test',
},
},
})
nuxt.options.modules.push(modules)
await nuxt.ready()

return new Promise<GetVitestConfigOptions>((resolve, reject) => {
nuxt.hook('vite:extendConfig', viteConfig => {
resolve({ nuxt, viteConfig })
throw new Error('_stop_')
})
buildNuxt(nuxt).catch(err => {
if (!err.toString().includes('_stop_')) {
reject(err)
}
})
}).finally(() => nuxt.close())
}

export async function getVitestConfig(
options?: GetVitestConfigOptions
): Promise<InlineConfig & { test: VitestConfig }> {
if (!options) options = await getNuxtAndViteConfig()

return {
...options.viteConfig,
test: {
...options.viteConfig.test,
dir: options.nuxt.options.rootDir,
environment: 'nuxt',
deps: {
...options.viteConfig.test?.deps,
inline:
options.viteConfig.test?.deps?.inline === true
? true
: [
// vite-node defaults
/\/(nuxt|nuxt3)\//,
/^#/,
// additional deps
'vue',
'vitest-environment-nuxt',
...(options.nuxt.options.build.transpile.filter(
r => typeof r === 'string' || r instanceof RegExp
) as Array<string | RegExp>),
...(options.viteConfig.test?.deps?.inline || []),
],
},
},
}
}

export function defineConfigWithNuxtEnv(config: InlineConfig = {}) {
return defineConfig(async () => {
return mergeConfig(await getVitestConfig(), config)
})
}
82 changes: 82 additions & 0 deletions src/vitest-environment-nuxt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Environment } from 'vitest'
import { Window, GlobalWindow } from 'happy-dom'
import { createFetch } from 'ofetch'
import { createApp, toNodeListener } from 'h3'
import type { App } from 'h3'
import { populateGlobal } from 'vitest/environments'
import {
createCall,
createFetch as createLocalFetch,
} from 'unenv/runtime/fetch/index'

export default <Environment>{
name: 'nuxt',
async setup() {
const win = new (GlobalWindow || Window)() as any as Window & {
__app: App
__registry: Set<string>
__NUXT__: any
$fetch: any
fetch: any
IntersectionObserver: any
}

win.__NUXT__ = {
serverRendered: false,
config: {
public: {},
app: { baseURL: '/' },
},
data: {},
state: {},
}

const app = win.document.createElement('div')
// this is a workaround for a happy-dom bug with ids beginning with _
app.id = 'nuxt-test'
win.document.body.appendChild(app)

win.IntersectionObserver =
win.IntersectionObserver ||
class IntersectionObserver {
observe() {}
}

const h3App = createApp()

// @ts-expect-error TODO: fix in h3
const localCall = createCall(toNodeListener(h3App))
const localFetch = createLocalFetch(localCall, globalThis.fetch)

const registry = new Set<string>()

win.fetch = (init: string, options?: any) => {
if (typeof init === 'string' && registry.has(init)) {
init = '/_' + init
}
return localFetch(init, options)
}

win.$fetch = createFetch({ fetch: win.fetch, Headers: win.Headers as any })

win.__registry = registry
win.__app = h3App

const { keys, originals } = populateGlobal(global, win, {
bindFunctions: true,
})

await import('#app/entry')

return {
// called after all tests with this env have been run
teardown() {
win.happyDOM.cancelAsync()
// @ts-expect-error
keys.forEach(key => delete global[key])
// @ts-expect-error
originals.forEach((v, k) => (global[k] = v))
},
}
},
}
11 changes: 11 additions & 0 deletions src/vitest-environment-nuxt/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineNuxtModule, installModule } from '@nuxt/kit'
import autoImportMock from './modules/auto-import-mock'

export default defineNuxtModule({
meta: {
name: 'vitest-env',
},
async setup() {
await installModule(autoImportMock)
},
})
127 changes: 127 additions & 0 deletions src/vitest-environment-nuxt/modules/auto-import-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { Import } from 'unimport'
import { addVitePlugin, defineNuxtModule } from '@nuxt/kit'
import { walk } from 'estree-walker'
import type { CallExpression } from 'estree'
import { AcornNode } from 'rollup'
import MagicString from 'magic-string'

const HELPER_NAME = 'mockNuxtImport'

export interface MockInfo {
name: string
import: Import
factory: string
start: number
end: number
}

/**
* This module is a macro that transforms `mockNuxtImport()` to `vi.mock()`,
* which make it possible to mock Nuxt imports.
*/
export default defineNuxtModule({
meta: {
name: 'vitest-env:auto-import-mock',
},
setup(_, nuxt) {
let imports: Import[] = []

nuxt.hook('imports:extend', _ => {
imports = _
})

addVitePlugin({
name: 'nuxt:auto-import-mock',
transform(code, id) {
if (!code.includes(HELPER_NAME)) return
if (id.includes('/node_modules/')) return

let ast: AcornNode
try {
ast = this.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
ranges: true,
})
} catch (e) {
return
}

const mocks: MockInfo[] = []

walk(ast, {
enter: node => {
if (node.type !== 'CallExpression') return
const call = node as CallExpression
if (
call.callee.type !== 'Identifier' ||
call.callee.name !== HELPER_NAME
) {
return
}
if (call.arguments.length !== 2) {
return
}
if (call.arguments[0].type !== 'Literal') {
return // TODO: warn
}
const name = call.arguments[0].value as string
const importItem = imports.find(_ => name === (_.as || _.name))
if (!importItem) {
return this.error(`Cannot find import "${name}" to mock`)
}
mocks.push({
name,
import: importItem,
factory: code.slice(
call.arguments[1].range![0],
call.arguments[1].range![1]
),
start: call.range![0],
end: call.range![1],
})
},
})

if (!mocks.length) return

const s = new MagicString(code)

const mockMap = new Map<string, MockInfo[]>()
for (const mock of mocks) {
s.overwrite(mock.start, mock.end, '')
if (!mockMap.has(mock.import.from)) {
mockMap.set(mock.import.from, [])
}
mockMap.get(mock.import.from)!.push(mock)
}

const mockCode = [...mockMap.entries()]
.map(([from, mocks]) => {
const lines = [
`vi.mock(${JSON.stringify(from)}, async () => {`,
` const mod = { ...await vi.importActual(${JSON.stringify(
from
)}) }`,
]
for (const mock of mocks) {
lines.push(
` mod[${JSON.stringify(mock.name)}] = (${mock.factory})()`
)
}
lines.push(` return mod`)
lines.push(`})`)
return lines.join('\n')
})
.join('\n')

s.append('\nimport {vi} from "vitest";\n' + mockCode)

return {
code: s.toString(),
map: s.generateMap(),
}
},
})
},
})
33 changes: 33 additions & 0 deletions src/vitest-environment-nuxt/runtime/components/RouterLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { defineComponent, useRouter } from '#imports'

export const RouterLink = defineComponent({
functional: true,
props: {
to: String,
custom: Boolean,
replace: Boolean,
// Not implemented
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: String,
},
setup: (props, { slots }) => {
const navigate = () => {}
return () => {
const route = props.to ? useRouter().resolve(props.to) : {}
return props.custom
? slots.default?.({ href: props.to, navigate, route })
: h(
'a',
{
href: props.to,
onClick: (e: MouseEvent) => {
e.preventDefault()
return navigate()
},
},
slots
)
}
},
})
18 changes: 18 additions & 0 deletions src/vitest-environment-nuxt/runtime/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineEventHandler } from 'h3'
import type { EventHandler } from 'h3'

export function registerEndpoint(url: string, handler: EventHandler) {
// @ts-expect-error private property
if (!window.__app) return
// @ts-expect-error private property
window.__app.use('/_' + url, defineEventHandler(handler))
// @ts-expect-error private property
window.__registry.add(url)
}

export function mockNuxtImport<T = any>(
name: string,
factory: () => T | Promise<T>
) {
throw new Error('mockNuxtImport() is a macro and it did not get transpiled')
}
32 changes: 32 additions & 0 deletions src/vitest-environment-nuxt/runtime/mount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { mount, VueWrapper } from '@vue/test-utils'
import { h, DefineComponent, Suspense, nextTick } from 'vue'

import { RouterLink } from './components/RouterLink'

// @ts-expect-error virtual file
import NuxtRoot from '#build/root-component.mjs'

export async function mountSuspended<
T extends DefineComponent<any, any, any, any>
>(component: T) {
return new Promise<VueWrapper<InstanceType<T>>>(resolve => {
const vm = mount(
{
setup: NuxtRoot.setup,
render: () =>
h(
Suspense,
{ onResolve: () => nextTick().then(() => resolve(vm as any)) },
{ default: () => h(component) }
),
},
{
global: {
components: {
RouterLink,
},
},
}
)
})
}
2 changes: 2 additions & 0 deletions src/vitest-environment-nuxt/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { registerEndpoint, mockNuxtImport } from './runtime/mock'
export { mountSuspended } from './runtime/mount'

0 comments on commit 982b46a

Please sign in to comment.