mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
14 Commits
claude/fix
...
claude/gra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3282981fcf | ||
|
|
6c1466385f | ||
|
|
c275a93bdb | ||
|
|
3f97345088 | ||
|
|
2c76947aac | ||
|
|
7186669efc | ||
|
|
ed22a78c37 | ||
|
|
3aaa0e15f3 | ||
|
|
6c5a063813 | ||
|
|
55820bec90 | ||
|
|
5b8e1b61dd | ||
|
|
53f6a137aa | ||
|
|
e2ef1692f9 | ||
|
|
bc32ddfbd3 |
@@ -4,6 +4,7 @@ pub const LinkerContext = struct {
|
||||
|
||||
pub const OutputFileListBuilder = @import("./linker_context/OutputFileListBuilder.zig");
|
||||
pub const StaticRouteVisitor = @import("./linker_context/StaticRouteVisitor.zig");
|
||||
pub const GraphVisualizer = @import("./graph_visualizer.zig").GraphVisualizer;
|
||||
|
||||
parse_graph: *Graph = undefined,
|
||||
graph: LinkerGraph = undefined,
|
||||
@@ -401,6 +402,13 @@ pub const LinkerContext = struct {
|
||||
}
|
||||
|
||||
try this.scanImportsAndExports();
|
||||
|
||||
// Dump graph state after scan
|
||||
if (comptime Environment.isDebug) {
|
||||
GraphVisualizer.dumpGraphState(this, "after_scan", null) catch |err| {
|
||||
debug("Failed to dump graph after scan: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
// Stop now if there were errors
|
||||
if (this.log.hasErrors()) {
|
||||
@@ -418,6 +426,13 @@ pub const LinkerContext = struct {
|
||||
}
|
||||
|
||||
const chunks = try this.computeChunks(bundle.unique_key);
|
||||
|
||||
// Dump graph state after computing chunks
|
||||
if (comptime Environment.isDebug) {
|
||||
GraphVisualizer.dumpGraphState(this, "after_chunks", chunks) catch |err| {
|
||||
debug("Failed to dump graph after chunks: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (this.log.hasErrors()) {
|
||||
return error.BuildFailed;
|
||||
@@ -428,12 +443,26 @@ pub const LinkerContext = struct {
|
||||
}
|
||||
|
||||
try this.computeCrossChunkDependencies(chunks);
|
||||
|
||||
// Dump graph state after computing dependencies
|
||||
if (comptime Environment.isDebug) {
|
||||
GraphVisualizer.dumpGraphState(this, "after_compute", chunks) catch |err| {
|
||||
debug("Failed to dump graph after compute: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (comptime FeatureFlags.help_catch_memory_issues) {
|
||||
this.checkForMemoryCorruption();
|
||||
}
|
||||
|
||||
this.graph.symbols.followAll();
|
||||
|
||||
// Final dump after linking
|
||||
if (comptime Environment.isDebug) {
|
||||
GraphVisualizer.dumpGraphState(this, "after_link", chunks) catch |err| {
|
||||
debug("Failed to dump graph after link: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
757
src/bundler/code_flow_visualizer.html
Normal file
757
src/bundler/code_flow_visualizer.html
Normal file
@@ -0,0 +1,757 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bun Bundler Code Flow Visualizer</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/material-darker.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#header {
|
||||
background: #161b22;
|
||||
border-bottom: 1px solid #30363d;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#controls button, #controls select {
|
||||
background: #21262d;
|
||||
color: #c9d1d9;
|
||||
border: 1px solid #30363d;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#controls button:hover, #controls select:hover {
|
||||
background: #30363d;
|
||||
border-color: #8b949e;
|
||||
}
|
||||
|
||||
#stage-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#stage-selector label {
|
||||
font-size: 14px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.code-pane:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.pane-header {
|
||||
background: #161b22;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #30363d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pane-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-selector {
|
||||
margin-left: auto;
|
||||
background: #21262d;
|
||||
color: #c9d1d9;
|
||||
border: 1px solid #30363d;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: 13px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
/* Symbol highlights */
|
||||
.symbol-highlight {
|
||||
position: relative;
|
||||
background: rgba(139, 148, 158, 0.15);
|
||||
border-bottom: 2px solid #58a6ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.symbol-renamed {
|
||||
background: rgba(251, 143, 68, 0.15);
|
||||
border-bottom: 2px solid #fb8f44;
|
||||
}
|
||||
|
||||
.symbol-removed {
|
||||
background: rgba(248, 81, 73, 0.15);
|
||||
border-bottom: 2px solid #f85149;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.symbol-added {
|
||||
background: rgba(63, 185, 80, 0.15);
|
||||
border-bottom: 2px solid #3fb950;
|
||||
}
|
||||
|
||||
/* Flow arrows overlay */
|
||||
#flow-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
stroke: #58a6ff;
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.flow-line.import {
|
||||
stroke: #a371f7;
|
||||
}
|
||||
|
||||
.flow-line.export {
|
||||
stroke: #3fb950;
|
||||
}
|
||||
|
||||
.flow-line.renamed {
|
||||
stroke: #fb8f44;
|
||||
}
|
||||
|
||||
.flow-arrow {
|
||||
fill: #58a6ff;
|
||||
}
|
||||
|
||||
/* Symbol info panel */
|
||||
#symbol-panel {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
width: 400px;
|
||||
max-height: 600px;
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
display: none;
|
||||
z-index: 1001;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#symbol-panel h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: #f0f6fc;
|
||||
}
|
||||
|
||||
.symbol-info {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.symbol-info-row {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.symbol-info-label {
|
||||
color: #8b949e;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.symbol-info-value {
|
||||
color: #c9d1d9;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
/* Stage diff panel */
|
||||
#diff-panel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 200px;
|
||||
background: #0d1117;
|
||||
border-top: 1px solid #30363d;
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.diff-header {
|
||||
background: #161b22;
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #30363d;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.diff-content {
|
||||
padding: 15px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.diff-item {
|
||||
margin: 10px 0;
|
||||
padding: 8px;
|
||||
background: #161b22;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.diff-added {
|
||||
border-left: 3px solid #3fb950;
|
||||
}
|
||||
|
||||
.diff-removed {
|
||||
border-left: 3px solid #f85149;
|
||||
}
|
||||
|
||||
.diff-modified {
|
||||
border-left: 3px solid #fb8f44;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
#loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 18px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
/* Split handle */
|
||||
.split-handle {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: #30363d;
|
||||
cursor: col-resize;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.split-handle:hover {
|
||||
background: #58a6ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1>🔍 Code Flow Visualizer</h1>
|
||||
<div id="stage-selector">
|
||||
<label>Stage:</label>
|
||||
<select id="stage-select">
|
||||
<option value="after_scan">After Scan</option>
|
||||
<option value="after_compute">After Compute</option>
|
||||
<option value="after_chunks">After Chunks</option>
|
||||
<option value="after_link">After Link</option>
|
||||
<option value="after_generation">After Generation (with output)</option>
|
||||
</select>
|
||||
<button id="compare-btn">📊 Compare Stages</button>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<input type="file" id="file-input" accept=".json" multiple style="display: none;">
|
||||
<button onclick="document.getElementById('file-input').click()">📁 Load Dumps</button>
|
||||
<button id="show-symbols">🔤 Symbols</button>
|
||||
<button id="show-imports">📥 Imports</button>
|
||||
<button id="show-exports">📤 Exports</button>
|
||||
<button id="show-renames">✏️ Renames</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-container">
|
||||
<div id="loading">Loading visualizer...</div>
|
||||
|
||||
<div class="code-pane" id="source-pane" style="display: none;">
|
||||
<div class="pane-header">
|
||||
<span class="pane-title">📄 Source Code</span>
|
||||
<select class="file-selector" id="source-file-select"></select>
|
||||
</div>
|
||||
<div class="code-container">
|
||||
<textarea id="source-editor"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split-handle" style="display: none;"></div>
|
||||
|
||||
<div class="code-pane" id="output-pane" style="display: none;">
|
||||
<div class="pane-header">
|
||||
<span class="pane-title">📦 Output Code</span>
|
||||
<select class="file-selector" id="output-file-select"></select>
|
||||
</div>
|
||||
<div class="code-container">
|
||||
<textarea id="output-editor"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg id="flow-overlay"></svg>
|
||||
|
||||
<div id="symbol-panel">
|
||||
<h3>Symbol Information</h3>
|
||||
<div class="symbol-info"></div>
|
||||
<div id="duplicate-exports" style="margin-top: 20px;"></div>
|
||||
<div id="symbol-chains" style="margin-top: 20px;"></div>
|
||||
<div id="export-details" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="diff-panel">
|
||||
<div class="diff-header">Stage Differences</div>
|
||||
<div class="diff-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let sourceEditor = null;
|
||||
let outputEditor = null;
|
||||
let graphData = {};
|
||||
let currentStage = 'after_generation'; // Default to stage with output
|
||||
let currentSourceFile = 0;
|
||||
let currentOutputFile = 0;
|
||||
let symbolMappings = [];
|
||||
let showSymbols = true;
|
||||
let showImports = true;
|
||||
let showExports = true;
|
||||
let showRenames = true;
|
||||
|
||||
// Initialize CodeMirror editors
|
||||
function initEditors() {
|
||||
sourceEditor = CodeMirror.fromTextArea(document.getElementById('source-editor'), {
|
||||
mode: 'javascript',
|
||||
theme: 'material-darker',
|
||||
lineNumbers: true,
|
||||
readOnly: true,
|
||||
lineWrapping: false
|
||||
});
|
||||
|
||||
outputEditor = CodeMirror.fromTextArea(document.getElementById('output-editor'), {
|
||||
mode: 'javascript',
|
||||
theme: 'material-darker',
|
||||
lineNumbers: true,
|
||||
readOnly: true,
|
||||
lineWrapping: false
|
||||
});
|
||||
|
||||
// Show panes
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('source-pane').style.display = 'flex';
|
||||
document.getElementById('output-pane').style.display = 'flex';
|
||||
document.querySelector('.split-handle').style.display = 'block';
|
||||
}
|
||||
|
||||
// Load graph data from files
|
||||
document.getElementById('file-input').addEventListener('change', async (event) => {
|
||||
const files = Array.from(event.target.files);
|
||||
|
||||
for (const file of files) {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
const stage = data.stage;
|
||||
graphData[stage] = data;
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// Update UI with loaded data
|
||||
function updateUI() {
|
||||
const data = graphData[currentStage];
|
||||
if (!data) return;
|
||||
|
||||
// Update file selectors
|
||||
updateFileSelectors(data);
|
||||
|
||||
// Load source and output code
|
||||
loadSourceCode(data);
|
||||
loadOutputCode(data);
|
||||
|
||||
// Analyze and visualize symbol flow
|
||||
analyzeSymbolFlow(data);
|
||||
visualizeFlow();
|
||||
}
|
||||
|
||||
function updateFileSelectors(data) {
|
||||
const sourceSelect = document.getElementById('source-file-select');
|
||||
const outputSelect = document.getElementById('output-file-select');
|
||||
|
||||
sourceSelect.innerHTML = '';
|
||||
outputSelect.innerHTML = '';
|
||||
|
||||
// Add source files
|
||||
(data.files || []).forEach((file, idx) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = idx;
|
||||
option.textContent = file.path || `File ${idx}`;
|
||||
sourceSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Add output chunks
|
||||
(data.chunks || []).forEach((chunk, idx) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = idx;
|
||||
option.textContent = `Chunk ${idx} (${chunk.final_path || 'unnamed'})`;
|
||||
outputSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function loadSourceCode(data) {
|
||||
const file = data.files?.[currentSourceFile];
|
||||
if (!file) {
|
||||
sourceEditor.setValue('// No source code available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use actual source snippet if available
|
||||
let sourceCode = '';
|
||||
if (file.source_snippet) {
|
||||
sourceCode = `// File: ${file.path}\n// Loader: ${file.loader}\n\n${file.source_snippet}`;
|
||||
} else {
|
||||
sourceCode = `// File: ${file.path}\n// No source code available\n\n// To see source code, ensure BUN_BUNDLER_GRAPH_DUMP=1 is set\n// and the bundler has captured source snippets.`;
|
||||
}
|
||||
|
||||
sourceEditor.setValue(sourceCode);
|
||||
|
||||
// Highlight symbols
|
||||
highlightSourceSymbols(data, file);
|
||||
}
|
||||
|
||||
function loadOutputCode(data) {
|
||||
const chunk = data.chunks?.[currentOutputFile];
|
||||
if (!chunk) {
|
||||
outputEditor.setValue('// No output code available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use actual output snippet if available
|
||||
let outputCode = '';
|
||||
if (chunk.output_snippet) {
|
||||
outputCode = `// Chunk ${chunk.index}\n`;
|
||||
outputCode += `// Entry point: ${chunk.is_entry_point}\n`;
|
||||
outputCode += `// Output path: ${chunk.final_path || 'unknown'}\n\n`;
|
||||
outputCode += chunk.output_snippet;
|
||||
} else {
|
||||
outputCode = `// Chunk ${chunk.index}\n// No output code available\n\n// Output code is only available in the 'after_generation' stage\n// after the bundler has completed code generation.`;
|
||||
}
|
||||
|
||||
outputEditor.setValue(outputCode);
|
||||
|
||||
// Highlight transformed symbols
|
||||
highlightOutputSymbols(data, chunk);
|
||||
}
|
||||
|
||||
|
||||
function highlightSourceSymbols(data, file) {
|
||||
// This would mark symbols in the source code
|
||||
// For real implementation, we'd use CodeMirror's markText
|
||||
}
|
||||
|
||||
function highlightOutputSymbols(data, chunk) {
|
||||
// This would mark transformed symbols in output
|
||||
}
|
||||
|
||||
function analyzeSymbolFlow(data) {
|
||||
symbolMappings = [];
|
||||
|
||||
// Clear panels
|
||||
document.getElementById('duplicate-exports').innerHTML = '';
|
||||
document.getElementById('symbol-chains').innerHTML = '';
|
||||
document.getElementById('export-details').innerHTML = '';
|
||||
|
||||
// Analyze symbol chains for duplicates
|
||||
if (data.symbol_chains && data.symbol_chains.length > 0) {
|
||||
const exportNames = {};
|
||||
data.symbol_chains.forEach(chain => {
|
||||
if (!exportNames[chain.export_name]) {
|
||||
exportNames[chain.export_name] = [];
|
||||
}
|
||||
exportNames[chain.export_name].push({
|
||||
file: chain.source_file,
|
||||
hasConflicts: chain.has_conflicts
|
||||
});
|
||||
});
|
||||
|
||||
// Show duplicate exports prominently
|
||||
const duplicates = Object.entries(exportNames).filter(([_, sources]) => sources.length > 1);
|
||||
if (duplicates.length > 0) {
|
||||
const dupPanel = document.getElementById('duplicate-exports');
|
||||
dupPanel.innerHTML = '<h4 style="color: red;">⚠️ DUPLICATE EXPORTS DETECTED</h4>';
|
||||
duplicates.forEach(([name, sources]) => {
|
||||
dupPanel.innerHTML += `
|
||||
<div style="background: rgba(255,0,0,0.1); padding: 8px; margin: 5px 0; border-left: 3px solid red;">
|
||||
<strong>"${name}"</strong> exported from files: ${sources.map(s => s.file).join(', ')}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Show symbol chains
|
||||
const chainsPanel = document.getElementById('symbol-chains');
|
||||
if (data.symbol_chains.length > 0) {
|
||||
chainsPanel.innerHTML = '<h4>Symbol Resolution Chains</h4>';
|
||||
const chainList = document.createElement('div');
|
||||
chainList.style.cssText = 'max-height: 250px; overflow-y: auto; font-size: 12px;';
|
||||
|
||||
data.symbol_chains.slice(0, 30).forEach(chain => {
|
||||
const isDuplicate = exportNames[chain.export_name]?.length > 1;
|
||||
const chainDiv = document.createElement('div');
|
||||
chainDiv.style.cssText = `
|
||||
margin: 8px 0;
|
||||
padding: 6px;
|
||||
border-left: 3px solid ${chain.has_conflicts || isDuplicate ? 'orange' : '#4a5568'};
|
||||
background: rgba(255,255,255,0.02);
|
||||
`;
|
||||
|
||||
let html = `<strong style="${isDuplicate ? 'color: orange;' : ''}">${chain.export_name}</strong> (file ${chain.source_file})`;
|
||||
if (chain.chain && chain.chain.length > 0) {
|
||||
chain.chain.forEach(link => {
|
||||
const color = link.link_type === 're-export' ? '#9f7aea' :
|
||||
link.link_type === 'import' ? '#4299e1' :
|
||||
'#48bb78';
|
||||
html += `<br> → <span style="color: ${color};">${link.link_type}</span>: ${link.symbol_name} @ file ${link.file_index}`;
|
||||
});
|
||||
}
|
||||
if (chain.has_conflicts) {
|
||||
html += `<br><span style="color: orange;">⚠️ Has conflicts with ${chain.conflict_sources?.length || 0} sources</span>`;
|
||||
}
|
||||
|
||||
chainDiv.innerHTML = html;
|
||||
chainList.appendChild(chainDiv);
|
||||
});
|
||||
|
||||
chainsPanel.appendChild(chainList);
|
||||
}
|
||||
}
|
||||
|
||||
// Show resolved exports with details
|
||||
if (data.imports_and_exports?.resolved_exports) {
|
||||
const resolved = data.imports_and_exports.resolved_exports;
|
||||
const ambiguous = resolved.filter(e => e.potentially_ambiguous);
|
||||
|
||||
if (ambiguous.length > 0) {
|
||||
const exportPanel = document.getElementById('export-details');
|
||||
exportPanel.innerHTML = '<h4 style="color: orange;">Ambiguous Exports</h4>';
|
||||
const ambList = document.createElement('div');
|
||||
ambList.style.cssText = 'max-height: 150px; overflow-y: auto; font-size: 12px;';
|
||||
|
||||
ambiguous.forEach(exp => {
|
||||
ambList.innerHTML += `
|
||||
<div style="background: rgba(255,165,0,0.1); padding: 6px; margin: 4px 0;">
|
||||
<strong>${exp.export_alias}</strong> (file ${exp.source})<br>
|
||||
Target: ${exp.target_source !== null ? `file ${exp.target_source}` : 'unresolved'}<br>
|
||||
Ambiguous: ${exp.ambiguous_count} sources
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
exportPanel.appendChild(ambList);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze regular symbol mappings
|
||||
const symbols = data.symbols?.by_source || [];
|
||||
symbols.forEach(source => {
|
||||
source.symbols?.forEach(symbol => {
|
||||
if (symbol.link) {
|
||||
symbolMappings.push({
|
||||
source: symbol,
|
||||
sourceFile: source.source_index,
|
||||
transformed: symbol.link,
|
||||
type: symbol.kind
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function visualizeFlow() {
|
||||
const svg = d3.select('#flow-overlay');
|
||||
svg.selectAll('*').remove();
|
||||
|
||||
// Draw flow arrows between source and output
|
||||
// This would connect highlighted symbols
|
||||
|
||||
if (!showSymbols) return;
|
||||
|
||||
// For now, just show we're ready to draw
|
||||
console.log('Ready to visualize', symbolMappings.length, 'symbol flows');
|
||||
}
|
||||
|
||||
// Stage selector
|
||||
document.getElementById('stage-select').addEventListener('change', (e) => {
|
||||
currentStage = e.target.value;
|
||||
updateUI();
|
||||
});
|
||||
|
||||
// File selectors
|
||||
document.getElementById('source-file-select').addEventListener('change', (e) => {
|
||||
currentSourceFile = parseInt(e.target.value);
|
||||
loadSourceCode(graphData[currentStage]);
|
||||
});
|
||||
|
||||
document.getElementById('output-file-select').addEventListener('change', (e) => {
|
||||
currentOutputFile = parseInt(e.target.value);
|
||||
loadOutputCode(graphData[currentStage]);
|
||||
});
|
||||
|
||||
// Toggle buttons
|
||||
document.getElementById('show-symbols').addEventListener('click', () => {
|
||||
showSymbols = !showSymbols;
|
||||
document.getElementById('show-symbols').style.opacity = showSymbols ? '1' : '0.5';
|
||||
visualizeFlow();
|
||||
});
|
||||
|
||||
document.getElementById('show-imports').addEventListener('click', () => {
|
||||
showImports = !showImports;
|
||||
document.getElementById('show-imports').style.opacity = showImports ? '1' : '0.5';
|
||||
visualizeFlow();
|
||||
});
|
||||
|
||||
document.getElementById('show-exports').addEventListener('click', () => {
|
||||
showExports = !showExports;
|
||||
document.getElementById('show-exports').style.opacity = showExports ? '1' : '0.5';
|
||||
visualizeFlow();
|
||||
});
|
||||
|
||||
document.getElementById('show-renames').addEventListener('click', () => {
|
||||
showRenames = !showRenames;
|
||||
document.getElementById('show-renames').style.opacity = showRenames ? '1' : '0.5';
|
||||
visualizeFlow();
|
||||
});
|
||||
|
||||
// Compare stages
|
||||
document.getElementById('compare-btn').addEventListener('click', () => {
|
||||
const diffPanel = document.getElementById('diff-panel');
|
||||
diffPanel.style.display = diffPanel.style.display === 'none' ? 'block' : 'none';
|
||||
|
||||
if (diffPanel.style.display === 'block') {
|
||||
compareStages();
|
||||
}
|
||||
});
|
||||
|
||||
function compareStages() {
|
||||
const stages = Object.keys(graphData).sort();
|
||||
if (stages.length < 2) return;
|
||||
|
||||
const diffContent = document.querySelector('.diff-content');
|
||||
diffContent.innerHTML = '';
|
||||
|
||||
for (let i = 1; i < stages.length; i++) {
|
||||
const prev = graphData[stages[i-1]];
|
||||
const curr = graphData[stages[i]];
|
||||
|
||||
const diff = document.createElement('div');
|
||||
diff.className = 'diff-item';
|
||||
diff.innerHTML = `
|
||||
<strong>${stages[i-1]} → ${stages[i]}</strong><br>
|
||||
Files: ${prev.metadata?.total_files} → ${curr.metadata?.total_files}<br>
|
||||
Symbols: ${prev.symbols?.total_symbols} → ${curr.symbols?.total_symbols}<br>
|
||||
Chunks: ${(prev.chunks?.length || 0)} → ${(curr.chunks?.length || 0)}
|
||||
`;
|
||||
|
||||
if (curr.metadata?.total_files > prev.metadata?.total_files) {
|
||||
diff.classList.add('diff-added');
|
||||
} else if (curr.metadata?.total_files < prev.metadata?.total_files) {
|
||||
diff.classList.add('diff-removed');
|
||||
} else {
|
||||
diff.classList.add('diff-modified');
|
||||
}
|
||||
|
||||
diffContent.appendChild(diff);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.addEventListener('load', () => {
|
||||
initEditors();
|
||||
});
|
||||
|
||||
// Handle split pane resizing
|
||||
const splitHandle = document.querySelector('.split-handle');
|
||||
let isResizing = false;
|
||||
|
||||
splitHandle.addEventListener('mousedown', (e) => {
|
||||
isResizing = true;
|
||||
document.body.style.cursor = 'col-resize';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const container = document.getElementById('main-container');
|
||||
const x = e.clientX - container.offsetLeft;
|
||||
const width = container.offsetWidth;
|
||||
const percentage = (x / width) * 100;
|
||||
|
||||
document.getElementById('source-pane').style.flex = `0 0 ${percentage}%`;
|
||||
document.getElementById('output-pane').style.flex = `0 0 ${100 - percentage}%`;
|
||||
|
||||
splitHandle.style.left = `${percentage}%`;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isResizing = false;
|
||||
document.body.style.cursor = '';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1087
src/bundler/graph_visualizer.html
Normal file
1087
src/bundler/graph_visualizer.html
Normal file
File diff suppressed because it is too large
Load Diff
1002
src/bundler/graph_visualizer.zig
Normal file
1002
src/bundler/graph_visualizer.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -193,6 +193,8 @@ pub fn generateChunksInParallel(
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Output capture moved to writeOutputFilesToDisk where it's safe to access
|
||||
|
||||
// When bake.DevServer is in use, we're going to take a different code path at the end.
|
||||
// We want to extract the source code of each part instead of combining it into a single file.
|
||||
// This is so that when hot-module updates happen, we can:
|
||||
|
||||
@@ -259,6 +259,17 @@ pub fn writeOutputFilesToDisk(
|
||||
break :brk null;
|
||||
};
|
||||
|
||||
// Capture final output for debugging (safe here as we have the actual buffer)
|
||||
if (comptime bun.Environment.isDebug) {
|
||||
if (chunk_index_in_chunks_list == chunks.len - 1) {
|
||||
// Only dump once at the end with all chunks' output
|
||||
const GraphVisualizer = @import("../graph_visualizer.zig").GraphVisualizer;
|
||||
GraphVisualizer.dumpGraphStateWithOutput(c, "after_write", chunks, code_result.buffer) catch |err| {
|
||||
Output.warn("Failed to dump graph after write: {}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (jsc.Node.fs.NodeFS.writeFileWithPathBuffer(
|
||||
&pathbuf,
|
||||
.{
|
||||
|
||||
@@ -491,6 +491,16 @@ pub fn toAST(
|
||||
) anyerror!js_ast.Expr {
|
||||
const type_info: std.builtin.Type = @typeInfo(Type);
|
||||
|
||||
// Check if type has custom toExprForJSON method (only for structs, unions, and enums)
|
||||
switch (type_info) {
|
||||
.@"struct", .@"union", .@"enum" => {
|
||||
if (comptime @hasDecl(Type, "toExprForJSON")) {
|
||||
return try Type.toExprForJSON(&value, allocator);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
switch (type_info) {
|
||||
.bool => {
|
||||
return Expr{
|
||||
@@ -538,7 +548,7 @@ pub fn toAST(
|
||||
const exprs = try allocator.alloc(Expr, value.len);
|
||||
for (exprs, 0..) |*ex, i| ex.* = try toAST(allocator, @TypeOf(value[i]), value[i]);
|
||||
|
||||
return Expr.init(js_ast.E.Array, js_ast.E.Array{ .items = exprs }, logger.Loc.Empty);
|
||||
return Expr.init(js_ast.E.Array, js_ast.E.Array{ .items = js_ast.ExprNodeList.fromOwnedSlice(exprs) }, logger.Loc.Empty);
|
||||
},
|
||||
else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
|
||||
},
|
||||
@@ -550,9 +560,20 @@ pub fn toAST(
|
||||
const exprs = try allocator.alloc(Expr, value.len);
|
||||
for (exprs, 0..) |*ex, i| ex.* = try toAST(allocator, @TypeOf(value[i]), value[i]);
|
||||
|
||||
return Expr.init(js_ast.E.Array, js_ast.E.Array{ .items = exprs }, logger.Loc.Empty);
|
||||
return Expr.init(js_ast.E.Array, js_ast.E.Array{ .items = .init(exprs) }, logger.Loc.Empty);
|
||||
},
|
||||
.@"struct" => |Struct| {
|
||||
// Check if struct has a slice() method - treat it as an array
|
||||
if (comptime @hasField(Type, "ptr") and @hasField(Type, "len")) {
|
||||
// This looks like it might be array-like, check for slice method
|
||||
if (comptime @hasDecl(Type, "slice")) {
|
||||
const slice = value.slice();
|
||||
const exprs = try allocator.alloc(Expr, slice.len);
|
||||
for (exprs, 0..) |*ex, i| ex.* = try toAST(allocator, @TypeOf(slice[i]), slice[i]);
|
||||
return Expr.init(js_ast.E.Array, js_ast.E.Array{ .items = js_ast.ExprNodeList.fromOwnedSlice(exprs) }, logger.Loc.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
const fields: []const std.builtin.Type.StructField = Struct.fields;
|
||||
var properties = try BabyList(js_ast.G.Property).initCapacity(allocator, fields.len);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user