one
Version:
One is a new React Framework that makes Vite serve both native and web.
267 lines (236 loc) • 8.24 kB
text/typescript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { EventEmitter } from 'node:events'
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(() => ({
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 previousVxrnVersion = globalThis['__vxrnVersion']
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
}
globalThis['__vxrnVersion'] = previousVxrnVersion
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 { createRouteIndex } = await import('../../utils/routeIndex')
const { createFileSystemRouterPlugin } = await import('./fileSystemRouterPlugin')
const plugin = createFileSystemRouterPlugin(
{
router: {
root: appDir,
},
},
createRouteIndex({ routerRoot: 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 { createRouteIndex } = await import('../../utils/routeIndex')
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 = {
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,
},
},
createRouteIndex({ routerRoot: appDir })
)
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 = Object.assign(new EventEmitter(), {
aborted: false,
complete: true,
originalUrl: pathname,
url: pathname,
headers: {
':authority': 'localhost',
':method': 'GET',
},
method: 'GET',
})
const res = Object.assign(new EventEmitter(), {
destroyed: false,
writableFinished: false,
setHeader: vi.fn(),
appendHeader: vi.fn(),
writeHead: vi.fn(),
write: vi.fn((chunk: string) => {
chunks.push(chunk)
}),
end: vi.fn(() => {
res.writableFinished = true
res.emit('finish')
}),
})
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 versionBeforeChange = globalThis['__vxrnVersion']
// vite calls hotUpdate on the ssr environment after invalidating the graph;
// that is what tells the next render its route tree is built out of pre-edit
// modules
;(plugin as any).hotUpdate.call(
{
environment: {
name: 'ssr',
moduleGraph: { getModulesByFile: () => new Set([{ id: routeFile }]) },
},
},
{ file: routeFile }
)
await expect(request('/')).resolves.toContain('<body>2</body>')
expect(render).toHaveBeenCalledTimes(2)
// the render also has to invalidate the route context the tree is built
// from, otherwise it renders fresh html out of pre-edit route modules
expect(globalThis['__vxrnVersion']).toBe((versionBeforeChange || 0) + 1)
})
})