Skip to content

Instantly share code, notes, and snippets.

@hyrious
Last active June 17, 2025 02:42
Show Gist options
  • Save hyrious/b8fcf2c525de1800113af3d4ee883709 to your computer and use it in GitHub Desktop.
Save hyrious/b8fcf2c525de1800113af3d4ee883709 to your computer and use it in GitHub Desktop.
/// <reference types="vite/client" />
// The contents of index.html:
// <script src="./node_modules/source-map/dist/source-map.js"></script>
// <script type="module" src="/using-source-map-in-browser.ts"></script>
import * as esbuild from 'esbuild-wasm/esm/browser.js'
import wasmURL from 'esbuild-wasm/esbuild.wasm?url'
import sourceMapWasmURL from 'source-map/lib/mappings.wasm?url'
declare const sourceMap: any // The source-map lib can only be initialized outside :/
sourceMap.SourceMapConsumer.initialize({ 'lib/mappings.wasm': sourceMapWasmURL })
const $ = document.createElement.bind(document) as typeof document.createElement
const $textarea = document.body.appendChild($('div'))
$textarea.style.font = '13px/1.22 monospace'
$textarea.style.whiteSpace = 'pre'
const logToTextarea = (s: string, br = true) => {
$textarea.appendChild(document.createTextNode(br ? s + '\n' : s))
}
const fakeFileName = 'virtual://main.js'
addEventListener('error', ev => {
if (ev.filename == fakeFileName) {
ev.preventDefault()
logError(ev.error)
}
})
logToTextarea('initializing... ', false)
let t = Date.now()
await esbuild.initialize({ wasmURL })
await esbuild.transform('let a = 1')
logToTextarea(`done in ${Date.now() - t}ms`)
let filename = 'hello.ts'
let contents = 'throw new Error( "hello TypeScript" as null!)'
let { code, map } = await esbuild.transform(contents, {
loader: 'ts',
sourcefile: filename,
sourcemap: true
})
let consumer = await new sourceMap.SourceMapConsumer(map)
let blob = new Blob([code, `//# sourceURL=${fakeFileName}`], { type: 'application/javascript' })
let url = URL.createObjectURL(blob)
let $script = $('script')
$script.src = url
document.body.appendChild($script)
// ref: https://github.com/evanw/node-source-map-support/blob/7b5b81/source-map-support.js#L485
function logError(error: Error) {
const stackRegex = new RegExp(`^\\s+at ${fakeFileName.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')}:(\\d+):(\\d+)`, 'gm')
const replace: [start: number, length: number, content: string][] = []
const debug: [line: number, column: number][] = []
let stack = error.stack || ''
let match: RegExpMatchArray | null = null
while ((match = stackRegex.exec(stack))) {
// Location in the final code, will be transformed to the source contents
let line = +match[1]
let column = +match[2]
let source: string
({ source, line, column } = consumer.originalPositionFor({ source: fakeFileName, line, column }))
replace.push([match.index!, match[0].length, ` at ${source}:${line}:${column + 1}`])
debug.push([line, column + 1])
}
let offset = 0
for (let [i, len, text] of replace) {
stack = stack.slice(0, i + offset) + text + stack.slice(i + len + offset)
offset += text.length - len
}
logToTextarea('')
logToTextarea(stack)
// Debug -- show sources
// Better using a formatter, like https://babeljs.io/docs/babel-code-frame
logToTextarea('')
logToTextarea(`// File: ${filename}`)
contents.split(/\r?\n/g).forEach((line, i) => {
logToTextarea(`${i + 1}: ${line}`)
let columns = debug.filter(([l]) => l === i + 1).map(([_, c]) => c)
if (columns.length) {
let cs = Array(columns.reduce((a, b) => Math.max(a, b))).fill(' ')
columns.forEach(c => cs[c - 1] = '^')
logToTextarea(' ' + cs.join(''))
}
})
}
@hyrious
Copy link
Author

hyrious commented Jun 17, 2025

Demo output:

initializing... done in 122ms

Error: hello TypeScript
    at hello.ts:1:8

// File: hello.ts
1: throw  new Error( "hello TypeScript" as null!)
          ^

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