Description
Suggestion
🔍 Search Terms
Source maps, post hoc debugging
✅ Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
I would like to propose that TypeScript create an experimental source map extension (version 4). The purpose of this would be to improve the ability to apply Source Map data to stack traces and to have the Source Map contain everything needed to go from a minified stack trace to an unminified stack trace without using function name guessing.
📃 Motivating Example
Suppose I receive the following information from my application:
TypeError: Cannot read properties of undefined (reading 'x')
at re.draw (bundle.js:1:16372)
at re.drawLayer (bundle.js:1:14170)
at be.draw (bundle.js:1:74592)
at Te.render (bundle.js:1:114230)
at Te.reflowCanvas (bundle.js:1:113897)
at HTMLDocument.anonymous (bundle.js:1:135849)
The source map will resolve the first stack frame to draw(src, sprite, x, y, frame)
, and will correctly point out that the failure here is that the undefined value is actually frame
. However, that is not the name of the function. The function's name is draw
, which is a member of the Render
class.
If I simply apply the value of the names
array to the function, decoding the stack trace is not particularly useful. It would look something like this:
TypeError: Cannot read properties of undefined (reading 'x')
at frame (file1.ts:302:7)
at draw (file1.ts:144:5)
at Render (file2.ts:95:5)
at drawArray (file3.ts:178:5)
at render (file3.ts:155:5)
at game (file4.ts:39:5)
Using the source map to navigate the source code by hand, I can reconstruct the original call stack:
TypeError: Cannot read properties of undefined (reading 'x')
at Render#draw (file1.ts:302:7)
at Render#drawLayer (file1.ts:144:5)
at GameObject#draw (file2.ts:95:5)
at Game#render (file3.ts:178:5)
at Game#reflowCanvas (file3.ts:155:5)
at [anonymous function passed to addEventListener] (file4.ts:39:5)
But doing this required that I dump the mappings and the source files from the source map and manually inspect the source files. In general, this requires that I use a library to parse the mappings
field from Source Maps (because as of Source Maps v3, this field is stored in a stateful way).
💻 Use Cases
I want to use this to improve in-production debugging experiences, specifically, to improve stack traces, particularly those that exist after minification.
Presently, to work around this, we need to do one of two things:
- Manually reconstruct the stack by inspecting the source contents (as mentioned above)
- Or, by writing code that "guesses" the function name by walking backwards from the location the Mapping produces. (This is the mechanism that stacktrace-gps uses, although it often doesn't work for TypeScript sources because it doesn't recognize type annotations).
Proposed changes to the Source Map spec
Substantively: Only the mappings
field would be altered, and would be altered by adding the 6th field. The complete section is included below:
The “mappings” data is broken down as follows:
- each group representing a line in the generated file is separated by a ”;”
- each segment is separated by a “,”
- each segment is made up of 1,4
or 5, 5, or 6 variable length fields.The fields in each segment are:
- The zero-based starting column of the line in the generated code that the segment represents. If this is the first field of the first segment, or the first segment following a new generated line (“;”), then this field holds the whole base 64 VLQ. Otherwise, this field contains a base 64 VLQ that is relative to the previous occurrence of this field. Note that this is different than the fields below because the previous value is reset after every generated line.
- If present, an zero-based index into the “sources” list. This field is a base 64 VLQ relative to the previous occurrence of this field, unless this is the first occurrence of this field, in which case the whole value is represented.
- If present, the zero-based starting line in the original source represented. This field is a base 64 VLQ relative to the previous occurrence of this field, unless this is the first occurrence of this field, in which case the whole value is represented. Always present if there is a source field.
- If present, the zero-based starting column of the line in the source represented. This field is a base 64 VLQ relative to the previous occurrence of this field, unless this is the first occurrence of this field, in which case the whole value is represented. Always present if there is a source field.
- If present, the zero-based index into the “names” list associated with this segment. This field is a base 64 VLQ relative to the previous occurrence of this field, unless this is the first occurrence of this field, in which case the whole value is represented.
6. If present, the zero-based index into the "names" list associated with the call stack of this segment. This field is a base 64 VLQ relative to the previous occurrence of this field, unless this is the first occurrence of this field, in which case the whole value is represented.
In addition, the version
field of the spec should be bumped to 4.
Suggested names
- If functions are named directly, they should be preserved (
function foo() { ... }
,const foo = () => { ... }
, { foo: function() { ... }` - If the function name cannot be deduced from an assignment, it should either be
[anonymous function]
or[anonymous arrow function]
- If the function appears to be passed as a callback, and the expression being called can be identified, its name should be used:
[anonymous function passed to addEventListener]
- If the area is not contained within a function, it should be the name
Global code