Diagnostics
LUM4003: Server-only module in a client graph
Server-only code is reachable from the browser.
Severity: error · Category: islands and boundaries (4xxx)
Reported by: lumiose build and lumiose dev (@lumiose/vite), when the client graph reaches a
server-only module: the build fails, and dev fails the module's request (the error overlay), with
the chain of importers from the client entry. lumiose check and the language server report it at
the import site, for the graphs of island components and of modules that import lumiose/dom (the
editor follows unsaved edits of open files). At runtime, server-only APIs
(request(), cookies(), env(), renderToString(), …) throw it in the browser build.
#What it means
*.server.ts modules (any *.server.* file of the project), everything under src/server/, and
the request APIs only exist on the server. Importing them into code that reaches the browser
(islands, SPA routes, client entries, and everything they import, directly or not) would ship
server code and secrets in the client bundle, or fail at runtime. Server code (pages rendered on
the server, endpoints, actions) imports them freely; stylesheets of .server.stsx files are fine.
Every way into the client graph counts: imports (static or dynamic, also ?raw and ?url), web
workers, and new URL("./db.server.ts", import.meta.url), which would ship the file as an asset.
Files are matched by their real path (a symlink to a server-only file is server-only) and
case-insensitively; a sibling workspace folder outside the Vite root follows the same rule from its
own package.json folder. Dependencies in node_modules are not checked.
LUM4003: src/server/db.server.ts is server-only, but client code imports it:
src/components/Likes.stsx
→ src/lib/likes.ts
→ src/server/db.server.ts#Example
import { db } from "../server/db.server.ts";
export function Likes() {
return <button onClick={() => db.like()}>Like</button>;
}#How to fix it
Keep the server code on the server and call it over HTTP: expose an endpoint (or a form action) and
call it with fetchJSON or submit() from the island. In client code, read public config from
import.meta.env.PUBLIC_* instead of env().
For the current URL, params and route in client code, import url(), params() and route() from
lumiose/router instead of lumiose. Those of lumiose read the request, so they are server-only
and throw this error in the browser; those of lumiose/router are reactive, follow client
navigations, and also work during server rendering.
Code that runs on both sides, such as a defineState() factory, may read request APIs only on the
server: guard them with isServer (signal(isServer ? locals().user : null)). The browser
receives the server's values. isServer is a constant, so client builds drop the guarded branch.