How this is builtGroundworkHow this is built
2 / 26 written chapters

B2 · Part 1 · Foundations

The tech stack

Every dependency, what it does here, where it runs, and what it would cost to remove.

6 min read1 diagram6 sections
06On this page
  1. The frame
  2. The editor
  3. The runtimes
  4. Why the runtimes come from a CDN
  5. Small pieces
  6. The tools that never reach a reader

The frame

package.json lists 43 runtime dependencies and 16 development ones. That sounds like a lot for a static site, and it is: 20 of the 43 are @codemirror/ packages, and most of the rest exist for the editor. The frame the site actually sits in is small.

ThingVersionWhat it does here
Next.js (App Router)16.3.0Routing, the build, static generation, metadata routes, the server/client split
React19.2.8Components; server components for everything that does not need a browser
TypeScript5.9.3Types the code and the content, so a malformed chapter fails the build. The same package also runs in the browser as the playground's TypeScript compiler
CSS—app/globals.css at about 7,400 lines, plus 84 CSS modules for the pages and tools that carry their own styles
Tailwind4Imported for its theme and utilities layers only, with no preflight reset. The site is styled with custom properties and its own class names
Vercel—Host, CDN, firewall, analytics
Node24.xPinned in engines and to 24 in .nvmrc, so the local tools, Vercel and the project settings agree

The editor

The playground's editor is CodeMirror 6, mounted through @uiw/react-codemirror in components/practice/CodeEditor.tsx. lib/codeLanguages.ts lists 17 languages, and each one's syntax support is a dynamic import(), so choosing Rust downloads the Rust grammar and nothing else. Kotlin, Swift, C#, Ruby and Lua use the older stream parsers from @codemirror/legacy-modes. Three packages from Replit add a Vim mode, a minimap and indentation guides.

The editor's tooling runs off the main thread. lib/editor/toolsWorker.ts is a Web Worker that loads eslint-linter-browserify with the globals package for linting, Prettier's standalone build for formatting, and the TypeScript compiler for type errors. That is why Prettier appears under dependencies rather than devDependencies: it is not only a tool the repository runs on itself, it ships to readers.

The runtimes

Eleven of the 17 languages run. None of them runs on a server. Each one is loaded the first time a reader picks it, and most of them come from jsDelivr rather than from this site.

LanguageWhat runs itServed fromRuns in
JavaScriptThe browser's own engine—jsWorker, a Web Worker
TypeScriptThe typescript package, type-checking against 46 lib files, then run as JavaScriptThis site: /wasm/typescript-lib/, 1.2 MBCompiled in the page, run in jsWorker
PythonPyodide 314.0.6jsDelivrpyodideWorker
SQLsql.js 1.14.2, SQLite compiled to WebAssemblyThe glue is bundled; the .wasm comes from jsDelivrThe page
Rubyruby.wasm 2.10.1 with Ruby 3.4jsDelivrscriptWorker
PHPphp-wasm 0.1.0jsDelivrscriptWorker
Luawasmoon 1.16.0jsDelivrscriptWorker
C, C++clang from @yowasp/clang, run on a WASI shimjsDelivrscriptWorker
HTML, CSSThe browser—A sandboxed preview iframe
React componentsreact-sandbox.js, 204 KB, bundled by esbuild from lib/reactSandbox/This site: /wasm/An iframe with sandbox="allow-scripts"
Java, Go, Rust, Kotlin, Swift, C#Nothing—Write-only: highlighted, formatted, not run
The page CodeMirror, SQL, TypeScript compile Sandboxed iframes HTML and CSS, React jsWorker JavaScript, and compiled TS pyodideWorker Python scriptWorker Ruby, PHP, Lua, C, C++ toolsWorker lint, format, type errors jsDelivr Pyodide, ruby.wasm, php-wasm, wasmoon, clang This site's /wasm/ 46 TypeScript lib files react-sandbox.js
Four workers, two kinds of iframe, and the page itself. The TypeScript lib files are also what the page's own compiler reads, and sql.js fetches its .wasm from jsDelivr straight into the page.

Why the runtimes come from a CDN

Pyodide and sql.js used to be copied into public/wasm/ by a prebuild script and shipped with every deploy. The commit that moved them to jsDelivr records why: public/wasm/ was 18 MB, Vercel keeps every deployment, and the project had gone over its storage allowance on two runtimes that only load when someone picks Python or SQL. After the move the folder was 1.3 MB, just the TypeScript lib files. The Ruby, PHP, Lua and clang runtimes were added the same way from the start. The trade is plain: deploys got small, and in exchange the Python and C++ paths depend on a third party being up. tests/wasm-assets.test.ts fails if the pinned versions drift from what is installed. lib/wasmAssets.ts holds every version and URL in one place, the NEXT_PUBLIC_PYODIDE_BASE, NEXT_PUBLIC_SQL_JS_BASE and NEXT_PUBLIC_NPM_CDN variables can point them somewhere else, and scripts/copy-wasm-assets.mjs --all still knows how to self-host Pyodide and sql.js. The Content-Security-Policy in next.config.ts is built from the same list, so adding a runtime origin in one place allows it in the other.

Small pieces

  • msedge-tts — the narrator's voice, behind /api/tts. Unofficial, free, and the one real single point of failure among the server functions. See the endpoints chapter.
  • Radix — @radix-ui/react-focus-scope traps focus in the shortcut help, loaded dynamically, and inside the shared FocusTrap, imported statically, which holds focus in the site menu, the Chapters sheet and the Filters sheet. The radix-ui package supplies one component, the Select in components/ui/select.tsx, used by four files.
  • clsx and tailwind-merge — one helper, cn() in lib/utils.ts, for that Select.
  • next/font — 13 Google font families behind the 7 font styles in the display picker, alongside 9 colour themes.
  • Vercel Analytics and Speed Insights — pageviews and field Core Web Vitals.
  • @sentry/nextjs — imported only when NEXT_PUBLIC_SENTRY_DSN is set. Without it, browser errors are posted to /api/client-error and land in the function log instead.

The tools that never reach a reader

ToolWhat it catches
ESLint + eslint-config-nextThe usual, plus Next-specific mistakes
PrettierFormatting, with content/interview-data.ts and content/practice/ ignored on purpose
cspellSpelling across every file, including all the prose
Vitest78 test files. Among them: the content's integrity, the site's claims about itself, SEO metadata, and the pure engines behind the mock interview, the grader, the debugger and the whiteboard
Playwright27 tests in one smoke spec against a production build: the pages load, the menu opens, the path trail stays clear of itself, the playground and an exercise run, a DSA chapter's code switch, player and chapter check work, the end-of-chapter popup asks once, the placement flow lands on a plan, unknown URLs return 404
Lighthouse CIPerformance budgets, run three times so one slow run cannot fail them; started by hand with npm run test:perf
esbuildBundles the React sandbox before every build
husky + lint-stagedLint, format and spell-check staged files on commit; reject code comments on push
What is not here, deliberately

No state management library: useSyncExternalStore over small storage modules covers it. No component library beyond that one Select: the site has its own vocabulary and its own stylesheet. No ORM, because there is no database. No CMS, because the content is the repository. No auth, because there is nothing per-user on the server. And no code-execution service: every language that runs, runs in the reader's tab, which is why the playground costs nothing to operate however much it is used.