Recursive AI

Point reports at the original source with source maps

All oneecma recipes

Run minified or bundled JavaScript with OneEcma and turn its error report back into one about the TypeScript you wrote: the location, the source line and the whole stack move to the original files through the bundle’s ECMA-426 source map.

The TypeScript, src/cart.ts and src/app.ts:

TS
interface Line { sku: string; qty: number; price: number }
interface Cart { id: string; lines: Line[] }
 
export function cartTotal(cart: Cart): number {
  return cart.lines.reduce((sum, line) => sum + line.qty * line.price, 0);
}
TS
import { cartTotal } from "./cart";
 
const saved = JSON.parse('{"id":"c1","items":[{"sku":"pen","qty":3,"price":1.5}]}');
(globalThis as any).total = cartTotal(saved);

Bundled and minified with esbuild 0.25.11 (esbuild src/app.ts --bundle --minify --format=iife --sourcemap --outfile=dist/app.js), dist/app.js is:

JS
(()=>{function r(t){return t.lines.reduce((i,e)=>i+e.qty*e.price,0)}var n=JSON.parse('{"id":"c1","items":[{"sku":"pen","qty":3,"price":1.5}]}');globalThis.total=r(n);})();
//# sourceMappingURL=app.js.map

The host runs the bundle and remaps the report:

C#
using RecursiveAI.OneEcma;
using RecursiveAI.OneEcma.Diagnostics;
using RecursiveAI.OneEcma.SourceMaps;
using RecursiveAI.OneEcma.Text;
 
string code = File.ReadAllText("web/dist/app.js");
var engine = new ScriptEngine();
try
{
    engine.Evaluate(code, "dist/app.js");
}
catch (JsException e)
{
    Console.WriteLine("--- as it ran");
    Console.WriteLine(DiagnosticRenderer.Render(e.Report!));
 
    // The engine reads no files: the host fetches the map the sourceMappingURL comment names.
    var generated = new SourceText(code, "dist/app.js");
    SourceMapParseResult? loaded = SourceMapLoader.Load(generated, url => File.ReadAllText(Path.Combine("web", url)));
    if (loaded?.Map is SourceMap map)
    {
        Diagnostic original = SourceMapReports.Remap(e.Report!, map);
        Console.WriteLine("--- remapped to the TypeScript");
        Console.WriteLine(DiagnosticRenderer.Render(original));
        Console.WriteLine($"generated {original.Mapping!.Generated.Line}:{original.Mapping.Generated.Column} -> original {original.Mapping.Original.Source}:{original.Mapping.Original.Line}:{original.Mapping.Original.Column}");
    }
}
Text
--- as it ran
error[TypeError]: Cannot read properties of undefined (reading 'reduce')
 --> dist/app.js:1:30
  |
1 | …{function r(t){return t.lines.reduce((i,e)=>i+e.qty*e.price,0)}var n=JSON.parse('{"id":"c1","items":[{"sku":"pen","qty":3,"price":1.5}]}');g…
  |                          ^^^^^ `t.lines` is undefined
  |
  = help: did you mean `t.items`? (Array, 1 item)
  = stack: r (dist/app.js:1) ← <anonymous> (dist/app.js:1) ← <top-level> (dist/app.js:1)
 
--- remapped to the TypeScript
error[TypeError]: Cannot read properties of undefined (reading 'reduce')
 --> src/cart.ts:5:15
  |
5 |   return cart.lines.reduce((sum, line) => sum + line.qty * line.price, 0);
  |               ^^^^^ `t.lines` is undefined
  |
  = help: did you mean `t.items`? (Array, 1 item) (in the generated code)
  = note: generated position: dist/app.js:1:30
  = stack: r (src/cart.ts:5) ← <anonymous> (src/app.ts:4) ← <top-level> (dist/app.js:1)
 
generated 1:30 -> original src/cart.ts:5:15

SourceMapReports.Remap moved the primary span and every stack frame it could map: r is in src/cart.ts and its caller in src/app.ts. The suggestion and the label keep the generated names (t.lines, t.items), because they were computed on the code that ran, so the help line says “(in the generated code)”. The map has sourcesContent, so the report quotes the TypeScript line itself.

How the map is found

  • SourceMapLoader.Load reads the //# sourceMappingURL= comment at the end of the generated code. A data: URL is decoded on the spot. Any other URL is resolved against the generated file’s name and passed to your fetcher, because the engine reads no files and opens no connections.
  • SourceMap.Parse(json) takes a map you already have. It never throws on bad input: problems come back as warnings you can render like any report, and a half-broken map still places most errors.
  • Without sourcesContent, pass a third argument to Remap that returns an original file’s text by URL. When no text is available the report keeps the generated source and adds original position: file:line:col.

Good to know

  • Diagnostic.Mapping records both places, and the JSON report carries them as mapping (see the JSON report).
  • oneecma check follows a map comment on its own, or takes --source-map <file>, for syntax errors in generated code.
  • Index maps with sections, sourceRoot, ignoreList and the decoding limits are covered in source maps.