Skip to content

feat: inline sourcemaps for .ts modules and remap Error.stack to source#227

Open
benpalevsky wants to merge 2 commits into
godotjs:mainfrom
hatgg:6-swc-sourcemaps
Open

feat: inline sourcemaps for .ts modules and remap Error.stack to source#227
benpalevsky wants to merge 2 commits into
godotjs:mainfrom
hatgg:6-swc-sourcemaps

Conversation

@benpalevsky

Copy link
Copy Markdown
Contributor

Part 2 of 3 — the SWC stack. Builds on PR 5 (5-swc-transpiler).

Problem

When a .ts file is transpiled to JavaScript, the line and column numbers shift. So a runtime error points at a position in the generated JavaScript, not at your actual TypeScript source. That makes stack traces hard to read.

What this adds

Real TypeScript stack traces.

  • The transpiler now also emits an inline sourcemap (a small map, embedded as a data URL, that says "generated line X came from .ts line Y").
  • A V8 hook (PrepareStackTraceCallback) rewrites each frame in Error.stack back to its original .ts line and column, using a cache of those sourcemaps.

The result: when something throws, the stack trace points at the .ts source you actually wrote.

Scope note (read before merging)

The PrepareStackTraceCallback hook is installed on every V8 build (#if JSB_WITH_V8), not just builds with the embedded transpiler. So this also routes the existing tsc sourcemap path (and the default Error.stack formatting) through the hook on a default build — Error.stack is now built by this callback and run through the sourcemap cache, where before it used V8's built-in formatter. That's a deliberate improvement to .js stack traces, but it is a behavior change to the default build, and the frame regex is widened to also match .ts/.tsx/.mjs/.cjs. If maintainers prefer the default build untouched, the hook can instead be gated behind the transpiler flag (JSB_WITH_TYPESCRIPT_TRANSPILER) so only transpiler builds get it — happy to do that.

Caveat to check

The remap matches each stack frame to the right sourcemap by source path. If the path V8 reports for a frame doesn't exactly match the key the sourcemap was stored under, that frame won't be remapped (it falls back to the generated position — no crash, just an unmapped frame). This is the one part that can only be confirmed by running it.

How to verify

Throw an error from inside a .ts module and confirm the printed stack trace shows .ts file positions, not generated .js positions.

A changeset is included. (Opened on its own, this PR's base is 5-swc-transpiler; see the README for how the stack is delivered.)

Compiles a Rust staticlib (transpiler-ffi/) built around SWC and exposed through a minimal two-function/one-struct C FFI, wired into the engine via SCsub. jsb_module_resolver loads .ts sources directly by transpiling them in-process into the CommonJS module wrapper, so TypeScript no longer needs a separate tsc emit step to run.

Opt-in via a new use_typescript_transpiler build flag, off by default. With the flag off (the default) no Rust toolchain enters the build graph and every .ts code path is compiled out behind JSB_WITH_TYPESCRIPT_TRANSPILER: script loading keeps using the tsc-emitted .js (convert_typescript_path), the resolver does not probe .ts, and the build behaves as before this change.

The FFI is panic-safe (catch_unwind at the boundary) and hands buffers to C through Box<[u8]> so the C-side free is sound. Build flags in SCsub allow the staticlib's libstd symbols to coexist with Godot's prebuilt accesskit staticlib on Linux/Windows.
The FFI now returns an inline data:application/json;base64 sourcemap URL, which the resolver appends to the wrapped module source as a //# sourceMappingURL comment and also decodes into a per-environment source-map cache. A V8 PrepareStackTraceCallback rewrites runtime Error.stack frames to .ts line/col through that cache, since V8 12.4 only applies source maps in DevTools and not to Error.stack.
@Benjamin-Dobell

Copy link
Copy Markdown
Member

I'll need to dig into this one.

Source maps should already be working in general, but it's more in collaboration with the V8 debugger protocol. So like when you attach a debugger it can step through your original TS files on disk. By the look of it, this is more about logged stack traces?

It's a good idea in general, but I wonder if I can get it working as an official external tool, rather than part of GodotJS builds. The ideal outcome would be that you're able to distribute your production builds without source maps, but you keep the build sourcemaps. Then when users report crashes, you could run them through a tool that gives you back clean .ts stack traces from real users.

I'm not too keen on the embedded TS compiler, because SWC has limitations equivalent to isolatedModules: true for tsc/tsgo, which may make it incompatible with some projects. I think I'd like to try make this stuff more pluggable.

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.

2 participants