server-only
Mark a module as server-only with one import, so a client bundle that pulls it in is refused instead of shipping it to the browser.
lib/db.ts
import '@gio.js/core/server-only';
export const db = createClient(process.env.DATABASE_URL);Reference
@gio.js/core/server-only exports nothing; you import it for its effect. On the server it does nothing at all. The client build resolves it to a marker and, after tree-shaking each route's bundle, refuses any bundle in which the marker is still reachable.
Three ways to mark a module
import '@gio.js/core/server-only';at the top of the module.import 'server-only';- the bare specifier is recognized too. Prefer the GioJS one: the npmserver-onlypackage throws when it is loaded outside React Server Components, which includes GioJS's server render.- A file name ending in
.server.ts(.tsx,.js,.jsx,.mts,.cts,.mjs,.cjs), with no import at all. This applies to your own files, not to files inside dependencies.
Behavior
- A route whose client bundle still reaches a marked module after tree-shaking gets no bundle: the bundle is never written to disk, and the page server-renders without hydrating (no client JavaScript).
- The error names the import chain. It is logged at startup and, in development, shown in the error overlay when you open the page. Other routes are unaffected.
gio exportwrites such a route as HTML only and lists it.- Code that only
getServerSideProps,getStaticPathsor other server exports import is tree-shaken out of client bundles anyway; the marker turns a mistake - a component importing it - into an error instead of a leak.
Examples
A leak, caught
lib/stats.ts
import '@gio.js/core/server-only';
export function dbUrl(): string {
return process.env.DATABASE_URL ?? 'postgres://localhost/app';
}app/leaky/page.tsx
import { dbUrl } from '../../lib/stats.ts';
export default function Leaky() {
return <h1>{dbUrl().length}</h1>; // a component - this code goes to the browser
}At startup the worker logs an error (client bundle imports server-only code - route will render without hydration) whose message names the chain:
text
client bundle for route "/leaky" imports server-only code: app/leaky/page.tsx -> lib/stats.ts -> @gio.js/core/server-only. The page still server-renders but will NOT hydrate (no client JS) until this import is removed from client code - keep server-only modules behind getServerSideProps or route.ts.The fix
app/leaky/page.tsx
import type { GetServerSideProps, InferPageProps } from '@gio.js/core';
import { dbUrl } from '../../lib/stats.ts';
export const getServerSideProps = (async () => {
return { props: { length: dbUrl().length } }; // runs on the server only
}) satisfies GetServerSideProps;
export default function Leaky({ length }: InferPageProps<typeof getServerSideProps>) {
return <h1>{length}</h1>;
}Good to know
- Opt-in per module, and not switchable. GioJS cannot know which of your modules hold secrets, so nothing is marked for you; and a marked module is always refused in a client bundle - there is no setting to let it through.
- Tree-shaking keeps calls it cannot prove harmless. A server export built by a call at module scope (
export const getServerSideProps = withAuth(...)) is kept in the client bundle with everything it wraps. Call the wrapper inside a function declaration instead, and mark the helper module server-only so a leak fails loudly. - Environment variables other than
GIO_PUBLIC_*areundefinedin the browser anyway; the marker protects code (queries, keys in source, internal logic), not just values. @gio.js/core/testingimports the marker itself, so a page that imports the test kit is refused too.
Related
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced @gio.js/core/server-only, the bare server-only specifier and *.server.ts file names. |