Use corepack pnpm to run pnpm commands, e.g.:
corepack pnpm installcorepack pnpm buildcorepack pnpm build:ts
Variants are WASM/JS builds of QuickJS with different configurations. They are generated by scripts/prepareVariants.ts.
To build a variant:
cd packages/variant-quickjs-<name>
make # builds the C code with emscripten
corepack pnpm build:ts # builds the TypeScript wrapper- Most variants use the default emscripten version (defined in
scripts/prepareVariants.tsasDEFAULT_EMSCRIPTEN_VERSION) - The asmjs variant uses an older emscripten version (
ASMJS_EMSCRIPTEN_VERSION) to avoid newer browser APIs - Emscripten runs via Docker when the local version doesn't match; see
scripts/emcc.sh
Run all tests:
corepack pnpm testRun tests for a specific variant (e.g., quickjs-ng only):
cd packages/quickjs-emscripten
npx vitest run -t "quickjs-ng"Other test filters:
-t "RELEASE_SYNC"- only release sync variants-t "DEBUG"- only debug variants-t "QuickJSContext"- only sync context tests
- Never use
git commit --amend- always create new commits. It's better to have a history of what was actually done since branches get rebased/squashed anyway when merged. - Before pushing, run
pnpm run prettierandpnpm run lintto fix formatting and lint issues. No need to check after every commit - just before pushing.
After pushing a PR, monitor CI using the gh CLI:
# Check CI status for a PR
gh pr checks <PR_NUMBER>
# View a specific job's progress
gh run view --job=<JOB_ID>
# Get logs for failed steps
gh run view <RUN_ID> --log-failedWhen CI fails:
- Get the failed logs with
gh run view <RUN_ID> --log-failed | tail -100 - Fix the issue locally
- Commit the fix (new commit, don't amend)
- Push and repeat until CI passes
The full CI build takes ~20 minutes due to emscripten compilation.
scripts/prepareVariants.ts- Generates all variant packages from templatesscripts/generate.ts- Generates FFI bindings and symbolstemplates/Variant.mk- Makefile template for variantsc/interface.c- C interface to QuickJS exposed to JavaScript
QuickJS has strict ownership semantics. Functions either "consume" (take ownership of) or "borrow" values:
Functions that CONSUME values (caller must NOT free afterward):
JS_DefinePropertyValue- consumesvalJS_DefinePropertyValueStr- consumesvalJS_DefinePropertyValueUint32- consumesvalJS_SetPropertyValue- consumesvalJS_SetPropertyStr- consumesvalJS_Throw- consumes the error value
Functions that DUP values internally (caller SHOULD free afterward):
JS_NewCFunctionData- callsJS_DupValueon data values, so free your reference afterJS_SetProperty- dups the value
Common double-free bug pattern:
// WRONG - double free!
JSValue val = JS_NewString(ctx, "hello");
JS_DefinePropertyValueStr(ctx, obj, "name", val, JS_PROP_CONFIGURABLE);
JS_FreeValue(ctx, val); // BUG: val was already consumed!
// CORRECT
JSValue val = JS_NewString(ctx, "hello");
JS_DefinePropertyValueStr(ctx, obj, "name", val, JS_PROP_CONFIGURABLE);
// No JS_FreeValue needed - value is consumedSome functions have different signatures between bellard/quickjs and quickjs-ng:
// bellard/quickjs - class ID is global
JS_NewClassID(&class_id);
// quickjs-ng - class ID is per-runtime
JS_NewClassID(rt, &class_id);Use #ifdef QTS_USE_QUICKJS_NG for compatibility:
#ifdef QTS_USE_QUICKJS_NG
JS_NewClassID(rt, &class_id);
#else
JS_NewClassID(&class_id);
#endifJS_NewClassIDallocates a new class ID (only call once globally or per-runtime for ng)JS_NewClassregisters the class definition with a runtimeJS_IsRegisteredClasschecks if a class is already registered with a runtime- Class prototypes default to
JS_NULLfor new classes - set withJS_SetClassProtoif needed
When disposing resources, order matters for finalizers:
// CORRECT: Free runtime first so GC finalizers can call back to JS
const rt = new Lifetime(ffi.QTS_NewRuntime(), undefined, (rt_ptr) => {
ffi.QTS_FreeRuntime(rt_ptr); // 1. Free runtime - runs GC finalizers
callbacks.deleteRuntime(rt_ptr); // 2. Then delete callbacks
});If you see assertions like:
Assertion failed: i != 0, at: quickjs.c, JS_FreeAtomStruct- atom hash corruption (often double-free)Assertion failed: list_empty(&rt->gc_obj_list)- objects leakedAssertion failed: p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT- memory corruption
These usually indicate memory management bugs: double-frees, use-after-free, or missing frees.
QTS_prefix for exported FFI functions (defined in interface.h, called from JavaScript)qts_prefix for internal C helper functionsstatickeyword for file-local functions and variables- Use
boolfrom<stdbool.h>for boolean values (already included)
- Section comments with
// ----separators for logical grouping - Forward declarations at the top of sections when needed
- Constants defined near related functions
- Check
JS_IsException(value)for error conditions - Use
JS_GetException(ctx)to retrieve the exception value - Clean up resources in reverse order of allocation
- Return
JS_EXCEPTIONorNULLto signal errors to callers
// ----------------------------------------------------------------------------
// Section Name
// Forward declaration
static JSValue qts_helper_function(JSContext *ctx, JSValueConst obj);
// Internal helper - not exported
static bool qts_check_something(JSContext *ctx, JSValueConst obj) {
if (!JS_IsObject(obj)) return false;
// ... implementation
return true;
}
// Exported FFI function
JSValue *QTS_DoSomething(JSContext *ctx, JSValueConst *obj) {
JSValue result = qts_helper_function(ctx, *obj);
if (JS_IsException(result)) {
return jsvalue_to_heap(JS_EXCEPTION);
}
return jsvalue_to_heap(result);
}