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>