UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

456 lines (407 loc) • 17.9 kB
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() }) })