Skip to content

refactor: move embedded runtime JS to real .js files (js2c) + enum eval caching#411

Draft
edusperoni wants to merge 2 commits into
feat/error-handlingfrom
feat/js-builtins
Draft

refactor: move embedded runtime JS to real .js files (js2c) + enum eval caching#411
edusperoni wants to merge 2 commits into
feat/error-handlingfrom
feat/js-builtins

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What

Moves the ~26KB of JavaScript that was embedded as C++ string literals into real, version-controlled .js files under NativeScript/runtime/js/, compiled into the framework at build time (Node-style js2c), and adds two steady-state perf fixes for jitless mode.

Stacked on #409; draft until that lands.

Extraction & build

  • tools/js2c.mjs (Node CLI) converts runtime/js/*.js into a generated RuntimeBuiltins.{h,cpp} table (byte arrays, deterministic output). Supports --minify via esbuild (whitespace+syntax only) — implemented but not enabled in any configuration.
  • New "Generate RuntimeBuiltins" shell build phase on the NativeScript target, incremental via input/output xcfilelists (skips when nothing changed; verified). Output dir NativeScript/runtime/generated/ is gitignored.
  • Extraction fidelity was machine-verified: prettified files were proven AST-identical to the original runtime strings by byte-comparing esbuild-minified output of both.

Loader

  • BuiltinLoader::RunBuiltin is the single compile-and-run path: proper internal/<name>.js script origins (runtime frames are now identifiable in stack traces) and a process-wide bytecode cache — first isolate compiles with kEagerCompile, workers/subsequent isolates consume via kConsumeCodeCache (with rejected-cache fallback), so the ~26KB is parsed once per process instead of once per isolate.
  • The placeholder-module proxy is now a static builtin (placeholder-module.js) parameterized with the error message instead of splicing it into source text.

Perf (jitless steady-state)

  • Interop::WriteValue no longer recompiles + re-runs an enum's __tsEnum snippet on every FFI marshal — the numeric value is memoized on EnumDataWrapper.
  • GlobalPropertyGetter no longer recompiles a JsCode global's snippet on every read — the evaluated result is defined as a real own property on the global (interceptor is kNonMasking, so later reads bypass it entirely). Includes a reentrancy guard: CreateDataProperty re-enters the interceptor before the property exists.

Behavior notes

  • The legacy placeholder proxy source never compiled — the spliced error message's quotes made it a guaranteed SyntaxError, so the first require() of a missing optional module always threw via the leaked pending exception (which the shared require tests rely on). CreatePlaceholderModule now keeps that contract explicitly with a proper Error: Cannot find module '<name>'; repeat requires still get the cached placeholder, whose exports now actually throw on access as originally designed.
  • Stack traces from runtime internals now show internal/<name>.js origins instead of anonymous <eval> frames.

Testing

  • Full TestRunner suite on iOS simulator (rebased on latest feat/error-handling): 903 tests, 0 failures (includes worker tests, which exercise the cross-isolate code cache).
  • Incremental build check: codegen phase skipped on no-change rebuild, reruns when a .js input is touched.

…js2c

The ~26KB of JavaScript previously embedded as C++ string literals now
lives in NativeScript/runtime/js/*.js. A "Generate RuntimeBuiltins"
build phase runs tools/js2c.mjs to emit a generated source table, and
call sites go through BuiltinLoader::RunBuiltin, which sets proper
internal/<name>.js script origins and shares an in-process bytecode
cache across isolates so workers stop re-parsing the builtins.

The placeholder-module proxy is now a static builtin parameterized with
the error message. The legacy inline proxy source never compiled (the
spliced message's quotes made it a guaranteed SyntaxError), so the first
require() of a missing optional module always threw via the leaked
pending exception; CreatePlaceholderModule now throws an explicit
"Cannot find module" error to keep that contract.
Interop::WriteValue recompiled and re-ran an enum wrapper's __tsEnum
snippet on every FFI marshal; the value is now memoized on the
EnumDataWrapper. GlobalPropertyGetter did the same on every read of a
JsCode global; the evaluated result is now defined as a real own
property on the global (the interceptor is kNonMasking, so later reads
bypass it entirely), with a reentrancy guard because CreateDataProperty
re-enters the interceptor before the property exists.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74dc8b92-396c-4be4-bf32-7068d45169cf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant