one
Version:
One is a new React Framework that makes Vite serve both native and web.
240 lines (210 loc) • 7.18 kB
text/typescript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('vite', async () => {
const actual = await vi.importActual<typeof import('vite')>('vite')
return {
...actual,
createServerModuleRunner: vi.fn(() => ({
clearCache: vi.fn(),
import: vi.fn(),
})),
}
})
type MiddlewareHandler = (
req: object,
res: object,
next: (error?: unknown) => void
) => void | Promise<void>
type WatcherListener = (...args: string[]) => void | Promise<void>
describe('createFileSystemRouterPlugin', () => {
const previousIsVxrnCli = process.env.IS_VXRN_CLI
const previousViteEnvironment = process.env.VITE_ENVIRONMENT
let previousVxrnPluginConfig: unknown
let tempRoot: string | undefined
beforeEach(() => {
previousVxrnPluginConfig = (globalThis as any).__vxrnPluginConfig__
})
afterEach(() => {
if (tempRoot) {
rmSync(tempRoot, { recursive: true, force: true })
tempRoot = undefined
}
if (previousIsVxrnCli === undefined) {
delete process.env.IS_VXRN_CLI
} else {
process.env.IS_VXRN_CLI = previousIsVxrnCli
}
if (previousViteEnvironment === undefined) {
delete process.env.VITE_ENVIRONMENT
} else {
process.env.VITE_ENVIRONMENT = previousViteEnvironment
}
if (previousVxrnPluginConfig === undefined) {
delete (globalThis as any).__vxrnPluginConfig__
} else {
;(globalThis as any).__vxrnPluginConfig__ = previousVxrnPluginConfig
}
vi.restoreAllMocks()
})
it('keeps route watcher rebuild errors handled', async () => {
process.env.IS_VXRN_CLI = '1'
tempRoot = mkdtempSync(path.join(tmpdir(), 'one-router-watch-'))
const appDir = path.join(tempRoot, 'app')
writeFileSync(path.join(tempRoot, 'package.json'), '{}\n')
mkdirSync(appDir)
writeFileSync(
path.join(appDir, 'index.tsx'),
'export default function Index() { return null }\n'
)
;(globalThis as any).__vxrnPluginConfig__ = {
web: {
defaultRenderMode: 'ssg',
},
}
const { createFileSystemRouterPlugin } = await import('./fileSystemRouterPlugin')
const plugin = createFileSystemRouterPlugin({
router: {
root: appDir,
},
})
let watcherListener:
| ((event: string, changedPath: string) => void | Promise<void>)
| undefined
const server = {
environments: {
ssr: {},
},
watcher: {
addListener: vi.fn((event: string, listener: typeof watcherListener) => {
if (event === 'all') {
watcherListener = listener
}
}),
on: vi.fn(),
},
}
;(plugin as any).configureServer(server)
expect(watcherListener).toBeDefined()
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
delete (globalThis as any).__vxrnPluginConfig__
if (!watcherListener) {
throw new Error('Expected route watcher listener to be registered')
}
await expect(
Promise.resolve(watcherListener('add', path.join(appDir, 'new-route.tsx')))
).resolves.toBeUndefined()
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[one] Failed to rebuild routes'),
expect.any(Error)
)
})
it('caches dev ssg html until the module runner is invalidated', async () => {
process.env.VITE_ENVIRONMENT = 'ssr'
tempRoot = mkdtempSync(path.join(tmpdir(), 'one-router-ssg-cache-'))
const appDir = path.join(tempRoot, 'app')
const routeFile = path.join(appDir, 'index+ssg.tsx')
writeFileSync(path.join(tempRoot, 'package.json'), '{}\n')
mkdirSync(appDir)
writeFileSync(routeFile, 'export default function Index() { return null }\n')
const { createServerModuleRunner } = await import('vite')
const { createFileSystemRouterPlugin } = await import('./fileSystemRouterPlugin')
const { virtualEntryId } = await import('./virtualEntryConstants')
let renderCount = 0
const render = vi.fn(async () => `<html><body>${++renderCount}</body></html>`)
const runner = {
clearCache: vi.fn(),
import: vi.fn(async (id: string) => {
if (id === virtualEntryId) {
return { default: { render } }
}
if (id === routeFile) {
return { default: () => null }
}
return {}
}),
}
vi.mocked(createServerModuleRunner).mockReturnValue(runner as any)
const middlewareHandlers: MiddlewareHandler[] = []
const watcherListeners = new Map<string, WatcherListener[]>()
const server = {
environments: {
ssr: {},
},
hot: {
send: vi.fn(),
},
middlewares: {
use: vi.fn((handler: MiddlewareHandler) => {
middlewareHandlers.push(handler)
}),
},
watcher: {
add: vi.fn(),
addListener: vi.fn((event: string, listener: WatcherListener) => {
watcherListeners.set(event, [...(watcherListeners.get(event) || []), listener])
}),
on: vi.fn((event: string, listener: WatcherListener) => {
watcherListeners.set(event, [...(watcherListeners.get(event) || []), listener])
}),
},
}
const plugin = createFileSystemRouterPlugin({
router: {
root: appDir,
},
server: {
loggingEnabled: false,
},
})
const installMiddlewares = (plugin as any).configureServer(server)
installMiddlewares()
const routeMiddleware = middlewareHandlers.at(-1)
if (!routeMiddleware) {
throw new Error('Expected route middleware to be registered')
}
const handleRouteRequest = routeMiddleware
async function request(pathname: string) {
const chunks: string[] = []
const req = {
originalUrl: pathname,
url: pathname,
headers: {
host: 'localhost',
},
method: 'GET',
}
const res = {
setHeader: vi.fn(),
appendHeader: vi.fn(),
writeHead: vi.fn(),
write: vi.fn((chunk: string) => {
chunks.push(chunk)
}),
end: vi.fn(),
}
const next = vi.fn((error?: unknown) => {
if (error) {
throw error
}
throw new Error('Expected One router middleware to handle request')
})
await handleRouteRequest(req, res, next)
expect(res.end).toHaveBeenCalled()
return chunks.join('')
}
await expect(request('/')).resolves.toContain('<body>1</body>')
await expect(request('/')).resolves.toContain('<body>1</body>')
expect(render).toHaveBeenCalledTimes(1)
const allListeners = watcherListeners.get('all') || []
const invalidateRunner = allListeners[0]
if (!invalidateRunner) {
throw new Error('Expected runner invalidation listener to be registered')
}
invalidateRunner('change', routeFile)
await expect(request('/')).resolves.toContain('<body>2</body>')
expect(render).toHaveBeenCalledTimes(2)
expect(runner.clearCache).toHaveBeenCalledTimes(2)
})
})