GioJSdocs
On this page

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 npm server-only package 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 export writes such a route as HTML only and lists it.
  • Code that only getServerSideProps, getStaticPaths or 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_* are undefined in the browser anyway; the marker protects code (queries, keys in source, internal logic), not just values.
  • @gio.js/core/testing imports the marker itself, so a page that imports the test kit is refused too.

Version history

VersionChanges
v0.1.0-beta.8Introduced @gio.js/core/server-only, the bare server-only specifier and *.server.ts file names.