one
Version:
One is a React framework for web, iOS, and Android apps.
456 lines (407 loc) • 17.9 kB
text/typescript
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { createHandleRequest } from './createDevHandleRequest'
import { resolveAPIRoute } from './createHandleRequest'
import type { Middleware } from './createMiddleware'
import type { RouteInfoCompiled } from './server/createRoutesManifest'
// Mock the manifest. Specific routes come before dynamic ones, matching how
// the real manifest is ordered after matchers.sortRoutes().
vi.mock('./vite/getManifest', () => ({
getManifest: () => ({
pageRoutes: [
{
file: 'app/index.tsx',
page: '/',
namedRegex: '^/(?:/)?$',
routeKeys: {},
type: 'ssr',
middlewares: [],
},
{
file: 'app/(public)/otp+ssg.tsx',
page: '/otp',
namedRegex: '^/otp(?:/)?$',
routeKeys: {},
type: 'ssg',
middlewares: [],
hasLoader: false,
},
{
file: 'app/(public)/profile+ssg.tsx',
page: '/profile',
namedRegex: '^/profile(?:/)?$',
routeKeys: {},
type: 'ssg',
middlewares: [],
hasLoader: true,
},
{
file: 'app/[slug].tsx',
page: '/[slug]',
namedRegex: '^/(?<nxtPslug>[^/]+?)(?:/)?$',
routeKeys: { nxtPslug: 'slug' },
type: 'ssr',
middlewares: [],
},
{
file: 'app/+not-found.tsx',
page: '/+not-found',
namedRegex: '^/(?<nxtPnotfound>.+?)(?:/)?$',
routeKeys: { nxtPnotfound: 'not-found' },
type: 'spa',
middlewares: [],
},
],
apiRoutes: [
{
file: 'app/api/github/agent/run.sh+api.ts',
page: '/api/github/agent/run.sh',
namedRegex: '^/api/github/agent/run\\.sh(?:/)?$',
routeKeys: {},
type: 'api',
middlewares: [],
},
],
}),
}))
function createRequest(path: string, method = 'GET') {
return new Request(`http://localhost:3000${path}`, {
method,
headers: {
host: 'localhost:3000',
},
})
}
describe('createHandleRequest', () => {
const mockHandlers = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('static file extension filtering', () => {
it('should skip paths with file extensions (favicon.ico)', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/favicon.ico'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should skip paths with file extensions (logo.png)', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/logo.png'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should skip paths with file extensions (styles.css)', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/styles.css'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should skip paths with file extensions (robots.txt)', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/robots.txt'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should skip nested paths with file extensions', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/assets/images/logo.png'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should skip extensions 2-4 chars like .xyz', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/somefile.xyz'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should skip common static extensions longer than 4 chars', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/font.woff2'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should not render a user +not-found page for sourcemap requests', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/deps-web/react-BwkDMLyL.js.map'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
it('should still route static-looking API paths', async () => {
const mockHandlersWithAPI = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
handleAPI: vi.fn().mockResolvedValue({
GET: () => new Response('script'),
}),
}
const { handler } = createHandleRequest(mockHandlersWithAPI, {
routerRoot: '/app',
})
const result = (await handler(
createRequest('/api/github/agent/run.sh')
)) as Response | null
expect(result).not.toBeNull()
expect(await result!.text()).toBe('script')
expect(mockHandlersWithAPI.handleAPI).toHaveBeenCalled()
expect(mockHandlersWithAPI.handlePage).not.toHaveBeenCalled()
})
it('should not skip non-static extensions in route names', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
await handler(createRequest('/route.normal'))
expect(mockHandlers.handlePage).toHaveBeenCalled()
})
it('should NOT skip loader paths ending with _vxrn_loader.js', async () => {
const mockHandlersWithLoader = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
handleLoader: vi.fn().mockResolvedValue('loader data'),
}
const { handler } = createHandleRequest(mockHandlersWithLoader, {
routerRoot: '/app',
})
// Loader paths are /assets/<path>_<nonce>_vxrn_loader.js - they should reach handleLoader
// for a matching route (the path /my-page matches the [slug] route)
await handler(createRequest('/assets/my-page_123_vxrn_loader.js'))
expect(mockHandlersWithLoader.handleLoader).toHaveBeenCalled()
})
})
describe('loader dispatch short-circuit for no-loader routes', () => {
it('should return empty loader module without calling handleLoader when route has hasLoader: false', async () => {
// Regression: for SSG routes with no loader export, the worker must NOT
// import the page bundle (which pulls in RN/Tamagui modules that crash
// when evaluated inside workerd on Cloudflare Workers).
const mockHandlersWithLoader = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
handleLoader: vi.fn().mockResolvedValue('loader data'),
}
const { handler } = createHandleRequest(mockHandlersWithLoader, {
routerRoot: '/app',
})
// /otp is an SSG route with hasLoader: false in the mock manifest above.
// The client fetches /assets/otp_<nonce>_vxrn_loader.js when navigating
// client-side to /otp.
const response = (await handler(
createRequest('/assets/otp_123_vxrn_loader.js')
)) as Response | null
// handleLoader must not be called — the whole point is to avoid
// importing the page module on the server for no-loader routes.
expect(mockHandlersWithLoader.handleLoader).not.toHaveBeenCalled()
expect(response).not.toBeNull()
expect(response!.status).toBe(200)
expect(response!.headers.get('Content-Type')).toBe('text/javascript')
const body = await response!.text()
expect(body).toContain('export function loader()')
expect(body).toContain('return undefined')
})
it('should still call handleLoader when route has hasLoader: true', async () => {
const mockHandlersWithLoader = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
handleLoader: vi.fn().mockResolvedValue('export function loader(){return 1}'),
}
const { handler } = createHandleRequest(mockHandlersWithLoader, {
routerRoot: '/app',
})
await handler(createRequest('/assets/profile_123_vxrn_loader.js'))
expect(mockHandlersWithLoader.handleLoader).toHaveBeenCalled()
})
it('should still call handleLoader when hasLoader is undefined (dev/back-compat)', async () => {
// Routes without hasLoader set (e.g. dev mode, older builds) must fall
// through to the normal handleLoader path — we only short-circuit on an
// explicit `false`.
const mockHandlersWithLoader = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
handleLoader: vi.fn().mockResolvedValue('loader data'),
}
const { handler } = createHandleRequest(mockHandlersWithLoader, {
routerRoot: '/app',
})
// /my-page matches the [slug] ssr route which has no hasLoader field
await handler(createRequest('/assets/my-page_123_vxrn_loader.js'))
expect(mockHandlersWithLoader.handleLoader).toHaveBeenCalled()
})
})
describe('dynamic route matching', () => {
it('should match dynamic routes for regular paths without extensions', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
await handler(createRequest('/my-page'))
expect(mockHandlers.handlePage).toHaveBeenCalled()
})
it('should match index route', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
await handler(createRequest('/'))
expect(mockHandlers.handlePage).toHaveBeenCalled()
})
it('should match paths with hyphens', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
await handler(createRequest('/my-awesome-page'))
expect(mockHandlers.handlePage).toHaveBeenCalled()
})
})
describe('request method dispatch', () => {
it('answers HEAD page requests exactly like GET', async () => {
const handlePage = vi.fn(
async () =>
new Response('<html></html>', { headers: { 'content-type': 'text/html' } })
)
const { handler } = createHandleRequest({ handlePage }, { routerRoot: '/app' })
const get = await handler(createRequest('/my-page', 'GET'))
const head = await handler(createRequest('/my-page', 'HEAD'))
if (!(get instanceof Response) || !(head instanceof Response)) {
throw new Error('expected both GET and HEAD to return a Response')
}
// the transport drops the body for HEAD, so the handler layer must agree
// with GET on everything else: status and content type.
expect(head.status).toBe(get.status)
expect(head.headers.get('content-type')).toBe(get.headers.get('content-type'))
expect(handlePage).toHaveBeenCalledTimes(2)
})
it('continues routing HEAD requests to API handlers', async () => {
const mockHandlersWithAPI = {
handlePage: vi.fn().mockResolvedValue('<html></html>'),
handleAPI: vi.fn().mockResolvedValue({
HEAD: () => new Response(null, { status: 204 }),
}),
}
const { handler } = createHandleRequest(mockHandlersWithAPI, {
routerRoot: '/app',
})
const result = await handler(createRequest('/api/github/agent/run.sh', 'HEAD'))
expect(result).toBeInstanceOf(Response)
if (!(result instanceof Response)) {
throw new Error('expected HEAD API request to return a Response')
}
expect(result.status).toBe(204)
expect(mockHandlersWithAPI.handleAPI).toHaveBeenCalledOnce()
expect(mockHandlersWithAPI.handlePage).not.toHaveBeenCalled()
})
it('does not route POST requests to page handlers', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/my-page', 'POST'))
expect(result).toBeNull()
expect(mockHandlers.handlePage).not.toHaveBeenCalled()
})
})
describe('special paths', () => {
it('should skip __vxrnhmr path', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const result = await handler(createRequest('/__vxrnhmr'))
expect(result).toBeNull()
})
it('should skip vite internal paths like /@fs/', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
expect(await handler(createRequest('/@fs/some/path'))).toBeNull()
expect(await handler(createRequest('/@vite/client'))).toBeNull()
expect(await handler(createRequest('/@id/__x00__virtual:one-entry'))).toBeNull()
})
it('should NOT skip user routes that start with @', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
// routes like /@admin should be matched, not skipped
await handler(createRequest('/@admin'))
expect(mockHandlers.handlePage).toHaveBeenCalled()
})
})
describe('requests without a Host header', () => {
// every other test here builds its request through createRequest, which
// always sets Host, and an incoming request on a real server always carries
// one. a request constructed by hand does not, and `host` is a forbidden
// header name in a browser so it cannot be added there. resolving the URL
// against an empty-string base threw "Invalid base URL" even though the
// request URL was already absolute, which took down every embedder that
// builds its own Request.
it('resolves an absolute request URL with no Host header', async () => {
const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' })
const request = new Request('http://localhost:3000/some-page')
expect(request.headers.get('host')).toBeNull()
await expect(handler(request)).resolves.not.toThrow()
expect(mockHandlers.handlePage).toHaveBeenCalled()
})
})
})
describe('API method contract', () => {
const route = {
file: 'app/api/link+api.ts',
page: '/api/link',
namedRegex: '^/api/link$',
routeKeys: {},
type: 'api',
middlewares: [{ contextKey: 'app/_middleware.ts' }],
} as RouteInfoCompiled
function dispatch(exports: Record<string, unknown>, method: string, middleware = true) {
const request = createRequest('/api/link', method)
return resolveAPIRoute(
{
handleAPI: async () => exports,
loadMiddleware: async () => ({
default: (async ({ request, next }) => {
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204 })
}
const response = await next()
if (!response) throw new Error('expected endpoint response')
response.headers.set('x-middleware', 'ran')
return response
}) satisfies Middleware,
}),
},
request,
new URL(request.url),
middleware ? route : { ...route, middlewares: [] }
)
}
it.each([true, false])(
'rejects missing methods with middleware=%s',
async (middleware) => {
const POST = vi.fn(() => new Response('authentication required', { status: 401 }))
const exports = { POST, helper: () => {}, GET: undefined }
const response = await dispatch(exports, 'GET', middleware)
expect(response.status).toBe(405)
expect(response.headers.get('allow')).toBe('POST')
expect(POST).not.toHaveBeenCalled()
if (middleware) expect(response.headers.get('x-middleware')).toBe('ran')
const supported = await dispatch(exports, 'POST', middleware)
expect(supported.status).toBe(401)
expect(await supported.text()).toBe('authentication required')
expect(POST).toHaveBeenCalledOnce()
}
)
it('preserves middleware preflight short circuit', async () => {
const OPTIONS = vi.fn(() => new Response(null, { status: 202 }))
expect((await dispatch({ OPTIONS }, 'OPTIONS')).status).toBe(204)
expect(OPTIONS).not.toHaveBeenCalled()
})
it('preserves named methods ahead of default and default catch-all', async () => {
const fallback = vi.fn(
(request: Request) => new Response(request.method, { status: 202 })
)
const GET = vi.fn(() => new Response('named'))
expect(await (await dispatch({ GET, default: fallback }, 'GET')).text()).toBe('named')
for (const method of ['POST', 'HEAD', 'OPTIONS']) {
expect((await dispatch({ GET, default: fallback }, method, false)).status).toBe(202)
}
expect(GET).toHaveBeenCalledOnce()
expect(fallback).toHaveBeenCalledTimes(3)
})
it('requires explicit HEAD and OPTIONS exports and preserves their responses', async () => {
const GET = vi.fn(() => new Response('get'))
for (const method of ['HEAD', 'OPTIONS']) {
const missing = await dispatch({ GET }, method, false)
expect(missing.status).toBe(405)
expect(missing.headers.get('allow')).toBe('GET')
const explicit = await dispatch(
{
GET,
[method]: () =>
new Response(null, {
status: 204,
headers: { 'x-method': method },
}),
},
method,
false
)
expect(explicit.status).toBe(204)
expect(explicit.headers.get('x-method')).toBe(method)
expect(await explicit.text()).toBe('')
}
expect(GET).not.toHaveBeenCalled()
})
})