I2 · Part 2 · How it runs
How 1,762 files get made
One source of truth, one pass, and every page, index, test case and preview image is derived from it in about thirty seconds.
07On this page
The pipeline
The steps, in order
- prebuild.
scripts/copy-wasm-assets.mjscopies the 46 TypeScript declaration files listed inlib/tsLibFiles.jsonintopublic/wasm/typescript-lib/. The editor's type checker needs them. Thenscripts/build-react-sandbox.mjsuses esbuild to bundlelib/reactSandbox/runtime.tsinto a minified ES2020 script,public/wasm/react-sandbox.js, which is 205 KB. The React exercises run inside it. - Compile. Turbopack bundles the app. In the last production build's trace this took 2.2 seconds.
- Type check.
tscruns insidenext buildand took 2.8 seconds. When the type check runs on its own on a fresh checkout,npx next typegenhas to run first, because the globalPagePropsandLayoutPropshelper types have to exist on disk before a separatetsc --noEmitcan checkapp/. - Static generation. Every
generateStaticParamsis called, and every page, route handler and metadata file is rendered to disk. This took 21.5 seconds.
The whole next-build span in .next/trace-build was 29.6 seconds. About
seventy percent of it is the step that grows with the content.
Where the routes come from
24 files under app/ export generateStaticParams. The 20 topic
[chapter] routes call topicChapterParams(topicId) in
components/reader/topicPages.tsx, which returns every chapter in the topic, including
outlines. An outline still gets a page, a roadmap card with its syllabus section and a "Meanwhile"
link to a related written topic, but its metadata sets index: false. Crawlers can
follow its links, but it stays out of search
results. /problems/[slug] and /problems/[slug]/cases both map over
exercises(), and /level/[topic] maps over topics that have levels.
Nothing reads the content directly except lib/content.ts, which is also where derived
fields come from. The reading time shown on every chapter is computed there: anything inside a
<script>, <pre>, <style> or
<svg> element is dropped, the remaining tags are stripped, the words are
counted and divided by 180, and the result is rounded with a minimum of two minutes.
Recording test cases for other languages
The most unusual build step is lib/polyglot/record.ts. Every exercise ships a
JavaScript reference solution and JavaScript tests. The playground can also grade Python, Ruby,
PHP, Lua, C and C++. Rather than hand-write tests in six more languages, the build
records what the JavaScript does:
- It finds the solution's top-level function and its parameter names with a regular expression. Anything else is declined with a reason.
- It keeps only tests made entirely of direct
assert.equal,strictEqual,deepEqualordeepStrictEqualcalls on that function, plusconstset-up lines. - It runs the solution and each test inside
vm.runInNewContextwith a two-second timeout. The function is wrapped so that every call logs a deep copy of its arguments and its return value. - Recorded values have to be plain: numbers, strings, booleans and nested arrays. From those it infers a signature,
int,float,bool,stringorlistof one of them, and it gives up if an argument changes type between calls.
app/problems/[slug]/cases/route.ts is force-static, so this runs 538
times during the build and never in production. Of those, 222 exercises came out gradable in other
languages, with 1,439 tests and 2,048 recorded cases, and 395 tests were skipped as not replayable.
The other 316 carry a reason the page shows instead:
| Why it stays JavaScript-only | Exercises |
|---|---|
| A React component | 99 |
| Tests check properties of the answer, not an exact value | 84 |
| Works on JavaScript objects | 61 |
| About JavaScript itself (no top-level function) | 27 |
| Parameters with destructuring or rest syntax | 17 |
| About the async model | 11 |
| Arguments change type between calls | 11 |
| Tests observe something besides the return value | 6 |
The cost is size. Most case files are a one-line refusal, and the median is 97 bytes. But
recording keeps every call, including the recursive and repeated calls a test makes, so the
largest file, ex-maximal-square, is 1,002,059 bytes. Together they come to 4.2 MB.
A reader only downloads one after switching that problem to another language.
Indexes, sitemap and preview images
The 21 search indexes are force-static route handlers too. The
search chapter covers what goes in them.
app/sitemap.ts reads the same interface and lists 1388 URLs. Outlines are left out.
A topic's pages carry the meta.updated month from its notes file as
lastModified; everything with no such date, including every problem page, carries one
shared constant that moves only when that content actually changes, so deploys stop telling
crawlers that everything just did. app/robots.ts disallows /api/ and
points at the sitemap.
Nine opengraph-image.tsx files draw 1200 by 630 PNG cards with next/og's
ImageResponse, through ogCard in lib/og.tsx. The topic cards
count their written chapters and exercises at build time, so the chips on a shared link cannot go
stale. Each PNG is between 37 and 58 KB, and all of them are prerendered.
lib/metadata.ts gives every other page one of those nine images by matching its path
to the nearest section, so a shared link always carries a picture, not bare text.
What next.config.ts does, and does not, do
It sets headers and redirects, turns off the automatic trailing-slash redirect so that proxy.ts can do it in one hop, and nothing else: no images block, no experimental
flags, no webpack changes. The CSP inside it is assembled from two helpers. One is
wasmOrigins() from lib/wasmAssets.ts. The other is
sentryOrigin(), which adds nothing unless a DSN is set.
lib/wasmAssets.ts is where every runtime version is pinned: Pyodide 314.0.6, sql.js
1.14.2, wasmoon 1.16.0 for Lua, Ruby WASI 2.10.1, php-wasm 0.1.0, clang 22.0.0-git20542-10 and the
WASI shim 0.4.2. By default they are all loaded from cdn.jsdelivr.net. The prebuild
script can copy Pyodide and sql.js into public/wasm/ with --all, and
three environment variables can point the loaders there, but the default build does neither.
tests/wasm-assets.test.ts fails if the pinned Pyodide or sql.js version stops
matching the installed package, or if a base URL loses its trailing slash.
What the build leaves behind
| Output | Count | Served how |
|---|---|---|
| Pages (HTML and RSC) | 1,222 | Static, never revalidated |
| Static route handlers | 571 | Static JSON |
| Metadata routes | 14 | Static |
| JavaScript chunks | 81 | Hashed, under /_next/static |
| Functions | 4 under /api, plus on-demand rendering for unknown slugs | Per request |
The prerendered HTML alone comes to 63.3 MB. .next/server/app, with the RSC payloads
and route bodies, is 198 MB on disk.
Static generation is the only step that grows with the content, and it renders about 80 files a second. Writing the 358 outlined chapters would not add a single route, because every outline already has its page; it would only make those pages longer. The build would get slower by seconds, not minutes. Deploy storage is a more likely limit: each deployment is large enough that a scheduled workflow prunes old ones.
Finished reading?
0 of 26 chapters read in this series