diff --git a/packages/one/src/createHandleRequest.test.ts b/packages/one/src/createHandleRequest.test.ts index 78a4cf6d8..f745982dc 100644 --- a/packages/one/src/createHandleRequest.test.ts +++ b/packages/one/src/createHandleRequest.test.ts @@ -242,6 +242,76 @@ describe('createHandleRequest', () => { }) }) + describe('loader responses for ?platform=native requests', () => { + // in dev, useLoader on native fetches loaders with ?platform=native and + // runs the response body as CJS on hermes - a web ESM body + // ("export function loader()...") is a parse error there, which the + // client swallows (the route silently loses its loader data) + + it('should serve loader bodies as CJS for platform=native', async () => { + const mockHandlersWithLoader = { + handlePage: vi.fn().mockResolvedValue(''), + handleLoader: vi + .fn() + .mockResolvedValue('export function loader() { return {"a":1} }'), + } + const { handler } = createHandleRequest(mockHandlersWithLoader, { + routerRoot: '/app', + }) + + const response = (await handler( + createRequest('/assets/profile_123_vxrn_loader.js?platform=native') + )) as Response | null + + expect(response).not.toBeNull() + const body = await response!.text() + expect(body).toContain('exports.loader=') + expect(body).not.toContain('export function loader') + }) + + it('should serve redirect loader bodies as CJS for platform=native', async () => { + const mockHandlersWithLoader = { + handlePage: vi.fn().mockResolvedValue(''), + handleLoader: vi.fn().mockRejectedValue( + new Response(null, { + status: 302, + headers: { location: '/login' }, + }) + ), + } + const { handler } = createHandleRequest(mockHandlersWithLoader, { + routerRoot: '/app', + }) + + const response = (await handler( + createRequest('/assets/profile_123_vxrn_loader.js?platform=native') + )) as Response | null + + expect(response).not.toBeNull() + const body = await response!.text() + expect(body).toContain('exports.loader=') + expect(body).toContain('__oneRedirect') + }) + + it('should keep serving ESM loader bodies to web requests', async () => { + const esmBody = 'export function loader() { return {"a":1} }' + const mockHandlersWithLoader = { + handlePage: vi.fn().mockResolvedValue(''), + handleLoader: vi.fn().mockResolvedValue(esmBody), + } + const { handler } = createHandleRequest(mockHandlersWithLoader, { + routerRoot: '/app', + }) + + const response = (await handler( + createRequest('/assets/profile_123_vxrn_loader.js') + )) as Response | null + + expect(response).not.toBeNull() + expect(await response!.text()).toBe(esmBody) + }) + }) + describe('dynamic route matching', () => { it('should match dynamic routes for regular paths without extensions', async () => { const { handler } = createHandleRequest(mockHandlers, { routerRoot: '/app' }) diff --git a/packages/one/src/createHandleRequest.ts b/packages/one/src/createHandleRequest.ts index 2279b58d7..df1e1512a 100644 --- a/packages/one/src/createHandleRequest.ts +++ b/packages/one/src/createHandleRequest.ts @@ -190,7 +190,8 @@ export async function resolveLoaderRoute( const isNativeRequest = url.searchParams.get('platform') === 'ios' || - url.searchParams.get('platform') === 'android' + url.searchParams.get('platform') === 'android' || + url.searchParams.get('platform') === 'native' const response = await runMiddlewares(handlers, request, route, async () => { return await resolveResponse(async () => { diff --git a/packages/one/src/vite/plugins/fileSystemRouterPlugin.tsx b/packages/one/src/vite/plugins/fileSystemRouterPlugin.tsx index dd9c08e33..ef83952b1 100644 --- a/packages/one/src/vite/plugins/fileSystemRouterPlugin.tsx +++ b/packages/one/src/vite/plugins/fileSystemRouterPlugin.tsx @@ -485,11 +485,22 @@ export function createFileSystemRouterPlugin( throw new Error(`No transformed js returned`) } + const platform = url.searchParams.get('platform') + const isNativeRequest = + platform === 'ios' || platform === 'android' || platform === 'native' + // the client tree-shake plugin replaces loader exports with stubs // like "export function loader()". if no stub exists, this route has // no loader - skip the SSR module import to avoid evaluating modules - // with potentially SSR-incompatible deps (e.g. tamagui in SSR) - if (!/export function loader\(\)/.test(transformedJS)) { + // with potentially SSR-incompatible deps (e.g. tamagui in SSR). + // never take this shortcut for native requests: native runs loader + // responses as CJS on hermes, so returning the vite-transformed web + // ESM (import statements + HMR preamble) is unparseable there and + // silently drops the route's loader data. the stub can also be + // legitimately absent for loader shapes the tree-shaker doesn't + // rewrite (e.g. `export { x as loader } from './loaders'`), which the + // module runner import below resolves fine. + if (!isNativeRequest && !/export function loader\(\)/.test(transformedJS)) { return transformedJS } @@ -562,9 +573,7 @@ export function createFileSystemRouterPlugin( }) } - const platform = url.searchParams.get('platform') - - if (platform === 'ios' || platform === 'android' || platform === 'native') { + if (isNativeRequest) { // Need to transpile to CommonJS for React Native const environment =