A tour of the Next.js repository
Find your way around vercel/next.js at v16.4.3: packages/next/src, the Rust crates, the vendored React, building Next.js, and following dist to src.
- 01Find where a feature livesName the folder in vercel/next.js that holds the CLI, the server, the build, the browser code and the Rust compilers.
- 02Follow dist back to srcRead a stack trace from node_modules/next/dist, find each frame's file and line in packages/next/src, and tell a compiled file from a bundled runtime.
- 03Read the source while it runsSay which React the App Router runs on, build Next.js yourself from its docs, and stop in Next.js's own code with a debugger.
Who called the tea shop's route?
Every page of the tea shop is answered by code you didn't write: Next.js's own. That code is open source, in one GitHub repository, vercel/next.js (the next package is in packages/next), and every link to its files here points at the tag v16.4.3, the release every example here was built with. A tag is a name git gives one exact version of the files, so the line numbers never move.
The quickest way into a large codebase is to ask it a question while it runs. Here the tea shop has a Route Handler, /where, that answers with the list of Next.js functions that called it. It was built with next build, run with next start, and its answer was recorded.
app/where/route.tsimport { readFileSync } from "node:fs";import { SourceMap } from "node:module";import { connection } from "next/server"; // One line of a stack trace: "at name (/…/node_modules/next/dist/file.js:row:column)".const FRAME = /at (?:async )?(?:(\S+) \()?(\S+\/node_modules\/next\/(dist\/\S+?\.js)):(\d+):(\d+)/; function fromSource(line: string) { const frame = FRAME.exec(line); if (!frame) return []; const [, name = "(anonymous)", file, distFile, row, column] = frame; const map = new SourceMap(JSON.parse(readFileSync(`${file}.map`, "utf8"))); const entry = map.findEntry(Number(row) - 1, Number(column) - 1); if (!("originalSource" in entry)) return []; const source = entry.originalSource.replace(/^.*?src\//, "packages/next/src/"); return [`${distFile}:${row} -> ${source}:${entry.originalLine + 1} ${name}`];} export async function GET() { Error.stackTraceLimit = 40; const stack = new Error().stack ?? ""; await connection(); return new Response(stack.split("\n").flatMap(fromSource).join("\n"));}GET /where200
- Response headers
varyrsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetchcontent-typetext/plain;charset=UTF-8
The body that arrived, as it arrived
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:825 r0.renderToResponse
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:986 (anonymous)
dist/server/lib/trace/tracer.js:196 -> packages/next/src/server/lib/trace/tracer.ts:404 NextTracerImpl.trace
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:977 (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:951 (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:950 (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:947 r0.executeRequest
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:1098 r0.handle
dist/compiled/next-server/app-route-turbo.runtime.prod.js:1 -> packages/next/src/server/route-modules/route-module.ts:1339 r0.handleResponse
dist/server/base-server.js:1765 -> packages/next/src/server/base-server.ts:2985 NextNodeServer.renderToResponseWithComponentsImpl
dist/server/base-server.js:1824 -> packages/next/src/server/base-server.ts:3062 NextNodeServer.renderPageComponent
dist/server/base-server.js:1867 -> packages/next/src/server/base-server.ts:3142 NextNodeServer.renderToResponseImpl
dist/server/base-server.js:1286 -> packages/next/src/server/base-server.ts:2209 NextNodeServer.pipeImpl
dist/server/next-server.js:340 -> packages/next/src/server/next-server.ts:1271 NextNodeServer.handleCatchallRenderRequest
dist/server/base-server.js:1008 -> packages/next/src/server/base-server.ts:1703 NextNodeServer.handleRequestImpl
dist/server/lib/router-server.js:363 -> packages/next/src/server/lib/router-server.ts:535 invokeRender
dist/server/lib/router-server.js:567 -> packages/next/src/server/lib/router-server.ts:813 handleRequest
dist/server/lib/router-server.js:636 -> packages/next/src/server/lib/router-server.ts:923 requestHandlerImpl
dist/server/lib/start-server.js:210 -> packages/next/src/server/lib/start-server.ts:254 Server.requestListenerStart at the bottom of app/where/route.ts. Line 21 makes an Error only to read its stack: the list of functions that were running when it was made, one per line, the newest first (The stack trace API explains the format). Line 20 raises how many lines it keeps from V8's default of 10 to 40. Line 22 waits for connection(), which tells Next.js to run this handler for each request instead of once during the build. Line 23 answers with the stack, each line rewritten by fromSource.
fromSource, on lines 8 to 17, keeps only the lines in node_modules/next/dist, the folder npm installs. Next.js ships a source map beside each of the server files this stack names: a .map file that says which line of the original source each compiled line came from. (Not every file in dist has one: much of the third-party code in dist/compiled doesn't.) Line 12 reads the map, and line 13 asks it about one line and column (maps count from 0, so both are 1 less). Line 15 shortens the answer to a path in the repository.
Open the Network tab. Each line reads file in dist → file and line in the repository, then the function's name. Read it from the bottom up, in the order the calls happened:
start-server.ts:254: Node's HTTP server got the request and called Next.js'srequestListener.router-server.ts, three lines: the router server decided what answers this address.next-server.tsandbase-server.ts: the server class that renders a route.route-modules/app-route/module.ts: the code that runs a Route Handler. Line 825 calls yourGET.
Every one of those files is in one folder of the repository, packages/next/src/server, and From next dev to a response reads them in order. First, the map: where each part of Next.js lives, how the files npm installs come from it, and how to read it while it runs.
packages/next/src is compiled into the next package's dist folder. The Rust crates, with SWC and Turbopack, are compiled into one native file, published as @next/swc-<platform>.A guest sees the reception and the restaurant. The boiler room in the basement keeps everything warm, and few guests ever go there. The floor plan by the lift says which floor holds what. And the bill at the end lists every desk that served the guest, in order, so you can walk back to each one.
- In real life: The hotel
- In Next.js: The repository vercel/next.js
- In real life: The rooms guests see: reception, the restaurant
- In Next.js: packages/next/src: the code npm installs as next
- In real life: The boiler room in the basement
- In Next.js: The Rust crates: SWC and Turbopack
- In real life: The floor plan by the lift
- In Next.js: A map of the repository: which folder holds what
- In real life: A guest's bill, which lists every desk that served them
- In Next.js: The stack trace from /where
Next.js is one repository with two halves. The TypeScript in packages/next/src becomes the next package you install, and the .map files beside its compiled server code lead from a line in node_modules/next/dist back to the source. The Rust crates become one native file for your computer, with the compilers inside.
packages/next/src, folder by folder
The repository holds about twenty packages in packages, like create-next-app, eslint-config-next and next-codemod. The one you install as next is packages/next, and its source is in packages/next/src. Each row is one of its folders:
| Folder | What it holds | One file to start with |
|---|---|---|
bin | The next command itself: bin/next.ts reads the command line and loads one command. | bin/next.ts: the list of commands. |
cli | One file per command: next-dev.ts, next-build.ts, next-start.ts and the others. | cli/next-dev.ts: starts the server process and passes it --inspect. |
server | Everything that answers requests: starting the HTTP server, routing, rendering, caching. | server/lib/start-server.ts: the HTTP server, at the bottom of every stack above. |
build | next build: collecting routes, compiling, prerendering, writing .next, and loading the Rust compiler. | build/swc/index.ts: loads @next/swc. |
client | The code that runs in the browser: hydration, the App Router's router, Link. | client/app-index.tsx: hydrates the first HTML. |
shared | Code both sides use, like the helpers that turn a path into a route. | shared/lib/router/utils/app-paths.ts: turns a page's path into its route. |
lib | Helpers the CLI, the build and the server share, like the file names proxy and middleware in lib/constants.ts. | lib/needs-experimental-react.ts: decides which React. |
compiled | Other projects' code, copied in or bundled so Next.js controls the exact version: React, commander, webpack and more. | compiled/react: the React the App Router runs. |
A command starts in bin. The next package's package.json says, in lines 81 to 83, that the command next is the file ./dist/bin/next. That file is compiled from src/bin/next.ts, which lists every command and loads its file from cli only when you run it: next dev loads cli/next-dev.ts on lines 481 and 482, and next start loads cli/next-start.ts on lines 560 and 561.
An import from next/… in your code takes the same kind of path. The where route imports connection from next/server, and this is how that name reaches the source:
import { connection } from "next/server"
node_modules/next/server.js, line 15
node_modules/next/dist/server/request/connection.js
packages/next/src/server/request/connection.tsLine 1 is the import. Line 2 is the small file at the top of the package, server.js, whose line 15 loads connection from dist. Line 3 is that compiled file, and line 4 the TypeScript it came from, connection.ts. So for any next/… import, open the matching file at the top of packages/next, and it names the folder to read.
From src to node_modules/next/dist
The stack had two kinds of files in dist. Files like dist/server/base-server.js match one source file each. But eight of its lines were in dist/compiled/next-server/app-route-turbo.runtime.prod.js, and they led back to two different files. The build of Next.js itself makes both kinds.
One file in, one file out
Most of src is compiled one file at a time by SWC, the Rust compiler Next.js also uses for your code. The repository's build runs tasks written in taskfile.js; this is the one for src/server:
export async function server(task, opts) {
await task
.source('src/server/**/!(*.test).+(js|ts|tsx)')
.swc('server', { dev: opts.dev })
.target('dist/server')
}Line 3 picks every .js, .ts and .tsx file under src/server, except tests. Line 4 compiles each one with SWC, and line 5 writes the result into dist/server, in the same place. So src/server/base-server.ts becomes dist/server/base-server.js. The line numbers change, because the types are removed and the imports rewritten: line 1703 of the source is line 1008 of the result. That's why the .map file matters. In lines 126 to 130 of taskfile-swc.js, SWC is asked for a source map that also holds the source's full text (inlineSourcesContent), so a debugger can show the TypeScript even though npm never installs it. The code is in lines 2513 to 2518 of taskfile.js.
The route runtimes: many files in, one file out
The code that runs your routes is different. It's bundled: many source files, React included, are joined into one file per kind of route, by webpack, with the settings in next-runtime.webpack-config.js. This is how those files are named:
output: {
path: path.join(__dirname, 'dist/compiled/next-server'),
filename: `[name]${turbo ? '-turbo' : ''}${
experimental ? '-experimental' : ''
}.runtime.${dev ? 'dev' : 'prod'}.js`,Line 2 puts them in dist/compiled/next-server. Lines 3 to 5 build each name from parts: the kind of bundle (like app-page, app-route or pages), -turbo for apps built with Turbopack, -experimental for experimental React, then dev or prod. That's app-route-turbo.runtime.prod.js: a Route Handler, a Turbopack app, stable React, production. When a route loads, a small file picks one of them:
if (process.env.NODE_ENV === 'development') {
if (process.env.TURBOPACK) {
module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.dev.js')
} else {
module.exports = require('next/dist/compiled/next-server/app-route.runtime.dev.js')
}
} else {
if (process.env.TURBOPACK) {
module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.prod.js')
} else {
module.exports = require('next/dist/compiled/next-server/app-route.runtime.prod.js')
}
}Line 1 asks whether this is development. Lines 2 and 8 ask whether Turbopack built the app. So under next start, line 9 loads the prod bundle, and under next dev, line 3 loads the dev one. (The lines around this excerpt pick the -experimental bundles instead when experimental React is on.) The file is module.compiled.js, and the names come from lines 213 to 217 of next-runtime.webpack-config.js. Try both below: the same route, served by next start and by next dev.
app/where/route.tsimport { readFileSync } from "node:fs";import { SourceMap } from "node:module";import { connection } from "next/server"; // One line of a stack trace: "at name (/…/node_modules/next/dist/file.js:row:column)".const FRAME = /at (?:async )?(?:(\S+) \()?(\S+\/node_modules\/next\/(dist\/\S+?\.js)):(\d+):(\d+)/; function fromSource(line: string) { const frame = FRAME.exec(line); if (!frame) return []; const [, name = "(anonymous)", file, distFile, row, column] = frame; const map = new SourceMap(JSON.parse(readFileSync(`${file}.map`, "utf8"))); const entry = map.findEntry(Number(row) - 1, Number(column) - 1); if (!("originalSource" in entry)) return []; const source = entry.originalSource.replace(/^.*?src\//, "packages/next/src/"); return [`${distFile}:${row} -> ${source}:${entry.originalLine + 1} ${name}`];} export async function GET() { Error.stackTraceLimit = 40; const stack = new Error().stack ?? ""; await connection(); return new Response(stack.split("\n").flatMap(fromSource).join("\n"));}GET /where200
- Response headers
varyrsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetchcontent-typetext/plain;charset=UTF-8
The body that arrived, as it arrived
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:825 r0.renderToResponse
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:986 (anonymous)
dist/server/lib/trace/tracer.js:196 -> packages/next/src/server/lib/trace/tracer.ts:404 NextTracerImpl.trace
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:977 (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:951 (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:950 (anonymous)
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:947 r0.executeRequest
dist/compiled/next-server/app-route-turbo.runtime.prod.js:5 -> packages/next/src/server/route-modules/app-route/module.ts:1098 r0.handle
dist/compiled/next-server/app-route-turbo.runtime.prod.js:1 -> packages/next/src/server/route-modules/route-module.ts:1339 r0.handleResponse
dist/server/base-server.js:1765 -> packages/next/src/server/base-server.ts:2985 NextNodeServer.renderToResponseWithComponentsImpl
dist/server/base-server.js:1824 -> packages/next/src/server/base-server.ts:3062 NextNodeServer.renderPageComponent
dist/server/base-server.js:1867 -> packages/next/src/server/base-server.ts:3142 NextNodeServer.renderToResponseImpl
dist/server/base-server.js:1286 -> packages/next/src/server/base-server.ts:2209 NextNodeServer.pipeImpl
dist/server/next-server.js:340 -> packages/next/src/server/next-server.ts:1271 NextNodeServer.handleCatchallRenderRequest
dist/server/base-server.js:1008 -> packages/next/src/server/base-server.ts:1703 NextNodeServer.handleRequestImpl
dist/server/lib/router-server.js:363 -> packages/next/src/server/lib/router-server.ts:535 invokeRender
dist/server/lib/router-server.js:567 -> packages/next/src/server/lib/router-server.ts:813 handleRequest
dist/server/lib/router-server.js:636 -> packages/next/src/server/lib/router-server.ts:923 requestHandlerImpl
dist/server/lib/start-server.js:210 -> packages/next/src/server/lib/start-server.ts:254 Server.requestListenerUnder next start, the Route Handler's code comes from app-route-turbo.runtime.prod.js, the bundle for production. Its names are shortened (r0.handle), but the source map still finds module.ts and the line.
Under next start, the class is called r0: the production bundle is minified, its names shortened to make it smaller. Under next dev, it's AppRouteRouteModule, and the file ends in .dev.js. The source map gives the same line of module.ts for both, line 1098. The dev version also has three more lines, from next-dev-server.ts and trace.ts: in development, a DevServer class wraps each request first.
src is compiled one file at a time, with a .map file beside each result. The code that runs a route is bundled: app-route/module.ts, the vendored React and more become one file per kind of route, like app-route-turbo.runtime.prod.js.One more folder: dist/compiled also holds other projects' code, like commander for the command line, webpack, and React. Next.js keeps its own copy of each, so the version it was tested with is the one that runs. The file names in dist are how this release is built, not a public API; they can change in any release.
The Rust crates: SWC and Turbopack
The other half of the repository is Rust. A Rust package is called a crate, and a workspace is a set of crates built together. The root Cargo.toml, Rust's version of package.json, lists them:
members = [
"scripts/send-trace-to-jaeger",
"crates/next-napi-bindings",
"crates/wasm",
"crates/next-api",
"crates/next-build-test",
"crates/next-build",
"crates/next-code-frame",
"crates/next-core",
"crates/next-custom-transforms",
"crates/next-taskless",
"turbopack/crates/*",
"turbopack/crates/*/fuzz",
"turbopack/xtask",
]Lines 3 to 11 are Next.js's own crates, in crates. Line 12 adds every crate in turbopack/crates: Turbopack, the bundler that builds your app by default since Next.js 16. SWC itself isn't in the list. It's a separate open-source project, and the crates use it as a dependency, swc_core, in line 217 of Cargo.toml. Each row is one crate this course meets:
| Crate | What it does |
|---|---|
crates/next-napi-bindings | Joins Node.js and Rust: it's built into the .node file that build/swc/index.ts loads. |
crates/next-core | Teaches Turbopack about Next.js: the app folder, route entries, and which React each part gets. |
crates/next-api | The project as a whole: its routes, entrypoints and outputs, for next dev and next build. |
crates/next-custom-transforms | Next.js's own SWC transforms, like the ones for "use client" and Server Actions. |
turbopack/crates/turbo-tasks | The engine that remembers every result Turbopack computed, so a change only redoes what depends on it. |
turbopack/crates/turbopack-* | The bundler: one crate each for JavaScript, CSS, images, resolving modules and more. |
swc_core, from crates.io | SWC itself, the Rust compiler for JavaScript and TypeScript. It's a separate project; the repository uses version 79.0.0. |
None of this Rust code is compiled on your computer. Line 3's next-napi-bindings is built ahead of time into one .node file per operating system and processor, a native add-on that Node.js can load, and each one is published as its own npm package. The next package lists all of them as optional dependencies, and npm installs only the one that fits. This recording lists what npm install put in the tea shop's node_modules, on the Mac with an Apple chip that made the recording:
app/page.tsxexport default function Home() { return <h1>Tea shop</h1>;}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (3/3) Finalizing page optimization ... Route (app)┌ ○ /└ ○ /_not-found ○ (Static) prerendered as static content # What node_modules holds: 10658 files, 351.5 MB (deeper folders added up)node_modules/.package-lock.json 15.8 kBnode_modules/@emnapi/runtime/ 436.9 kB, 17 filesnode_modules/@img/colour/ 73.3 kB, 6 filesnode_modules/@img/sharp-darwin-arm64/ 292.2 kB, 5 filesnode_modules/@img/sharp-libvips-darwin-arm64/ 18.2 MB, 6 filesnode_modules/@img/sharp-wasm32/ 9.3 MB, 7 filesnode_modules/@next/env/ 11.9 kB, 4 filesnode_modules/@next/swc-darwin-arm64/ 90.5 MB, 3 filesnode_modules/@swc/helpers/ 282.3 kB, 438 filesnode_modules/@types/node/ 2.3 MB, 69 filesnode_modules/@types/react-dom/ 32.9 kB, 17 filesnode_modules/@types/react/ 408.1 kB, 16 filesnode_modules/baseline-browser-mapping/ 136.7 kB, 9 filesnode_modules/caniuse-lite/ 1.5 MB, 841 filesnode_modules/client-only/ 611 bytes, 3 filesnode_modules/csstype/ 1.3 MB, 5 filesnode_modules/detect-libc/ 26.4 kB, 8 filesnode_modules/nanoid/ 26.8 kB, 26 filesnode_modules/next/ 192.1 MB, 8705 filesnode_modules/picocolors/ 6.4 kB, 7 filesnode_modules/postcss/ 216.0 kB, 55 filesnode_modules/react-dom/ 8.1 MB, 43 filesnode_modules/react/ 178.7 kB, 27 filesnode_modules/scheduler/ 82.7 kB, 15 filesnode_modules/semver/ 101.1 kB, 53 filesnode_modules/sharp/ 962.2 kB, 47 filesnode_modules/source-map-js/ 142.7 kB, 18 filesnode_modules/styled-jsx/ 1.0 MB, 20 filesnode_modules/tslib/ 90.4 kB, 14 filesnode_modules/typescript/ 23.6 MB, 132 filesnode_modules/undici-types/ 83.7 kB, 41 filesIn the Terminal tab, after the build, @next/swc-darwin-arm64 is about 90 MB in 3 files: one of them is next-swc.darwin-arm64.node, with SWC and Turbopack inside. next is about 192 MB in thousands of files, almost all of it dist. On Linux or Windows, npm picks another @next/swc-… package. The sizes are this machine's and this version's. When next build starts, loadNative in build/swc/index.ts loads the package for your platform, and loadBindings, in lines 190 to 265 of the same file, falls back to a WebAssembly build of the same crates when there isn't one.
The Rust crates get a lesson of their own, SWC and Turbopack, and the transforms for "use client" and Server Actions in next-custom-transforms get 'use client' and 'use server' under the hood.
Where React comes from
A new Next.js project's package.json lists react and react-dom, version 19.3.0. You might expect every page to run on them. Here is the tea shop with a page that prints version, the React version string React exports, from a Server Component, from a Client Component, and from an old Pages Router page in the same project:
app/page.tsximport { version } from "react";import { ClientVersion } from "./client-version"; export default function Home() { return ( <main> <h1>Tea shop</h1> <p>Server Component React: {version}</p> <ClientVersion /> </main> );}Server Component React: 19.3.0-canary-278794d7-20261002
Client Component React: 19.3.0-canary-278794d7-20261002
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/page.tsx, line 1 imports version from react, and line 8 prints it. The Browser tab shows 19.3.0-canary-278794d7-20261002 for the Server Component and the same for ClientVersion, the Client Component in app/client-version.tsx. Pick /legacy above the page: pages/legacy.tsx printed 19.3.0. The recording adds one more fact: the JavaScript files the home page loads contain the text 19.3.0-canary-278794d7-20261002, so the browser runs that build too.
A canary is a build of React published from its newest code, before a stable release (React's blog post React Canaries explains the channel). Next.js's docs say it in line 44 of docs/index.mdx: the App Router uses React canary releases built in, with all of stable React 19 and newer features that frameworks are trying out. The copy lives in next/dist/compiled/react, and the repository pins it in its root package.json:
"react-builtin": "npm:react@19.3.0-canary-278794d7-20261002",
"react-dom-builtin": "npm:react-dom@19.3.0-canary-278794d7-20261002",
"react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-278794d7-20261002",
"react-experimental-builtin": "npm:react@0.0.0-experimental-278794d7-20261002",Line 1 names the canary as react-builtin, and line 2 the matching react-dom. Lines 3 and 4 name an experimental build, with features React hasn't settled yet (they're lines 262, 264, 265 and 266 of the file). The build task copy_vendor_react, declared at line 1490 of taskfile.js, copies these packages into src/compiled/react and its neighbors, which are kept in git; lines 1561 to 1569 copy React's files there. The script scripts/sync-react.js moves the pin to a newer canary.
Then the bundler sends App Router code's react imports to that copy. For Turbopack, that happens in Rust, in crates/next-core. First it picks the channel:
let react_channel = if *next_config.use_react_experimental().await? {
"-experimental"
} else {
""
};Lines 1 to 5 pick the folder's ending: -experimental when the app needs experimental React, and nothing otherwise, so the stable copy in next/dist/compiled/react is the one used. Then, in lines 905 to 970, the aliases depend on where the code runs. Server code on Node.js goes through entry files of Next.js, like next/dist/server/route-modules/app-page/vendored/rsc/react for Server Components and …/vendored/ssr/react for rendering Client Components to HTML, which hand out that copy; other code, like the Edge runtime's, imports next/dist/compiled/react itself. Either way, it's the canary the page printed. Which settings turn on experimental React is in TypeScript, and the Rust side says it must stay in step with it:
export function needsExperimentalReact(config: NextConfig) {
const { blockingSSR, taint, transitionIndicator, gestureTransition } =
config.experimental || {}
return Boolean(
blockingSSR || taint || transitionIndicator || gestureTransition
)
}Lines 2 and 3 read four experimental settings from next.config.ts, and lines 4 to 6 answer true if any is on. The tea shop sets none of them, so it got the canary. The Pages Router is different again:
match ty {
ServerContextType::Pages { .. } | ServerContextType::PagesApi { .. } => {
import_map.insert_exact_alias(rcstr!("react"), external);
import_map.insert_wildcard_alias(rcstr!("react/"), external);
import_map.insert_exact_alias(rcstr!("react-dom"), external);
import_map.insert_exact_alias(rcstr!("react-dom/client"), external);
import_map.insert_wildcard_alias(rcstr!("react-dom/"), external);Line 2 matches server code for pages, and lines 3 to 7 mark react and react-dom as external: not bundled, but loaded from the app's own node_modules when it runs. That's the 19.3.0 that /legacy printed, and why the Pages Router still works with the React version in your package.json (Migrating to the App Router runs both routers side by side).
import { version } from "react", two answers. In the App Router, Server and Client Components both get the React that Next.js carries in next/dist/compiled/react. A Pages Router page gets the app's own react package.React's own repository has a tour like this one: A tour of the React repository. The react-server-dom-turbopack and react-server-dom-webpack folders next to compiled/react hold React's code for Server Components, which 'use client' and 'use server' under the hood reads.
Running a local build of Next.js
To change Next.js, or to put a console.log in its source, you build it yourself. These commands are from the repository's own guides, contributing/core/developing.md and building.md. They aren't recorded: a full build takes many minutes and gigabytes of disk.
gh repo clone vercel/next.js -- --filter=blob:none --single-branch
cd next.js
corepack enable pnpm
pnpm install
pnpm build
pnpm dev
pnpm swc-build-native
pnpm next ./dev-app
pnpm pack-next --tar && pnpm unpack-next path/to/projectEach row is one of those commands, in order:
| Command | What it does |
|---|---|
gh repo clone vercel/next.js -- --filter=blob:none --single-branch | Clones the repository without downloading every old file version until you need it. |
corepack enable pnpm, then pnpm install | Uses the pnpm version the root package.json names (pnpm@10.33.0) and installs every workspace package. |
pnpm build | Builds every package once: SWC compiles packages/next/src into dist, webpack bundles the route runtimes, and tsc writes the types. |
pnpm dev | Watches packages/next/src and compiles again on every save. |
pnpm swc-build-native | Builds the Rust code into a new .node file. Only needed when you change Rust code. |
pnpm next ./dev-app | Runs your local Next.js on a folder inside the repository, with Node's source maps turned on. |
pnpm pack-next --tar && pnpm unpack-next path/to/project | Packs your local Next.js into tarballs and installs them into another project. |
The repository uses pnpm, a package manager like npm. Its root package.json names pnpm@10.33.0 and Node.js 20.9 or later, and .node-version says v20. Line 5, pnpm build, runs the tasks from the last section: SWC for dist, webpack for the runtime bundles. Line 7 is only for Rust changes and needs Rust installed; without it, the build uses the published @next/swc binary. Line 8 runs next from your build, on a folder inside the repository (developing-using-local-app.md explains it): the root script next sets NODE_OPTIONS="--trace-deprecation --enable-source-maps", so Node.js turns stack traces into source lines for you. The guide says linking the package into another project isn't enough, because Turbopack doesn't follow links outside a project's folder; line 9 packs it into files instead.
You can read and change Next.js without building it. In any project, edit a file in node_modules/next/dist, add a console.log, and run next dev again. It's gone the next time npm installs the package, which is what you want for an experiment. Use the .map file to find the line, as /where did.
Reading the source with a debugger
A stack trace shows one moment. A debugger lets you stop there and look at every variable. The server's code runs in Node.js, so you attach to Node's inspector, as Debugging showed for your own code. This is the tea shop under next dev --inspect:
app/where/route.tsimport { readFileSync } from "node:fs";import { SourceMap } from "node:module";import { connection } from "next/server"; // One line of a stack trace: "at name (/…/node_modules/next/dist/file.js:row:column)".const FRAME = /at (?:async )?(?:(\S+) \()?(\S+\/node_modules\/next\/(dist\/\S+?\.js)):(\d+):(\d+)/; function fromSource(line: string) { const frame = FRAME.exec(line); if (!frame) return []; const [, name = "(anonymous)", file, distFile, row, column] = frame; const map = new SourceMap(JSON.parse(readFileSync(`${file}.map`, "utf8"))); const entry = map.findEntry(Number(row) - 1, Number(column) - 1); if (!("originalSource" in entry)) return []; const source = entry.originalSource.replace(/^.*?src\//, "packages/next/src/"); return [`${distFile}:${row} -> ${source}:${entry.originalLine + 1} ${name}`];} export async function GET() { Error.stackTraceLimit = 40; const stack = new Error().stack ?? ""; await connection(); return new Response(stack.split("\n").flatMap(fromSource).join("\n"));}$ next dev --inspectDebugger listening on ws://127.0.0.1:9229/<id>For help, see: https://nodejs.org/learn/getting-started/debugging▲ Next.js 16.4.3 (Turbopack)- Local: http://127.0.0.1:3000- Network: http://127.0.0.1:3000- Debugger port: 9229✓ Ready- Cache Components enabled- Partial Prefetching enabledThe Terminal tab starts with Debugger listening on ws://127.0.0.1:9229/<id> (the recorder used a free port and shows it as 9229). That line comes from the process that runs the server. next dev starts that process as a second Node.js process, and passes the flag on to it; From next dev to a response shows the two processes. This is the code that decides which inspector each one gets:
const nodeDebugType = getNodeDebugType(nodeOptions)
const originalAddress =
nodeDebugType === undefined ? undefined : nodeOptions[nodeDebugType]
delete nodeOptions.inspect
delete nodeOptions['inspect-brk']
delete nodeOptions['inspect_brk']
if (nodeDebugType !== undefined) {
const address = getParsedDebugAddress(originalAddress)
address.port = address.port === 0 ? 0 : address.port + 1
nodeOptions[nodeDebugType] = formatDebugAddress(address)
} else if (options.inspect) {
const address: DebugAddress =
options.inspect === true
? getParsedDebugAddress(true)
: options.inspect
nodeOptions.inspect = formatDebugAddress(address)
}Line 1 looks for --inspect or --inspect-brk in the NODE_OPTIONS environment variable. Lines 4 to 6 remove them from what the server process gets. If they were there, lines 8 to 10 give the server the same option on the next port: the next dev process itself already took the first one, since NODE_OPTIONS reached it too. If you used the flag --inspect instead, lines 11 to 16 pass it on, and only the server listens. So:
next dev --inspect: one inspector, the server's, on 9229.NODE_OPTIONS=--inspect next dev:next dev's own process on 9229, the server on 9230. Attach to 9230 to stop in route code. Next.js's debugging guide uses this form for--inspect-brk, which waits for a debugger before running any code.
This comes from reading lines 486 to 502 of next-dev.ts; the recording shows only the first case. Once attached (in Chrome, open chrome://inspect and click inspect), DevTools shows the server files in node_modules/next/dist, like router-server.js, as their TypeScript, because their .map files hold the source text. With the ignore list changed (below), a breakpoint on line 923 of router-server.ts stops for every request.
Two things stop people the first time:
- The right file. A Route Handler or a page doesn't run from
dist/server/route-modules/app-route/module.js; that file is never loaded on Node.js. It runs from the bundlemodule.compiled.jspicked, likeapp-route-turbo.runtime.dev.js. Put the breakpoint there, through its source map. - The ignore list. DevTools skips Next.js's server code when you step, and hides its frames in the call stack. Their source maps ask it to:
// ignore-list everything
sourceMapPayload.ignoreList = sourceMapPayload.sources.map(
(source, sourceIndex) => {
return sourceIndex
}
)For each file the SWC task compiles, lines 2 to 6 put every source of its map in the map's ignoreList, the list of sources a debugger should treat as library code. The runtime bundles in the /where stack have maps that list their sources there too. So, by default, stepping goes from your code to your code, which is what you want most days. To step into Next.js, change DevTools' ignore list setting (in its settings, under Ignore list), or show the ignore-listed frames in the call stack. The code is in lines 170 to 175 of taskfile-swc.js.
A question, and the folder to open
Most visits to the repository start with a question about your own app: why does this redirect win, why is this page ◐, why did the build stop. The first step is always the same: pick the folder. Sort these questions by the folder you'd open first.
- What
next devdoes with--inspectbefore the server starts - The list of commands that
nextaccepts - Whether a redirect or a rewrite wins for the same address
- How
next startopens its HTTP server - How the build picks ○ or ◐ for each route
- How Next.js finds the Rust compiler for your computer
- What
<Link>prefetches when it comes on screen - The code that hydrates the first HTML in the browser
- The compile step that turns
"use client"into a client reference - How Turbopack remembers what it already compiled
Sort each question by the part of the repository you'd open first.
Then search inside it. GitHub's search at the tag works, and so does searching node_modules/next/dist for an error message the app printed: most messages are written out in the source, so the file that throws it is one search away. When the code you find is internal, like a file name in dist or a manifest's shape, read it to understand your app, but never make your app depend on it: it can change in any release.
Common mix-ups
- "node_modules/next is the source." No. It's
dist: compiled JavaScript. The server files, likebase-server.js, have a.mapbeside them that holds the TypeScript and leads back to it. - "Every file in src becomes one file in dist." Most do. The route runtimes don't: many files, React included, are bundled into each
…runtime.prod.jsand…runtime.dev.js. - "The App Router uses the React in my package.json." No. In this version it runs
19.3.0-canary-278794d7-20261002fromnext/dist/compiled/react. The Pages Router uses yours. - "SWC lives in the Next.js repository." No. SWC is its own project. The repository has Next.js's transforms for it, in
crates/next-custom-transforms, and Turbopack, inturbopack/crates. - "npm compiles the Rust when it installs Next.js." No. It downloads a
.nodefile that was compiled for your platform before the release. - "The debugger can't see Next.js's code." It can. The source maps of Next.js's server files ask it to skip that code, through
ignoreList. Change the setting, and you can step into it.
Each row is one of those beliefs, and what the recordings showed instead:
| What you might expect | What happened |
|---|---|
node_modules/next holds the TypeScript source. | It holds dist: compiled JavaScript, .map files that carry the source text, and the bundled runtimes. |
A breakpoint in dist/server/route-modules/app-route/module.js stops a Route Handler. | That file isn't loaded on Node.js. The handler runs from dist/compiled/next-server/app-route-turbo.runtime.prod.js (or .dev.js). |
Your package.json's React renders App Router pages. | The App Router runs the canary React inside next/dist/compiled/react. Your react package serves the Pages Router. |
Practice exercises
These exercises are about the repository and recorded projects, which can't run inside this page. Answer first, then use the hints.
You type next dev. Which file of packages/next/src runs first?
packages/next/src/bin/next.ts. The bin field points the command at dist/bin/next, which SWC compiled from src/bin/next.ts (the bin task strips the extension). That file reads the command line and loads cli/next-dev.ts for next dev.
In the tea shop with both routers, which React version does pages/legacy.tsx run on?
19.3.0. pages/legacy.tsx printed Pages Router React: 19.3.0: the Pages Router's server code leaves react external, so it comes from the app's node_modules/react, the version in its package.json.
Which file and line of the repository does this line of the stack come from?
dist/server/lib/router-server.js:567 -> ?packages/next/src/server/lib/router-server.ts:813. The map beside router-server.js says its line 567 is line 813 of the source, where handleRequest calls invokeRender to render the matched route.
The tea shop runs under next dev with Turbopack. Which file does its Route Handler's runtime come from?
if (process.env.NODE_ENV === 'development') {
if (process.env.TURBOPACK) {
module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.dev.js')
} else {
module.exports = require('next/dist/compiled/next-server/app-route.runtime.dev.js')
}
} else {
if (process.env.TURBOPACK) {
module.exports = require('next/dist/compiled/next-server/app-route-turbo.runtime.prod.js')
} else {
module.exports = require('next/dist/compiled/next-server/app-route.runtime.prod.js')
}
}app-route-turbo.runtime.dev.js. Line 1 is true in development, line 2 is true with Turbopack, so line 3 loads it. The recording of next dev shows it in the first lines of /where's answer.
A teammate puts a breakpoint in node_modules/next/dist/server/route-modules/app-route/module.js, runs the tea shop with next start under the inspector, and asks for /where. It never stops. Which file should the breakpoint go in?
In dist/compiled/next-server/app-route-turbo.runtime.prod.js. On Node.js, module.compiled.js loads that bundle instead of module.js, so a breakpoint in module.js never stops. Open the bundle in DevTools (its map shows module.ts) and turn off ignore-listing for it.
The code that rewrites a file starting with "use client" while compiling is an SWC transform. Which crate holds it?
crates/
next-api
next-build
next-build-test
next-code-frame
next-core
next-custom-transforms
next-napi-bindings
next-taskless
wasmcrates/next-custom-transforms. Its src/transforms folder holds Next.js's transforms, like react_server_components.rs and server_actions.rs. SWC itself comes from crates.io as swc_core.
Check your understanding
For each question, ask which half of the repository holds the code, and which file in dist runs it.
Question 1 of 7In which folder of vercel/next.js is the code that runs when you type
next start?Choose an answer to see the explanation.
Question 2 of 7Under
next start, the stack showsr0.handle; undernext dev,AppRouteRouteModule.handle. Why?Choose an answer to see the explanation.
Question 3 of 7Which React renders a Server Component in the App Router of Next.js 16.4.3?
Choose an answer to see the explanation.
Question 4 of 7This line came from the tea shop's stack. Which line of packages/next/src does it point to?
Read the code, then predictTextdist/server/base-server.js:1008 -> ? NextNodeServer.handleRequestImplChoose an answer to see the explanation.
Question 5 of 7What's inside the
@next/swc-darwin-arm64package that npm installed for the recording?Choose an answer to see the explanation.
Question 6 of 7You run
NODE_OPTIONS=--inspect next dev. Which inspector port does the server process get?Choose an answer to see the explanation.
Question 7 of 7In DevTools, stepping into a Next.js function skips it, and its frames are hidden. Why?
Choose an answer to see the explanation.
Key takeaways
vercel/next.jsholds thenextpackage inpackages/next/src:binandclifor the commands,serverfor requests,buildfornext build,clientfor the browser,sharedandlibfor helpers.- SWC compiles most source files one by one into
dist, with a.mapbeside each. The route runtimes are bundled into files likeapp-route-turbo.runtime.prod.js, andmodule.compiled.jspicks one. - The Rust crates, in
cratesandturbopack/crates, are compiled into one.nodefile per platform, published as@next/swc-<platform>. SWC itself is a separate project. - The App Router runs the canary React in
next/dist/compiled/react(19.3.0-canary-278794d7-20261002in 16.4.3); the Pages Router runs the app's ownreact. - To build Next.js, follow its contributing guides: pnpm,
pnpm buildorpnpm dev, andpnpm swc-build-nativeonly for Rust. - A debugger attaches to the server process. The source maps of Next.js's server files hold the TypeScript, and ignore-list it on purpose.
Remember the one-liner.
A line of Next.js's server code in node_modules/next/dist has a .map beside it that leads back to packages/next/src, at the tag of the version you run.
Coming next: From next dev to a response. It follows one request through those files: the process next dev starts, the router server, the order of redirects, proxy.ts and rewrites, and the server classes that hand the request to a render.