github.com/evanw/esbuild@v0.21.4/scripts/graph-debugger.html (about) 1 <!DOCTYPE html> 2 <html> 3 4 <head> 5 <meta charset="utf8"> 6 <title>Graph debugger</title> 7 <style> 8 body { 9 margin: 0; 10 overflow: hidden; 11 font: 14px/20px sans-serif; 12 background: #fff; 13 color: #000; 14 } 15 16 @media (prefers-color-scheme: dark) { 17 body { 18 background: #222; 19 color: #ddd; 20 } 21 } 22 23 canvas { 24 position: fixed; 25 left: 0; 26 top: 0; 27 width: 100%; 28 height: 100%; 29 } 30 31 #instructions { 32 padding: 20px; 33 } 34 35 </style> 36 </head> 37 38 <body> 39 <div id="instructions"> 40 This is a debugger for some of esbuild's internals. To use: 41 <ol> 42 <li>Set "debugVerboseMetafile = true"</li> 43 <li>Serve the esbuild repo over localhost</li> 44 <li>Visit this page with "#metafile=path/to/metafile.json"</li> 45 </ol> 46 </div> 47 <canvas></canvas> 48 <script type="module"> 49 50 const lightColors = { 51 bg: '#fff', 52 fg: '#000', 53 accent: '#7BF', 54 } 55 56 const darkColors = { 57 bg: '#222', 58 fg: '#ddd', 59 accent: '#FB0', 60 } 61 62 const prefersColorSchemeDark = matchMedia("(prefers-color-scheme: dark)") 63 64 function lightOrDarkColors() { 65 return prefersColorSchemeDark.matches ? darkColors : lightColors 66 } 67 68 const paddingX = 10 69 const paddingY = 10 70 71 class InputFile { 72 constructor(source, data) { 73 this.source = source 74 this.data = data 75 this.x = 0 76 this.y = 0 77 this.w = 0 78 this.h = 0 79 this.measure() 80 } 81 82 measure() { 83 this.w = 0 84 this.h = 0 85 86 // Title 87 c.font = titleFont 88 this.w = Math.max(this.w, c.measureText(this.source).width) 89 this.h += titleLineHeight 90 91 // Parts 92 let prevIndent = 0 93 c.font = codeFont 94 for (const part of this.data.parts) { 95 let code = part.code.replace(/\t/g, ' ') 96 const lastNewline = code.lastIndexOf('\n') 97 let nextIndent = 0 98 if (lastNewline >= 0) { 99 const lastLine = code.slice(lastNewline + 1) 100 nextIndent = lastLine.length 101 if (!/\S/.test(lastLine)) code = code.slice(0, lastNewline) 102 } 103 code = ' '.repeat(prevIndent) + code 104 prevIndent = nextIndent 105 part.lines = code.split('\n') 106 part.y = this.h 107 for (const line of part.lines) { 108 this.w = Math.max(this.w, c.measureText(line).width) 109 this.h += codeLineHeight 110 } 111 if (part.nsExportPartIndex) { 112 this.w = Math.max(this.w, c.measureText('/* <nsExportPartIndex> */').width) 113 } 114 if (part.wrapperPartIndex) { 115 this.w = Math.max(this.w, c.measureText('/* <wrapperPartIndex> */').width) 116 } 117 part.h = this.h - part.y 118 } 119 120 this.w += paddingX * 2 121 this.h += paddingY * 2 122 } 123 124 render() { 125 const colors = lightOrDarkColors() 126 127 // Background 128 c.clearRect(this.x, this.y, this.w, this.h) 129 130 // Title 131 c.font = titleFont 132 c.textBaseline = 'middle' 133 c.fillStyle = colors.fg 134 c.fillText(this.source, this.x + paddingX, this.y + paddingY + titleLineHeight / 2) 135 136 // Lines 137 c.font = codeFont 138 c.textBaseline = 'middle' 139 c.fillStyle = colors.fg 140 for (const part of this.data.parts) { 141 c.globalAlpha = part.isLive ? 1 : 0.2 142 for (let i = 0; i < part.lines.length; i++) { 143 c.fillText(part.lines[i], this.x + paddingX, this.y + paddingY + part.y + i * codeLineHeight + codeLineHeight / 2) 144 } 145 if (part.nsExportPartIndex) { 146 c.fillText('/* <nsExportPartIndex> */', this.x + paddingX, this.y + paddingY + part.y + codeLineHeight / 2) 147 } 148 if (part.wrapperPartIndex) { 149 c.fillText('/* <wrapperPartIndex> */', this.x + paddingX, this.y + paddingY + part.y + codeLineHeight / 2) 150 } 151 } 152 153 // Border 154 c.globalAlpha = 0.2 155 c.strokeStyle = colors.fg 156 c.strokeRect(this.x, this.y, this.w, this.h) 157 c.globalAlpha = 1 158 } 159 160 renderHover() { 161 const colors = lightOrDarkColors() 162 163 if (this.hoveredPart === -1) { 164 c.fillStyle = colors.accent 165 c.globalAlpha = 0.2 166 c.fillRect(this.x, this.y + paddingY, this.w, titleLineHeight) 167 c.globalAlpha = 1 168 c.fillRect(this.x, this.y + paddingY, 4, titleLineHeight) 169 170 c.strokeStyle = colors.fg 171 c.fillStyle = colors.fg 172 for (const part of this.data.parts) { 173 if (part.canBeRemovedIfUnused) continue 174 drawArrow( 175 this.x, this.y + paddingY + titleLineHeight / 2, -1, 176 this.x, this.y + paddingY + part.y + codeLineHeight / 2, -1, 177 ) 178 } 179 } else if (this.hoveredPart !== null) { 180 const part = this.data.parts[this.hoveredPart] 181 c.fillStyle = colors.accent 182 c.globalAlpha = 0.2 183 c.fillRect(this.x, this.y + paddingY + part.y, this.w, part.h) 184 c.globalAlpha = 1 185 c.fillRect(this.x, this.y + paddingY + part.y, 4, part.h) 186 187 c.strokeStyle = colors.fg 188 c.fillStyle = colors.fg 189 drawArrow( 190 this.x, this.y + paddingY + part.y + codeLineHeight / 2, -1, 191 this.x, this.y + paddingY + titleLineHeight / 2, -1, 192 ) 193 194 for (const dep of part.dependencies) { 195 if (dep.source === this.source) { 196 const otherPart = this.data.parts[dep.partIndex] 197 drawArrow( 198 this.x, this.y + paddingY + part.y + codeLineHeight / 2, -1, 199 this.x, this.y + paddingY + otherPart.y + codeLineHeight / 2, -1, 200 ) 201 continue 202 } 203 204 const otherFile = inputFiles.find(file => file.source === dep.source) 205 if (!otherFile) continue 206 const otherPart = otherFile.data.parts[dep.partIndex] 207 drawArrow( 208 this.x + this.w, this.y + paddingY + part.y + codeLineHeight / 2, 1, 209 otherFile.x, otherFile.y + paddingY + otherPart.y + codeLineHeight / 2, -1, 210 ) 211 } 212 213 for (const record of part.importRecords) { 214 const otherFile = inputFiles.find(file => file.source === record.source) 215 if (!otherFile) continue 216 drawArrow( 217 this.x + this.w, this.y + paddingY + part.y + codeLineHeight / 2, 1, 218 otherFile.x, otherFile.y + paddingY + titleLineHeight / 2, -1, 219 ) 220 } 221 222 let lines = [] 223 lines.push(`isLive: ${part.isLive}`) 224 if (part.declaredSymbols.length > 0) { 225 lines.push(`declaredSymbols:`) 226 for (const declSym of part.declaredSymbols) { 227 lines.push(` ${declSym.name}`) 228 } 229 } 230 if (part.symbolUses.length > 0) { 231 lines.push(`symbolUses:`) 232 for (const use of part.symbolUses) { 233 lines.push(` ${use.name} ${use.countEstimate}x`) 234 } 235 } 236 if (part.importRecords.length > 0) { 237 lines.push(`importRecords:`) 238 for (const record of part.importRecords) { 239 lines.push(` ${record.source}`) 240 } 241 } 242 243 c.font = normalFont 244 c.textBaseline = 'middle' 245 c.fillStyle = colors.fg 246 for (let i = 0; i < lines.length; i++) { 247 c.fillText(lines[i], this.x + 10, this.y + this.h + 10 + i * normalLineHeight + normalLineHeight / 2) 248 } 249 } 250 } 251 252 hoveredPart = null 253 254 onHover(mouseX, mouseY) { 255 this.hoveredPart = null 256 257 if (mouseX !== null && mouseY !== null && 258 mouseX >= this.x && mouseX < this.x + this.w && 259 mouseY >= this.y && mouseY < this.y + this.h) { 260 let y = mouseY - this.y - paddingY 261 262 if (y >= 0 && y < titleLineHeight) { 263 this.hoveredPart = -1 264 return true 265 } 266 267 for (let i = 0; i < this.data.parts.length; i++) { 268 const part = this.data.parts[i] 269 if (y >= part.y && y < part.y + part.h) { 270 this.hoveredPart = i 271 return true 272 } 273 } 274 } 275 } 276 277 onMouseMove(mouseX, mouseY) { 278 if (mouseX >= this.x && mouseX < this.x + this.w && 279 mouseY >= this.y && mouseY < this.y + this.h) { 280 document.body.style.cursor = 'move' 281 return true 282 } 283 } 284 285 oldX = 0 286 oldY = 0 287 288 onMouseDown(mouseX, mouseY) { 289 if (mouseX >= this.x && mouseX < this.x + this.w && 290 mouseY >= this.y && mouseY < this.y + this.h) { 291 this.oldX = mouseX 292 this.oldY = mouseY 293 document.body.style.cursor = 'move' 294 return true 295 } 296 } 297 298 onMouseDrag(mouseX, mouseY) { 299 this.x += mouseX - this.oldX 300 this.y += mouseY - this.oldY 301 this.oldX = mouseX 302 this.oldY = mouseY 303 document.body.style.cursor = 'move' 304 } 305 306 onMouseUp(e) { 307 } 308 } 309 310 function drawArrow(ax, ay, adx, bx, by, bdx) { 311 let dx = bx - ax 312 let dy = by - ay 313 let d = Math.sqrt(dx * dx + dy * dy) 314 let scale = d / 2 315 c.beginPath() 316 c.moveTo(ax, ay) 317 c.bezierCurveTo( 318 ax + adx * (10 + scale), ay, 319 bx + bdx * 10 + bdx * scale, by, 320 bx + bdx * 10, by, 321 ) 322 c.stroke() 323 c.beginPath() 324 c.moveTo(bx, by) 325 c.lineTo(bx + bdx * 10, by - 5) 326 c.lineTo(bx + bdx * 10, by + 5) 327 c.fill() 328 } 329 330 const canvas = document.querySelector('canvas') 331 const c = canvas.getContext('2d') 332 const titleFont = '20px sans-serif' 333 const titleLineHeight = 30 334 const codeFont = '12px monospace' 335 const codeLineHeight = 18 336 const normalFont = '14px sans-serif' 337 const normalLineHeight = 18 338 let width = 0, height = 0 339 let scrollX = 0, scrollY = 0 340 341 let metafile 342 const instructions = document.getElementById('instructions') 343 try { 344 if (!location.hash.startsWith('#metafile=')) throw new Error('Expected "#metafile=" in URL') 345 metafile = await fetch(location.hash.slice('#metafile='.length)).then(r => r.json()) 346 } catch (err) { 347 const error = document.createElement('div') 348 error.style.color = 'red' 349 error.textContent = err 350 instructions.append(error) 351 throw err 352 } 353 instructions.remove() 354 355 const outputSource = Object.keys(metafile.outputs)[0] 356 const output = metafile.outputs[outputSource] 357 const inputFiles = Object.entries(output.inputs).map(([source, data]) => new InputFile(source, data)).reverse() 358 359 for (let i = 0; i < inputFiles.length; i++) { 360 const file = inputFiles[i] 361 file.y = 100 362 if (i === 0) { 363 file.x = 100 364 } else { 365 const prevFile = inputFiles[i - 1] 366 file.x = prevFile.x + prevFile.w + 100 367 } 368 } 369 370 function render() { 371 const colors = lightOrDarkColors() 372 373 width = innerWidth 374 height = innerHeight 375 const ratio = devicePixelRatio 376 canvas.width = Math.round(width * ratio) 377 canvas.height = Math.round(height * ratio) 378 canvas.style.background = colors.bg 379 c.scale(ratio, ratio) 380 381 // Title 382 c.font = titleFont 383 c.textBaseline = 'top' 384 c.fillStyle = colors.fg 385 c.fillText(outputSource, 10, 10) 386 387 // Content 388 c.translate(-scrollX, -scrollY) 389 390 // Inputs 391 for (let i = inputFiles.length - 1; i >= 0; i--) inputFiles[i].render() 392 for (let i = inputFiles.length - 1; i >= 0; i--) inputFiles[i].renderHover() 393 } 394 395 addEventListener('wheel', e => { 396 e.preventDefault() 397 if (e.ctrlKey) return 398 scrollX += e.deltaX 399 scrollY += e.deltaY 400 }, { passive: false }) 401 402 let draggingFile = null 403 let isDragging = false 404 405 onmousemove = e => { 406 let mouseX = e.pageX + scrollX 407 let mouseY = e.pageY + scrollY 408 document.body.style.cursor = 'default' 409 if (isDragging) { 410 if (draggingFile) { 411 draggingFile.onMouseDrag(mouseX, mouseY) 412 } 413 } else { 414 for (const file of inputFiles) { 415 if (file.onMouseMove(mouseX, mouseY)) { 416 break 417 } 418 } 419 onhover(mouseX, mouseY) 420 } 421 } 422 423 onmousedown = e => { 424 let mouseX = e.pageX + scrollX 425 let mouseY = e.pageY + scrollY 426 if (!isDragging) { 427 isDragging = true 428 for (const file of inputFiles) { 429 if (file.onMouseDown(mouseX, mouseY)) { 430 draggingFile = file 431 break 432 } 433 } 434 } 435 onhover(mouseX, mouseY) 436 } 437 438 onmouseup = e => { 439 let mouseX = e.pageX + scrollX 440 let mouseY = e.pageY + scrollY 441 if (isDragging) { 442 if (draggingFile) { 443 draggingFile.onMouseUp(mouseX, mouseY) 444 draggingFile = null 445 } 446 isDragging = false 447 } 448 onhover(mouseX, mouseY) 449 } 450 451 function onhover(mouseX, mouseY) { 452 for (const file of inputFiles) { 453 if (file.onHover(mouseX, mouseY)) { 454 mouseX = null 455 mouseY = null 456 } 457 } 458 } 459 460 onblur = () => { 461 draggingFile = null 462 isDragging = false 463 } 464 465 function tick() { 466 requestAnimationFrame(tick) 467 render() 468 } 469 470 tick() 471 472 </script> 473 </body> 474 475 </html>