Skip to content

fix: serve loader responses as CJS for ?platform=native requests - #755

Closed
clayrisser wants to merge 1 commit into
onejs:mainfrom
clayrisser:fix/native-loader-platform-param
Closed

fix: serve loader responses as CJS for ?platform=native requests#755
clayrisser wants to merge 1 commit into
onejs:mainfrom
clayrisser:fix/native-loader-platform-param

Conversation

@clayrisser

Copy link
Copy Markdown
Contributor

Symptom

In dev, useLoader on native fetches route loaders with ?platform=native (the comment in useLoader.ts even says "?platform=native tells vite plugin to return CJS") and evaluates the response body as CJS on Hermes. But two gates in the request pipeline only recognize platform=ios|android, so platform=native requests can receive the Vite-transformed web ESM module instead. Hermes then throws while compiling the loader body:

[one] native loader error for /pokemon: 1:21:import declaration must be at top level of module

The error is caught and swallowed (data: {}), so the route silently loses its server-side loader data — including __oneRedirect / __oneError signals, meaning server redirects thrown from loaders never reach the native client.

Root cause (two spots)

  1. createHandleRequest.tsresolveLoaderRoute: isNativeRequest misses platform=native, so the CJS conversion (toCjsLoader) and the redirect (302) / auth (401/403) fallback loader bodies are emitted as web ESM for those requests. (The dispatch-level check in handleRequest already recognizes native; this inner one didn't.)

  2. fileSystemRouterPlugin.tsxhandleLoader: the export function loader() stub-regex gate early-returns the raw Vite-transformed web ESM (HMR preamble included) above the platform check that would have produced the native CJS body. Any route whose loader the client tree-shake plugin doesn't rewrite to the literal stub — e.g. a re-export like export { pokemonListLoader as loader } from './loaders' — fails the regex and sends web ESM to native. The regex is irrelevant for native responses: the native branch never uses transformedJS at all — it imports the route through the module runner (which resolves re-exports fine) and returns the JSON-embedding CJS body.

Fix

  • include native in resolveLoaderRoute's isNativeRequest
  • hoist the platform check in handleLoader above the stub-regex gate so native requests always take the module-runner path; web requests keep the existing shortcut (including the skip-SSR-import optimization for routes without loader stubs)

Verification

  • 3 new unit tests in createHandleRequest.test.ts. The two platform=native ones fail before the fix (body arrives as export function loader(){...} web ESM) and pass after; the web test guards against regressions on the ESM path. Full file: 23/23 pass.
  • Downstream: we've been carrying the dist-level equivalent of this diff as a pnpm patch in multiplatform.one against one@1.16.5. With routes written as plain re-exports:
curl 'http://localhost:3000/assets/pokemon_<cachekey>_vxrn_loader.js?platform=native'
# before: import { createHotContext as __vite__createHotContext } from "/@vite/client";...  (Hermes parse error)
# after:  exports.loader = () => ({"pokemon":[{"name":"bulbasaur",...}]});

On device (Expo Go SDK 55 / Hermes): the [one] native loader error toast is gone and the route receives the server-prefetched loader data.

Made with Cursor

In dev, useLoader on native fetches route loaders with ?platform=native
and runs the response as CJS on hermes. Two gates only recognized
platform=ios|android, so platform=native requests could receive the
vite-transformed web ESM module instead - hermes fails to parse it, the
client swallows the error, and the route silently loses its loader data
(including redirect/auth signals):

- resolveLoaderRoute's isNativeRequest missed platform=native, emitting
  redirect (302) and auth (401/403) fallback loader bodies as web ESM
- handleLoader's "export function loader()" stub regex early-returned
  the web ESM module above the platform check, so any loader shape the
  client tree-shake plugin doesn't rewrite to the literal stub (e.g.
  export { x as loader } from './loaders') sent web ESM to native; the
  native branch never uses transformedJS at all - it imports the route
  through the module runner, which resolves re-exports fine

Web requests keep the existing shortcut (including the skip-SSR-import
optimization for routes without loader stubs).

Co-authored-by: Cursor <cursoragent@cursor.com>
@clayrisser clayrisser closed this Aug 14, 2026
@clayrisser
clayrisser deleted the fix/native-loader-platform-param branch August 14, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant