Next.js
Markdown for agentsThis guide keeps server-only code out of the browser in a Next.js app, with the nextjs preset.
Before you start
Section titled “Before you start”- Read Getting started.
- You have a Next.js app that uses the App Router and
'use client'files.
What the preset checks
Section titled “What the preset checks”A file that starts with 'use client' runs in the browser. So does each file that it reaches
through runtime imports. The preset refuses a chain of imports from a client file to the package
server-only or to a Node built-in module, such as node:fs.
A chain stops at a file that starts with 'use server'. Next.js changes an import of a server
action into a network call, so the code of the action does not go to the browser.
The preset has one rule, nextjs/client-stays-client. It counts only runtime imports, because the
compiler removes an import type.
Add the preset
Section titled “Add the preset”The example app has a cart page, a client button, and a module that reads the cart from disk:
- archangel.config.ts
- tsconfig.json
Directoryapp/
Directorycart/
- actions.ts
- page.tsx
Directorycomponents/
- cart-button.tsx
Directorylib/
- cart.ts
- format.ts
-
Put the preset in
extends:archangel.config.ts import { defineConfig, nextjs } from '@enhance-eng/archangel';export default defineConfig({extends: [nextjs()],});nextjs()checks the whole repository. To check one app of a monorepo, give its folder:nextjs({ root: 'apps/web' }). -
Check the app:
Terminal $ npx archangel checkerror[may-not-import]: components/cart-button.tsx reaches server-only through components/cart-button.tsx → lib/format.ts → lib/cart.ts → server-only--> components/cart-button.tsx:3:29|3 | import { formatTotal } from '@/lib/format';| ^^^^^^^^^^^^^^ remove an import of the chain, or move what the file needs into code that does not reach it|= note: rule: nextjs/client-stays-client= note: reason: Code that runs in the browser cannot use server-only code or Node built-in modules.= note: see: https://nextjs.org/docs/app/getting-started/server-and-client-componentschecked 6 files and 9 imports: 1 problemFor what a code means, run `archangel docs may-not-import`.The button is a client component. It imports
lib/format.tsto format the total.format.tsimportslib/cart.tsfor one constant, andcart.tsimportsserver-only. So the button pulls server code into the browser. The error shows the full chain.
These are the files of the chain:
'use client';import { checkout } from '@/app/cart/actions';import { formatTotal } from '@/lib/format';
export function CartButton({ total }: { total: number }) { return <button onClick={() => checkout()}>Pay {formatTotal(total)}</button>;}import { currency } from './cart';
export function formatTotal(cents: number): string { return `${currency} ${(cents / 100).toFixed(2)}`;}import 'server-only';import { readFile } from 'node:fs/promises';
export const currency = 'USD';
export async function cartTotal(): Promise<number> { return Number(await readFile('cart.txt', 'utf8'));}
export async function saveCart(): Promise<void> {}The button also imports checkout from app/cart/actions.ts, which also imports lib/cart.ts.
The rule does not refuse that chain, because actions.ts starts with 'use server'.
Fix the chain
Section titled “Fix the chain”-
Move the constant into
format.ts, so thatformat.tsimports nothing from the server:lib/format.ts export const currency = 'USD';export function formatTotal(cents: number): string {return `${currency} ${(cents / 100).toFixed(2)}`;} -
Check again:
Terminal $ npx archangel checkchecked 6 files and 8 imports: no problems
Find a chain yourself
Section titled “Find a chain yourself”archangel why shows the shortest chain of imports from one file to a file or a package:
$ npx archangel why components/cart-button.tsx server-onlycomponents/cart-button.tsx:2 imports '@/app/cart/actions'app/cart/actions.ts:2 imports '@/lib/cart'lib/cart.ts:1 imports 'server-only'server-onlyProve the preset
Section titled “Prove the preset”$ npx archangel testpass nextjs/client-stays-client accepted #1 app/example-actions.ts, app/example-page.tsxpass nextjs/client-stays-client rejected #1 app/example-helper.ts, app/example-page.tsx2 examples, none failed; 0 rules without a rejected exampleThe preset brings its own examples: one client page that reaches node:fs through a helper, and
one that reaches it only through a 'use server' file.
Next steps
Section titled “Next steps”- Presets lists the options of
nextjs. - Conditions tells you how to write your own rule about
chains of imports, with
transitivelyandstopAt.