This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
txiki.js is a small JavaScript runtime built on QuickJS-ng (JS engine), libuv (platform I/O), and libwebsockets (HTTP/WebSocket). It targets WinterTC compliance and provides Web Platform APIs.
make # Build (Release). Output: build/tjs
make debug # Build (Debug)
make js # Rebuild JS bundles only (after editing src/js/)
make test # Run all tests
make test-advanced # Run advanced/integration tests (requires npm install in tests/advanced/)
make format # Run clang-format on C sources
make lint # Run ESLint on JS sourcesAfter modifying files in src/js/, run make js && make to rebuild.
After modifying C files, just run make.
BUILDTYPE=Debug make # Debug build
MIMALLOC=OFF make # Disable mimalloc (required for ASAN)
BUILD_WITH_ASAN=ON MIMALLOC=OFF make # Enable AddressSanitizer (must disable mimalloc)
BUILD_WITH_UBSAN=ON make # Enable UndefinedBehaviorSanitizer (Linux/macOS only)
BUILD_WITH_GC_STRESS=ON make # Force a full GC before every JS allocation (GC stress)
BUILD_WITH_WASM=OFF make # Disable WebAssembly / WAMR (drops the WebAssembly global and tjs:wasi)
BUILD_WITH_SQLITE=OFF make # Disable SQLite (drops the tjs:sqlite module; localStorage falls back to in-memory)These flags shrink the binary; they are orthogonal to one another and can be combined.
BUILD_WITH_STRIP=ON make # Strip the symbol table from the binary after linking
BUILD_WITH_LTO=ON make # Enable link-time optimization (slower link, smaller/faster binary)
BUILD_WITH_GC_SECTIONS=ON make # Per-function/data sections + linker dead-code stripping
BUILDTYPE=MinSizeRel make # Optimize for size instead of speed (-Os)BUILD_WITH_STRIP runs ${CMAKE_STRIP} as a post-build step (skipped when CMAKE_STRIP is
unset, e.g. MSVC). BUILD_WITH_LTO falls back to a warning if the toolchain can't do IPO.
BUILD_WITH_GC_SECTIONS maps to -Wl,--gc-sections (GNU/lld), -Wl,-dead_strip (Apple), or
/OPT:REF /OPT:ICF (MSVC). BUILDTYPE=MinSizeRel is a standard CMake build type (no extra flag).
ASAN and mimalloc are mutually exclusive. UBSAN is not supported on MSVC.
ASAN (AddressSanitizer): Detects memory errors (use-after-free, buffer overflow, etc.).
cmake -B build-asan -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_WITH_ASAN=ON -DBUILD_WITH_MIMALLOC=OFF
cmake --build build-asan
./build-asan/tjs test tests/UBSAN (UndefinedBehaviorSanitizer): Detects undefined behavior (misaligned access, integer overflow, etc.).
cmake -B build-ubsan -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_WITH_UBSAN=ON
cmake --build build-ubsan
UBSAN_OPTIONS="halt_on_error=1:suppressions=$(pwd)/ubsan.supp" ./build-ubsan/tjs test tests/UBSAN requires a suppressions file (ubsan.supp) for known issues in vendored dependencies (WAMR alignment). Fix UBSan issues in our own code (src/) rather than adding suppression rules.
BUILD_WITH_GC_STRESS=ON defines QuickJS's FORCE_GC_AT_MALLOC, which runs a full garbage collection before every JS object allocation. This surfaces GC bugs — objects that are collected while still referenced (missing GC roots / mark hooks). Such premature frees usually only manifest as use-after-free, so combine it with ASAN to actually catch them:
cmake -B build-gcstress -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_WITH_GC_STRESS=ON -DBUILD_WITH_ASAN=ON -DBUILD_WITH_MIMALLOC=OFF
cmake --build build-gcstress
TJS_GC_STRESS=1 ./build-gcstress/tjs test tests/Set TJS_GC_STRESS=1 when running the test suite against a GC-stress build: a full GC before every allocation starves the event loop, so a handful of timing-sensitive tests (e.g. test-performance) skip themselves when that variable is set rather than failing on wall-clock assertions. The first tjs bundle call (esbuild download) is implemented in JS and is pathologically slow under GC stress, so the CI job warms the shared ~/.tjs esbuild cache with a normal build first.
GC stress is orthogonal to the allocator and the sanitizers; it just changes the GC trigger threshold. It makes execution drastically slower (a full GC per allocation), so the CI job runs it on Linux only.
./build/tjs test tests/ # All tests
./build/tjs run tests/test-something.js # Single test file (use "run", not "test")
VERBOSE_TESTS=1 ./build/tjs test tests/ # Verbose outputTest files must be named test-*.js and live in tests/. They use tjs:assert for assertions.
One test, one file. Each test file should cover a single feature or behavior. Prefer
splitting distinct behaviors into separate test-*.js files (e.g. test-fetch-h2-post-body.js,
test-fetch-h2-empty-body.js) over accumulating many unrelated cases in one file — a focused
file is easier to run in isolation (tjs run tests/test-foo.js) and makes a failure point
directly at the behavior that broke.
When a test file requires a feature that can be compiled out (e.g. BUILD_WITH_WASM=OFF),
add its filename or glob to the matching feature key in tests/feature-skip.json. The test
runner reads this file and skips matched tests on builds where the feature is absent
(detected via tjs.engine.features). Only * wildcards at a single position are supported
(no **, no ?).
C layer (src/): Native modules (mod_*.c) expose low-level APIs to JS via QuickJS bindings. Key files:
vm.c/private.h— TJSRuntime lifecycle (QuickJS runtime + libuv loop)builtins.c— Registers all native modulescli.c— Entry point, CLI argument parsingmod_*.c— Native module implementations (fs, os, process, dns, udp, tls, sqlite3, ffi, etc.)httpclient.c/httpserver.c/ws.c— HTTP and WebSocket via libwebsocketswebcrypto.c— Web Crypto APIwasm.c— WebAssembly via WAMR
JS layer (src/js/): Builds on C APIs to implement Web Platform interfaces.
polyfills/— Browser API polyfills (EventTarget, fetch, WebSocket, console, crypto, etc.). Import order inindex.jsmatters — dependencies before dependents.core/— Initializes thetjsglobal objectstdlib/— Standard library modules importable astjs:modulename(path, fs, http, sqlite, ffi, etc.)run-main/— CLI subcommands (test runner, bundler, compiler)run-repl/— REPL implementationworker/— Web Worker bootstrap
JS source → esbuild bundle → tjsc (QuickJS bytecode compiler) → C byte arrays → compiled into binary.
Generated files live in src/bundles/ (git-ignored). The Makefile js target runs this pipeline.
All vendored as git submodules: quickjs, libuv, mimalloc, sqlite3, libwebsockets, mbedtls, wamr, miniz, tweetnacl, ada.
- C code follows
.clang-formatstyle; runmake formatbefore committing - Prefer
Promise.withResolvers()for deferred promises - Platform-specific C code uses
#ifdef _WIN32/#ifndef _WIN32guards - Stdlib modules are imported as
tjs:modulename(e.g.,import assert from 'tjs:assert') - Comment only what the code can't say itself. Add a comment when the why is non-obvious — an unusual approach, a subtle invariant, a workaround, a non-local consequence. Do not add comments that restate what the code plainly does; if reading the code tells you the same thing, the comment is noise. Delete such comments.
- Don't defend against states that can't happen. If an invariant must always hold,
assert it with
CHECK(...)(which aborts), not with a fallback branch that silently "handles" the impossible case. Fallback-for-the-impossible hides bugs; aCHECKdocuments the invariant and fails loudly if it is ever violated. - In JS classes, keep internal state in real private fields (
#foo), never in_foo-prefixed public properties. When another class in the same module needs access, expose a module-private accessor from astatic {}block instead of widening the public API. Use a module-scopedSymbolkey only for state that must hang off an object of another class (e.g. pinning an owner on a native object to keep it alive).