mirror of
https://github.com/oven-sh/bun
synced 2026-02-05 00:18:53 +00:00
Compare commits
12 Commits
dylan/byte
...
claude/gra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c275a93bdb | ||
|
|
3f97345088 | ||
|
|
2c76947aac | ||
|
|
7186669efc | ||
|
|
ed22a78c37 | ||
|
|
3aaa0e15f3 | ||
|
|
6c5a063813 | ||
|
|
55820bec90 | ||
|
|
5b8e1b61dd | ||
|
|
53f6a137aa | ||
|
|
e2ef1692f9 | ||
|
|
bc32ddfbd3 |
@@ -414,6 +414,7 @@ src/bundler/BundleThread.zig
|
||||
src/bundler/Chunk.zig
|
||||
src/bundler/DeferredBatchTask.zig
|
||||
src/bundler/entry_points.zig
|
||||
src/bundler/graph_visualizer.zig
|
||||
src/bundler/Graph.zig
|
||||
src/bundler/HTMLImportManifest.zig
|
||||
src/bundler/linker_context/computeChunks.zig
|
||||
|
||||
@@ -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,
|
||||
@@ -392,6 +393,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()) {
|
||||
@@ -409,18 +417,39 @@ 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 (comptime FeatureFlags.help_catch_memory_issues) {
|
||||
this.checkForMemoryCorruption();
|
||||
}
|
||||
|
||||
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
996
src/bundler/graph_visualizer.zig
Normal file
996
src/bundler/graph_visualizer.zig
Normal file
@@ -0,0 +1,996 @@
|
||||
const std = @import("std");
|
||||
const bun = @import("bun");
|
||||
const string = bun.string;
|
||||
const Output = bun.Output;
|
||||
const Global = bun.Global;
|
||||
const Environment = bun.Environment;
|
||||
const strings = bun.strings;
|
||||
const MutableString = bun.MutableString;
|
||||
const stringZ = bun.stringZ;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const C = bun.C;
|
||||
const JSC = bun.JSC;
|
||||
const sys = bun.sys;
|
||||
const js_ast = bun.ast;
|
||||
const bundler = bun.bundle_v2;
|
||||
const LinkerContext = bundler.LinkerContext;
|
||||
const Index = js_ast.Index;
|
||||
const Ref = js_ast.Ref;
|
||||
const Symbol = js_ast.Symbol;
|
||||
const ImportRecord = bun.ImportRecord;
|
||||
const DeclaredSymbol = js_ast.DeclaredSymbol;
|
||||
const logger = bun.logger;
|
||||
const Part = js_ast.Part;
|
||||
const Chunk = bundler.Chunk;
|
||||
const js_printer = bun.js_printer;
|
||||
const JSON = bun.json;
|
||||
const JSAst = bun.ast;
|
||||
|
||||
pub const GraphVisualizer = struct {
|
||||
pub fn dumpGraphStateWithOutput(ctx: *LinkerContext, stage: []const u8, chunks: []Chunk, output_buffer: []const u8) !void {
|
||||
// Special version that takes the safe output buffer directly
|
||||
if (comptime !Environment.isDebug) return;
|
||||
if (!shouldDump()) return;
|
||||
|
||||
const allocator = bun.default_allocator;
|
||||
const timestamp = std.time.milliTimestamp();
|
||||
const output_dir = "/tmp/bun-bundler-debug";
|
||||
|
||||
// Ensure output directory exists
|
||||
std.fs.cwd().makePath(output_dir) catch {};
|
||||
|
||||
// Build graph data but pass the safe output buffer
|
||||
const graph_data = try buildGraphDataWithOutput(allocator, ctx, stage, timestamp, chunks, output_buffer);
|
||||
defer allocator.free(graph_data);
|
||||
|
||||
// Write JSON file
|
||||
const json_filename = try std.fmt.allocPrint(allocator, "{s}/bundler_graph_{s}_{d}.json", .{
|
||||
output_dir,
|
||||
stage,
|
||||
timestamp,
|
||||
});
|
||||
defer allocator.free(json_filename);
|
||||
|
||||
const json_file = try std.fs.cwd().createFile(json_filename, .{});
|
||||
defer json_file.close();
|
||||
|
||||
try json_file.writeAll(graph_data);
|
||||
|
||||
// Also generate the visualizer HTML
|
||||
try generateVisualizerHTML(allocator, output_dir, timestamp);
|
||||
}
|
||||
|
||||
fn buildGraphDataWithOutput(
|
||||
allocator: std.mem.Allocator,
|
||||
ctx: *LinkerContext,
|
||||
stage: []const u8,
|
||||
timestamp: i64,
|
||||
chunks: []Chunk,
|
||||
output_buffer: []const u8,
|
||||
) ![]const u8 {
|
||||
// Build the graph data but use the provided safe output buffer
|
||||
// instead of trying to extract it from potentially poisoned memory
|
||||
var graph_data = try buildGraphData(ctx, allocator, stage, timestamp, chunks);
|
||||
|
||||
// Override the output snippet with the safe buffer
|
||||
if (chunks.len > 0 and graph_data.chunks != null) {
|
||||
// Use the safe output buffer directly
|
||||
graph_data.chunks.?[chunks.len - 1].output_snippet = try allocator.dupe(u8, output_buffer);
|
||||
}
|
||||
|
||||
// Convert to JSON AST
|
||||
const json_ast = try JSON.toAST(allocator, GraphData, graph_data);
|
||||
|
||||
// Print JSON to buffer
|
||||
var stack_fallback = std.heap.stackFallback(1024 * 1024, allocator); // 1MB stack fallback
|
||||
const print_allocator = stack_fallback.get();
|
||||
|
||||
const buffer_writer = js_printer.BufferWriter.init(print_allocator);
|
||||
var writer = js_printer.BufferPrinter.init(buffer_writer);
|
||||
defer writer.ctx.buffer.deinit();
|
||||
|
||||
const source = &logger.Source.initEmptyFile("graph_data.json");
|
||||
_ = js_printer.printJSON(
|
||||
*js_printer.BufferPrinter,
|
||||
&writer,
|
||||
json_ast,
|
||||
source,
|
||||
.{ .mangled_props = null },
|
||||
) catch |err| {
|
||||
debug("Failed to print JSON: {}", .{err});
|
||||
return err;
|
||||
};
|
||||
|
||||
// Return the printed JSON as a string
|
||||
return try allocator.dupe(u8, writer.ctx.buffer.list.items);
|
||||
}
|
||||
const debug = Output.scoped(.GraphViz, .visible);
|
||||
|
||||
pub fn shouldDump() bool {
|
||||
if (comptime !Environment.isDebug) return false;
|
||||
return bun.getenvZ("BUN_BUNDLER_GRAPH_DUMP") != null;
|
||||
}
|
||||
|
||||
pub fn getDumpStage() DumpStage {
|
||||
const env_val = bun.getenvZ("BUN_BUNDLER_GRAPH_DUMP") orelse return .none;
|
||||
|
||||
if (strings.eqlComptime(env_val, "all")) return .all;
|
||||
if (strings.eqlComptime(env_val, "scan")) return .after_scan;
|
||||
if (strings.eqlComptime(env_val, "compute")) return .after_compute;
|
||||
if (strings.eqlComptime(env_val, "chunks")) return .after_chunks;
|
||||
if (strings.eqlComptime(env_val, "link")) return .after_link;
|
||||
if (strings.eqlComptime(env_val, "write")) return .after_write;
|
||||
if (strings.eqlComptime(env_val, "generation")) return .after_write; // Legacy alias
|
||||
|
||||
return .all; // Default to all if set but not recognized
|
||||
}
|
||||
|
||||
pub const DumpStage = enum {
|
||||
none,
|
||||
after_scan,
|
||||
after_compute,
|
||||
after_chunks,
|
||||
after_link,
|
||||
after_write,
|
||||
all,
|
||||
};
|
||||
|
||||
pub fn dumpGraphState(
|
||||
ctx: *bundler.LinkerContext,
|
||||
stage: []const u8,
|
||||
chunks: ?[]const Chunk,
|
||||
) !void {
|
||||
debug("dumpGraphState called for stage: {s}", .{stage});
|
||||
|
||||
if (!shouldDump()) {
|
||||
debug("shouldDump() returned false", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const dump_stage = getDumpStage();
|
||||
debug("dump_stage: {}", .{dump_stage});
|
||||
|
||||
const should_dump_now = switch (dump_stage) {
|
||||
.none => false,
|
||||
.all => true,
|
||||
.after_scan => strings.eqlComptime(stage, "after_scan"),
|
||||
.after_compute => strings.eqlComptime(stage, "after_compute"),
|
||||
.after_chunks => strings.eqlComptime(stage, "after_chunks"),
|
||||
.after_link => strings.eqlComptime(stage, "after_link"),
|
||||
.after_write => strings.eqlComptime(stage, "after_write"),
|
||||
};
|
||||
|
||||
if (!should_dump_now) {
|
||||
debug("should_dump_now is false for stage {s}", .{stage});
|
||||
return;
|
||||
}
|
||||
|
||||
debug("Proceeding with dump for stage: {s}", .{stage});
|
||||
|
||||
debug("Dumping graph state: {s}", .{stage});
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(default_allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
// Create output directory
|
||||
const output_dir = "/tmp/bun-bundler-debug";
|
||||
std.fs.cwd().makePath(output_dir) catch |err| {
|
||||
debug("Failed to create output directory: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// Generate filename with timestamp
|
||||
const timestamp = std.time.milliTimestamp();
|
||||
const filename = try std.fmt.allocPrint(allocator, "{s}/bundler_graph_{s}_{d}.json", .{
|
||||
output_dir,
|
||||
stage,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
// Build the graph data structure
|
||||
const graph_data = try buildGraphData(ctx, allocator, stage, timestamp, chunks);
|
||||
|
||||
// Convert to JSON AST
|
||||
const json_ast = try JSON.toAST(allocator, GraphData, graph_data);
|
||||
|
||||
// Print JSON to buffer
|
||||
var stack_fallback = std.heap.stackFallback(1024 * 1024, allocator); // 1MB stack fallback
|
||||
const print_allocator = stack_fallback.get();
|
||||
|
||||
const buffer_writer = js_printer.BufferWriter.init(print_allocator);
|
||||
var writer = js_printer.BufferPrinter.init(buffer_writer);
|
||||
defer writer.ctx.buffer.deinit();
|
||||
|
||||
const source = &logger.Source.initEmptyFile(filename);
|
||||
_ = js_printer.printJSON(
|
||||
*js_printer.BufferPrinter,
|
||||
&writer,
|
||||
json_ast,
|
||||
source,
|
||||
.{ .mangled_props = null },
|
||||
) catch |err| {
|
||||
debug("Failed to print JSON: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// Write to file
|
||||
const file = try std.fs.cwd().createFile(filename, .{});
|
||||
defer file.close();
|
||||
try file.writeAll(writer.ctx.buffer.list.items);
|
||||
|
||||
debug("Graph dump written to: {s}", .{filename});
|
||||
|
||||
// Also generate the visualizer HTML
|
||||
try generateVisualizerHTML(allocator, output_dir, timestamp);
|
||||
}
|
||||
|
||||
const GraphData = struct {
|
||||
stage: []const u8,
|
||||
timestamp: i64,
|
||||
metadata: Metadata,
|
||||
files: []FileData,
|
||||
symbols: SymbolData,
|
||||
entry_points: []EntryPointData,
|
||||
imports_and_exports: ImportsExports,
|
||||
chunks: ?[]ChunkData,
|
||||
dependency_graph: DependencyGraph,
|
||||
runtime_meta: RuntimeMeta,
|
||||
symbol_chains: []SymbolChain,
|
||||
};
|
||||
|
||||
const SymbolChain = struct {
|
||||
export_name: []const u8,
|
||||
source_file: u32,
|
||||
chain: []ChainLink,
|
||||
has_conflicts: bool,
|
||||
conflict_sources: ?[]u32,
|
||||
};
|
||||
|
||||
const ChainLink = struct {
|
||||
file_index: u32,
|
||||
symbol_name: []const u8,
|
||||
symbol_ref: []const u8,
|
||||
link_type: []const u8, // "export", "import", "re-export", "namespace"
|
||||
};
|
||||
|
||||
const RuntimeMeta = struct {
|
||||
memory_usage_mb: f64,
|
||||
parse_graph_file_count: usize,
|
||||
estimated_file_loader_count: usize,
|
||||
has_css: bool,
|
||||
has_html: bool,
|
||||
};
|
||||
|
||||
const Metadata = struct {
|
||||
total_files: usize,
|
||||
reachable_files: usize,
|
||||
entry_points: usize,
|
||||
code_splitting: bool,
|
||||
output_format: []const u8,
|
||||
target: []const u8,
|
||||
tree_shaking: bool,
|
||||
minify: bool,
|
||||
};
|
||||
|
||||
const FileData = struct {
|
||||
index: usize,
|
||||
path: []const u8,
|
||||
loader: []const u8,
|
||||
source_length: usize,
|
||||
entry_point_kind: []const u8,
|
||||
part_count: usize,
|
||||
parts: ?[]PartData,
|
||||
named_exports_count: usize,
|
||||
named_imports_count: usize,
|
||||
flags: FileFlags,
|
||||
source_snippet: ?[]const u8, // First 500 chars of source
|
||||
transformed_code: ?[]const u8, // Transformed output for this file
|
||||
};
|
||||
|
||||
const FileFlags = struct {
|
||||
is_async: bool,
|
||||
needs_exports_variable: bool,
|
||||
needs_synthetic_default_export: bool,
|
||||
wrap: []const u8,
|
||||
};
|
||||
|
||||
const PartData = struct {
|
||||
index: usize,
|
||||
stmt_count: usize,
|
||||
import_record_count: usize,
|
||||
declared_symbol_count: usize,
|
||||
can_be_removed_if_unused: bool,
|
||||
force_tree_shaking: bool,
|
||||
symbol_uses: []SymbolUse,
|
||||
dependencies: []PartDependency,
|
||||
import_records: []ImportRecordInfo,
|
||||
declared_symbols: []DeclaredSymbolInfo,
|
||||
};
|
||||
|
||||
const ImportRecordInfo = struct {
|
||||
index: usize,
|
||||
kind: []const u8,
|
||||
path: []const u8,
|
||||
is_internal: bool,
|
||||
};
|
||||
|
||||
const DeclaredSymbolInfo = struct {
|
||||
ref: []const u8,
|
||||
is_top_level: bool,
|
||||
};
|
||||
|
||||
const SymbolUse = struct {
|
||||
ref: []const u8,
|
||||
count: u32,
|
||||
};
|
||||
|
||||
const PartDependency = struct {
|
||||
source: u32,
|
||||
part: u32,
|
||||
};
|
||||
|
||||
const SymbolData = struct {
|
||||
total_symbols: usize,
|
||||
by_source: []SourceSymbols,
|
||||
};
|
||||
|
||||
const SourceSymbols = struct {
|
||||
source_index: usize,
|
||||
symbol_count: usize,
|
||||
symbols: []SymbolInfo,
|
||||
};
|
||||
|
||||
const SymbolInfo = struct {
|
||||
inner_index: usize,
|
||||
kind: []const u8,
|
||||
original_name: []const u8,
|
||||
link: ?[]const u8,
|
||||
use_count_estimate: u32,
|
||||
chunk_index: ?u32,
|
||||
nested_scope_slot: ?u32,
|
||||
flags: SymbolFlags,
|
||||
namespace_alias: ?struct {
|
||||
namespace_ref: []const u8,
|
||||
alias: []const u8,
|
||||
},
|
||||
};
|
||||
|
||||
const SymbolFlags = struct {
|
||||
must_not_be_renamed: bool,
|
||||
did_keep_name: bool,
|
||||
has_been_assigned_to: bool,
|
||||
must_start_with_capital_letter_for_jsx: bool,
|
||||
private_symbol_must_be_lowered: bool,
|
||||
remove_overwritten_function_declaration: bool,
|
||||
import_item_status: []const u8,
|
||||
};
|
||||
|
||||
const EntryPointData = struct {
|
||||
source_index: u32,
|
||||
output_path: []const u8,
|
||||
};
|
||||
|
||||
const ImportsExports = struct {
|
||||
total_exports: usize,
|
||||
total_imports: usize,
|
||||
total_import_records: usize,
|
||||
exports: []ExportInfo,
|
||||
resolved_exports: []ResolvedExportInfo,
|
||||
imports: []ImportInfo,
|
||||
};
|
||||
|
||||
const ExportInfo = struct {
|
||||
source: u32,
|
||||
name: []const u8,
|
||||
ref: []const u8,
|
||||
original_symbol_name: ?[]const u8,
|
||||
alias_loc: i32,
|
||||
};
|
||||
|
||||
const ResolvedExportInfo = struct {
|
||||
source: u32,
|
||||
export_alias: []const u8,
|
||||
target_source: ?u32,
|
||||
target_ref: ?[]const u8,
|
||||
potentially_ambiguous: bool,
|
||||
ambiguous_count: usize,
|
||||
};
|
||||
|
||||
const ImportInfo = struct {
|
||||
source: u32,
|
||||
kind: []const u8,
|
||||
path: []const u8,
|
||||
target_source: ?u32,
|
||||
};
|
||||
|
||||
const ChunkData = struct {
|
||||
index: usize,
|
||||
is_entry_point: bool,
|
||||
source_index: u32,
|
||||
files_in_chunk: []u32,
|
||||
cross_chunk_import_count: usize,
|
||||
cross_chunk_imports: []CrossChunkImportInfo,
|
||||
unique_key: []const u8,
|
||||
final_path: []const u8,
|
||||
content_type: []const u8,
|
||||
output_snippet: ?[]const u8, // First 1000 chars of output
|
||||
sourcemap_data: ?[]const u8, // VLQ-encoded sourcemap mappings
|
||||
};
|
||||
|
||||
const CrossChunkImportInfo = struct {
|
||||
chunk_index: u32,
|
||||
import_kind: []const u8,
|
||||
};
|
||||
|
||||
const DependencyGraph = struct {
|
||||
edges: []GraphEdge,
|
||||
};
|
||||
|
||||
const GraphEdge = struct {
|
||||
from: NodeRef,
|
||||
to: NodeRef,
|
||||
};
|
||||
|
||||
const NodeRef = struct {
|
||||
source: u32,
|
||||
part: u32,
|
||||
};
|
||||
|
||||
fn buildGraphData(
|
||||
ctx: *bundler.LinkerContext,
|
||||
allocator: std.mem.Allocator,
|
||||
stage: []const u8,
|
||||
timestamp: i64,
|
||||
chunks: ?[]const Chunk,
|
||||
) !GraphData {
|
||||
const sources = ctx.parse_graph.input_files.items(.source);
|
||||
const loaders = ctx.parse_graph.input_files.items(.loader);
|
||||
const ast_list = ctx.graph.ast.slice();
|
||||
const meta_list = ctx.graph.meta.slice();
|
||||
const files_list = ctx.graph.files.slice();
|
||||
|
||||
// Build metadata
|
||||
const metadata = Metadata{
|
||||
.total_files = ctx.graph.files.len,
|
||||
.reachable_files = ctx.graph.reachable_files.len,
|
||||
.entry_points = ctx.graph.entry_points.len,
|
||||
.code_splitting = ctx.graph.code_splitting,
|
||||
.output_format = @tagName(ctx.options.output_format),
|
||||
.target = @tagName(ctx.options.target),
|
||||
.tree_shaking = ctx.options.tree_shaking,
|
||||
.minify = ctx.options.minify_syntax,
|
||||
};
|
||||
|
||||
// Build file data
|
||||
var file_data_list = try allocator.alloc(FileData, ctx.graph.files.len);
|
||||
for (0..ctx.graph.files.len) |i| {
|
||||
var parts_data: ?[]PartData = null;
|
||||
|
||||
if (i < ast_list.items(.parts).len) {
|
||||
const parts = ast_list.items(.parts)[i].slice();
|
||||
if (parts.len > 0) {
|
||||
parts_data = try allocator.alloc(PartData, parts.len);
|
||||
for (parts, 0..) |part, j| {
|
||||
// Build symbol uses
|
||||
var symbol_uses = try allocator.alloc(SymbolUse, part.symbol_uses.count());
|
||||
var use_idx: usize = 0;
|
||||
var use_iter = part.symbol_uses.iterator();
|
||||
while (use_iter.next()) |entry| : (use_idx += 1) {
|
||||
symbol_uses[use_idx] = .{
|
||||
.ref = try std.fmt.allocPrint(allocator, "{}", .{entry.key_ptr.*}),
|
||||
.count = entry.value_ptr.count_estimate,
|
||||
};
|
||||
}
|
||||
|
||||
// Build dependencies
|
||||
var deps = try allocator.alloc(PartDependency, part.dependencies.len);
|
||||
for (part.dependencies.slice(), 0..) |dep, k| {
|
||||
deps[k] = .{
|
||||
.source = dep.source_index.get(),
|
||||
.part = dep.part_index,
|
||||
};
|
||||
}
|
||||
|
||||
// Build import records info
|
||||
var import_records = try allocator.alloc(ImportRecordInfo, part.import_record_indices.len);
|
||||
const ast_import_records = if (i < ast_list.items(.import_records).len)
|
||||
ast_list.items(.import_records)[i].slice()
|
||||
else
|
||||
&[_]ImportRecord{};
|
||||
|
||||
for (part.import_record_indices.slice(), 0..) |record_idx, k| {
|
||||
if (record_idx < ast_import_records.len) {
|
||||
const record = ast_import_records[record_idx];
|
||||
import_records[k] = .{
|
||||
.index = record_idx,
|
||||
.kind = @tagName(record.kind),
|
||||
.path = record.path.text,
|
||||
.is_internal = record.source_index.isValid(),
|
||||
};
|
||||
} else {
|
||||
import_records[k] = .{
|
||||
.index = record_idx,
|
||||
.kind = "unknown",
|
||||
.path = "",
|
||||
.is_internal = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build declared symbols info
|
||||
var declared_symbols = try allocator.alloc(DeclaredSymbolInfo, part.declared_symbols.entries.len);
|
||||
const decl_entries = part.declared_symbols.entries.slice();
|
||||
for (decl_entries.items(.ref), decl_entries.items(.is_top_level), 0..) |ref, is_top_level, k| {
|
||||
declared_symbols[k] = .{
|
||||
.ref = try std.fmt.allocPrint(allocator, "{}", .{ref}),
|
||||
.is_top_level = is_top_level,
|
||||
};
|
||||
}
|
||||
|
||||
parts_data.?[j] = .{
|
||||
.index = j,
|
||||
.stmt_count = part.stmts.len,
|
||||
.import_record_count = part.import_record_indices.len,
|
||||
.declared_symbol_count = part.declared_symbols.entries.len,
|
||||
.can_be_removed_if_unused = part.can_be_removed_if_unused,
|
||||
.force_tree_shaking = part.force_tree_shaking,
|
||||
.symbol_uses = symbol_uses,
|
||||
.dependencies = deps,
|
||||
.import_records = import_records,
|
||||
.declared_symbols = declared_symbols,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const path = if (i < sources.len) sources[i].path.text else "unknown";
|
||||
const loader = if (i < loaders.len) @tagName(loaders[i]) else "unknown";
|
||||
const entry_point_kind = @tagName(files_list.items(.entry_point_kind)[i]);
|
||||
|
||||
var flags = FileFlags{
|
||||
.is_async = false,
|
||||
.needs_exports_variable = false,
|
||||
.needs_synthetic_default_export = false,
|
||||
.wrap = "none",
|
||||
};
|
||||
|
||||
if (i < meta_list.items(.flags).len) {
|
||||
const meta_flags = meta_list.items(.flags)[i];
|
||||
flags = .{
|
||||
.is_async = meta_flags.is_async_or_has_async_dependency,
|
||||
.needs_exports_variable = meta_flags.needs_exports_variable,
|
||||
.needs_synthetic_default_export = meta_flags.needs_synthetic_default_export,
|
||||
.wrap = @tagName(meta_flags.wrap),
|
||||
};
|
||||
}
|
||||
|
||||
const named_exports_count = if (i < ast_list.items(.named_exports).len)
|
||||
ast_list.items(.named_exports)[i].count() else 0;
|
||||
const named_imports_count = if (i < ast_list.items(.named_imports).len)
|
||||
ast_list.items(.named_imports)[i].count() else 0;
|
||||
const part_count = if (i < ast_list.items(.parts).len)
|
||||
ast_list.items(.parts)[i].len else 0;
|
||||
|
||||
// Get full source code
|
||||
const source_snippet = if (i < sources.len and sources[i].contents.len > 0) blk: {
|
||||
break :blk try allocator.dupe(u8, sources[i].contents);
|
||||
} else null;
|
||||
|
||||
file_data_list[i] = .{
|
||||
.index = i,
|
||||
.path = path,
|
||||
.loader = loader,
|
||||
.source_length = if (i < sources.len) sources[i].contents.len else 0,
|
||||
.entry_point_kind = entry_point_kind,
|
||||
.part_count = part_count,
|
||||
.parts = parts_data,
|
||||
.named_exports_count = named_exports_count,
|
||||
.named_imports_count = named_imports_count,
|
||||
.flags = flags,
|
||||
.source_snippet = source_snippet,
|
||||
.transformed_code = null, // Transformed code is available in chunk compile results, not per-file
|
||||
};
|
||||
}
|
||||
|
||||
// Build symbol data
|
||||
var by_source = try allocator.alloc(SourceSymbols, ctx.graph.symbols.symbols_for_source.len);
|
||||
var total_symbols: usize = 0;
|
||||
for (ctx.graph.symbols.symbols_for_source.slice(), 0..) |symbols, source_idx| {
|
||||
total_symbols += symbols.len;
|
||||
|
||||
var symbol_infos = try allocator.alloc(SymbolInfo, symbols.len);
|
||||
for (symbols.slice(), 0..) |symbol, j| {
|
||||
const invalid_chunk_index = std.math.maxInt(u32);
|
||||
const invalid_nested_scope_slot = std.math.maxInt(u32);
|
||||
|
||||
symbol_infos[j] = .{
|
||||
.inner_index = j,
|
||||
.kind = @tagName(symbol.kind),
|
||||
.original_name = symbol.original_name,
|
||||
.link = if (symbol.link.isValid())
|
||||
try std.fmt.allocPrint(allocator, "Ref[inner={}, src={}, .symbol]", .{symbol.link.innerIndex(), symbol.link.sourceIndex()})
|
||||
else null,
|
||||
.use_count_estimate = symbol.use_count_estimate,
|
||||
.chunk_index = if (symbol.chunk_index != invalid_chunk_index) symbol.chunk_index else null,
|
||||
.nested_scope_slot = if (symbol.nested_scope_slot != invalid_nested_scope_slot) symbol.nested_scope_slot else null,
|
||||
.flags = .{
|
||||
.must_not_be_renamed = symbol.must_not_be_renamed,
|
||||
.did_keep_name = symbol.did_keep_name,
|
||||
.has_been_assigned_to = symbol.has_been_assigned_to,
|
||||
.must_start_with_capital_letter_for_jsx = symbol.must_start_with_capital_letter_for_jsx,
|
||||
.private_symbol_must_be_lowered = symbol.private_symbol_must_be_lowered,
|
||||
.remove_overwritten_function_declaration = symbol.remove_overwritten_function_declaration,
|
||||
.import_item_status = @tagName(symbol.import_item_status),
|
||||
},
|
||||
.namespace_alias = if (symbol.namespace_alias) |alias| .{
|
||||
.namespace_ref = try std.fmt.allocPrint(allocator, "Ref[inner={}, src={}, .symbol]", .{alias.namespace_ref.innerIndex(), alias.namespace_ref.sourceIndex()}),
|
||||
.alias = alias.alias,
|
||||
} else null,
|
||||
};
|
||||
}
|
||||
|
||||
by_source[source_idx] = .{
|
||||
.source_index = source_idx,
|
||||
.symbol_count = symbols.len,
|
||||
.symbols = symbol_infos,
|
||||
};
|
||||
}
|
||||
|
||||
const symbol_data = SymbolData{
|
||||
.total_symbols = total_symbols,
|
||||
.by_source = by_source,
|
||||
};
|
||||
|
||||
// Build entry points
|
||||
const entry_points = ctx.graph.entry_points.slice();
|
||||
var entry_point_data = try allocator.alloc(EntryPointData, entry_points.len);
|
||||
for (entry_points.items(.source_index), entry_points.items(.output_path), 0..) |source_idx, output_path, i| {
|
||||
entry_point_data[i] = .{
|
||||
.source_index = source_idx,
|
||||
.output_path = output_path.slice(),
|
||||
};
|
||||
}
|
||||
|
||||
// Build imports and exports
|
||||
const ast_named_exports = ast_list.items(.named_exports);
|
||||
const ast_named_imports = ast_list.items(.named_imports);
|
||||
const import_records_list = ast_list.items(.import_records);
|
||||
|
||||
var total_exports: usize = 0;
|
||||
var total_imports: usize = 0;
|
||||
var total_import_records: usize = 0;
|
||||
|
||||
// Count totals
|
||||
for (ast_named_exports) |exports| {
|
||||
total_exports += exports.count();
|
||||
}
|
||||
for (ast_named_imports) |imports| {
|
||||
total_imports += imports.count();
|
||||
}
|
||||
for (import_records_list) |records| {
|
||||
total_import_records += records.len;
|
||||
}
|
||||
|
||||
// Collect all exports with symbol resolution
|
||||
var exports_list = try std.ArrayList(ExportInfo).initCapacity(allocator, @min(total_exports, 1000));
|
||||
for (ast_named_exports, 0..) |exports, source_idx| {
|
||||
if (exports.count() == 0) continue;
|
||||
|
||||
var iter = exports.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
if (exports_list.items.len >= 1000) break; // Limit for performance
|
||||
|
||||
const export_name = entry.key_ptr.*;
|
||||
const export_ref = entry.value_ptr.ref;
|
||||
|
||||
// Get the actual symbol name
|
||||
var original_symbol_name: ?[]const u8 = null;
|
||||
if (export_ref.isValid() and source_idx < ctx.graph.symbols.symbols_for_source.len) {
|
||||
const symbols = ctx.graph.symbols.symbols_for_source.at(export_ref.sourceIndex());
|
||||
if (export_ref.innerIndex() < symbols.len) {
|
||||
original_symbol_name = symbols.at(export_ref.innerIndex()).original_name;
|
||||
}
|
||||
}
|
||||
|
||||
try exports_list.append(.{
|
||||
.source = @intCast(source_idx),
|
||||
.name = export_name,
|
||||
.ref = try std.fmt.allocPrint(allocator, "Ref[inner={}, src={}, .symbol]", .{export_ref.innerIndex(), export_ref.sourceIndex()}),
|
||||
.original_symbol_name = original_symbol_name,
|
||||
.alias_loc = entry.value_ptr.alias_loc.start,
|
||||
});
|
||||
}
|
||||
if (exports_list.items.len >= 1000) break;
|
||||
}
|
||||
|
||||
// Also track resolved exports if available
|
||||
const meta_resolved_exports = meta_list.items(.resolved_exports);
|
||||
var resolved_exports_list = try std.ArrayList(ResolvedExportInfo).initCapacity(allocator, 1000);
|
||||
for (meta_resolved_exports, 0..) |resolved, source_idx| {
|
||||
if (resolved.count() == 0) continue;
|
||||
|
||||
var iter = resolved.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
if (resolved_exports_list.items.len >= 1000) break;
|
||||
|
||||
const export_alias = entry.key_ptr.*;
|
||||
const export_data = entry.value_ptr.*;
|
||||
|
||||
try resolved_exports_list.append(.{
|
||||
.source = @intCast(source_idx),
|
||||
.export_alias = export_alias,
|
||||
.target_source = if (export_data.data.source_index.isValid()) export_data.data.source_index.get() else null,
|
||||
.target_ref = if (export_data.data.import_ref.isValid())
|
||||
try std.fmt.allocPrint(allocator, "Ref[inner={}, src={}, .symbol]", .{export_data.data.import_ref.innerIndex(), export_data.data.import_ref.sourceIndex()})
|
||||
else null,
|
||||
.potentially_ambiguous = export_data.potentially_ambiguous_export_star_refs.len > 0,
|
||||
.ambiguous_count = export_data.potentially_ambiguous_export_star_refs.len,
|
||||
});
|
||||
}
|
||||
if (resolved_exports_list.items.len >= 1000) break;
|
||||
}
|
||||
|
||||
// Collect all imports
|
||||
var imports_list = try std.ArrayList(ImportInfo).initCapacity(allocator, @min(total_import_records, 1000));
|
||||
for (import_records_list, 0..) |records, source_idx| {
|
||||
if (records.len == 0) continue;
|
||||
|
||||
for (records.slice()[0..@min(records.len, 100)]) |record| {
|
||||
if (imports_list.items.len >= 1000) break; // Limit for performance
|
||||
|
||||
try imports_list.append(.{
|
||||
.source = @intCast(source_idx),
|
||||
.kind = @tagName(record.kind),
|
||||
.path = record.path.text,
|
||||
.target_source = if (record.source_index.isValid()) record.source_index.get() else null,
|
||||
});
|
||||
}
|
||||
if (imports_list.items.len >= 1000) break;
|
||||
}
|
||||
|
||||
const imports_exports = ImportsExports{
|
||||
.total_exports = total_exports,
|
||||
.total_imports = total_imports,
|
||||
.total_import_records = total_import_records,
|
||||
.exports = exports_list.items,
|
||||
.resolved_exports = resolved_exports_list.items,
|
||||
.imports = imports_list.items,
|
||||
};
|
||||
|
||||
// Build chunks data
|
||||
var chunks_data: ?[]ChunkData = null;
|
||||
if (chunks) |chunk_list| {
|
||||
chunks_data = try allocator.alloc(ChunkData, chunk_list.len);
|
||||
for (chunk_list, 0..) |chunk, i| {
|
||||
// Collect files in chunk
|
||||
var files_in_chunk = try allocator.alloc(u32, chunk.files_with_parts_in_chunk.count());
|
||||
var file_iter = chunk.files_with_parts_in_chunk.iterator();
|
||||
var j: usize = 0;
|
||||
while (file_iter.next()) |entry| : (j += 1) {
|
||||
files_in_chunk[j] = entry.key_ptr.*;
|
||||
}
|
||||
|
||||
// Build cross-chunk imports info
|
||||
var cross_chunk_imports = try allocator.alloc(CrossChunkImportInfo, chunk.cross_chunk_imports.len);
|
||||
for (chunk.cross_chunk_imports.slice(), 0..) |import, k| {
|
||||
cross_chunk_imports[k] = .{
|
||||
.chunk_index = import.chunk_index,
|
||||
.import_kind = @tagName(import.import_kind),
|
||||
};
|
||||
}
|
||||
|
||||
// Output will be captured safely in writeOutputFilesToDisk
|
||||
const output_snippet: ?[]const u8 = null;
|
||||
// Don't try to extract from compile_results here - it may be poisoned
|
||||
|
||||
// Store sourcemap data if available
|
||||
var sourcemap_data: ?[]const u8 = null;
|
||||
if (chunk.output_source_map.mappings.items.len > 0) {
|
||||
// The sourcemap mappings are in VLQ format
|
||||
sourcemap_data = try allocator.dupe(u8, chunk.output_source_map.mappings.items);
|
||||
} else if (chunk.output_source_map.prefix.items.len > 0 or chunk.output_source_map.suffix.items.len > 0) {
|
||||
// Sometimes the mappings might be empty but we have prefix/suffix
|
||||
// Just indicate that source map generation was attempted
|
||||
sourcemap_data = try allocator.dupe(u8, "sourcemap_generated");
|
||||
}
|
||||
|
||||
chunks_data.?[i] = .{
|
||||
.index = i,
|
||||
.is_entry_point = chunk.entry_point.is_entry_point,
|
||||
.source_index = chunk.entry_point.source_index,
|
||||
.files_in_chunk = files_in_chunk,
|
||||
.cross_chunk_import_count = chunk.cross_chunk_imports.len,
|
||||
.cross_chunk_imports = cross_chunk_imports,
|
||||
.unique_key = chunk.unique_key,
|
||||
.final_path = chunk.final_rel_path,
|
||||
.content_type = @tagName(chunk.content),
|
||||
.output_snippet = output_snippet,
|
||||
.sourcemap_data = sourcemap_data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build dependency graph
|
||||
const parts_lists = ast_list.items(.parts);
|
||||
var edges = try std.ArrayList(GraphEdge).initCapacity(allocator, 1000);
|
||||
|
||||
for (parts_lists, 0..) |parts, source_idx| {
|
||||
for (parts.slice(), 0..) |part, part_idx| {
|
||||
for (part.dependencies.slice()) |dep| {
|
||||
if (edges.items.len >= 1000) break; // Limit for performance
|
||||
|
||||
try edges.append(.{
|
||||
.from = .{ .source = @intCast(source_idx), .part = @intCast(part_idx) },
|
||||
.to = .{ .source = dep.source_index.get(), .part = dep.part_index },
|
||||
});
|
||||
}
|
||||
if (edges.items.len >= 1000) break;
|
||||
}
|
||||
if (edges.items.len >= 1000) break;
|
||||
}
|
||||
|
||||
const dependency_graph = DependencyGraph{
|
||||
.edges = edges.items,
|
||||
};
|
||||
|
||||
// Build runtime meta
|
||||
const has_css = ctx.parse_graph.css_file_count > 0;
|
||||
const has_html = blk: {
|
||||
for (loaders) |loader| {
|
||||
if (loader == .html) break :blk true;
|
||||
}
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
// Get current memory usage
|
||||
const memory_usage_mb: f64 = if (bun.sys.selfProcessMemoryUsage()) |rss|
|
||||
@as(f64, @floatFromInt(rss)) / (1024.0 * 1024.0)
|
||||
else 0;
|
||||
|
||||
const runtime_meta = RuntimeMeta{
|
||||
.memory_usage_mb = memory_usage_mb,
|
||||
.parse_graph_file_count = ctx.parse_graph.input_files.len,
|
||||
.estimated_file_loader_count = ctx.parse_graph.estimated_file_loader_count,
|
||||
.has_css = has_css,
|
||||
.has_html = has_html,
|
||||
};
|
||||
|
||||
// Build symbol resolution chains
|
||||
const symbol_chains = try buildSymbolChains(allocator, ctx, exports_list.items, resolved_exports_list.items);
|
||||
|
||||
return GraphData{
|
||||
.stage = stage,
|
||||
.timestamp = timestamp,
|
||||
.metadata = metadata,
|
||||
.files = file_data_list,
|
||||
.symbols = symbol_data,
|
||||
.entry_points = entry_point_data,
|
||||
.imports_and_exports = imports_exports,
|
||||
.chunks = chunks_data,
|
||||
.dependency_graph = dependency_graph,
|
||||
.runtime_meta = runtime_meta,
|
||||
.symbol_chains = symbol_chains,
|
||||
};
|
||||
}
|
||||
|
||||
fn buildSymbolChains(
|
||||
allocator: std.mem.Allocator,
|
||||
ctx: *LinkerContext,
|
||||
exports: []const ExportInfo,
|
||||
resolved_exports: []const ResolvedExportInfo,
|
||||
) ![]SymbolChain {
|
||||
_ = ctx; // Will use for more detailed symbol lookups
|
||||
var chains = std.ArrayList(SymbolChain).init(allocator);
|
||||
defer chains.deinit();
|
||||
|
||||
// Track each unique export and build its resolution chain
|
||||
var seen = std.StringHashMap(void).init(allocator);
|
||||
defer seen.deinit();
|
||||
|
||||
// Process resolved exports (these have full resolution info)
|
||||
for (resolved_exports) |resolved| {
|
||||
const key = try std.fmt.allocPrint(allocator, "{s}@{}", .{resolved.export_alias, resolved.source});
|
||||
if (seen.contains(key)) continue;
|
||||
try seen.put(key, {});
|
||||
|
||||
var chain_links = std.ArrayList(ChainLink).init(allocator);
|
||||
|
||||
// Add export link
|
||||
try chain_links.append(.{
|
||||
.file_index = resolved.source,
|
||||
.symbol_name = resolved.export_alias,
|
||||
.symbol_ref = resolved.target_ref orelse "unresolved",
|
||||
.link_type = if (resolved.target_source != null and resolved.target_source.? != resolved.source) "re-export" else "export",
|
||||
});
|
||||
|
||||
// If it's a re-export, trace to the target
|
||||
if (resolved.target_source) |target| {
|
||||
if (target != resolved.source) {
|
||||
// Find the original symbol name at target
|
||||
const original_name = resolved.export_alias;
|
||||
if (resolved.target_ref) |ref| {
|
||||
// Parse ref to get indices
|
||||
// Format: "Ref[inner=X, src=Y, .symbol]"
|
||||
// This is a simplified extraction - in production would need proper parsing
|
||||
if (std.mem.indexOf(u8, ref, "inner=")) |inner_start| {
|
||||
if (std.mem.indexOf(u8, ref[inner_start..], ",")) |_| {
|
||||
// Get symbol from graph if possible
|
||||
// For now, just mark it as imported
|
||||
try chain_links.append(.{
|
||||
.file_index = target,
|
||||
.symbol_name = original_name,
|
||||
.symbol_ref = ref,
|
||||
.link_type = "import",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try chains.append(.{
|
||||
.export_name = resolved.export_alias,
|
||||
.source_file = resolved.source,
|
||||
.chain = try chain_links.toOwnedSlice(),
|
||||
.has_conflicts = resolved.potentially_ambiguous,
|
||||
.conflict_sources = if (resolved.potentially_ambiguous)
|
||||
try allocator.dupe(u32, &[_]u32{resolved.source})
|
||||
else null,
|
||||
});
|
||||
}
|
||||
|
||||
// Also process direct exports that might not be in resolved
|
||||
for (exports) |exp| {
|
||||
const key = try std.fmt.allocPrint(allocator, "{s}@{}", .{exp.name, exp.source});
|
||||
if (seen.contains(key)) continue;
|
||||
try seen.put(key, {});
|
||||
|
||||
var chain_links = std.ArrayList(ChainLink).init(allocator);
|
||||
try chain_links.append(.{
|
||||
.file_index = exp.source,
|
||||
.symbol_name = exp.original_symbol_name orelse exp.name,
|
||||
.symbol_ref = exp.ref,
|
||||
.link_type = "export",
|
||||
});
|
||||
|
||||
try chains.append(.{
|
||||
.export_name = exp.name,
|
||||
.source_file = exp.source,
|
||||
.chain = try chain_links.toOwnedSlice(),
|
||||
.has_conflicts = false,
|
||||
.conflict_sources = null,
|
||||
});
|
||||
}
|
||||
|
||||
return try chains.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn generateVisualizerHTML(allocator: std.mem.Allocator, output_dir: []const u8, timestamp: i64) !void {
|
||||
// Generate original graph visualizer
|
||||
const graph_html = @embedFile("./graph_visualizer.html");
|
||||
const graph_filename = try std.fmt.allocPrint(allocator, "{s}/visualizer_{d}.html", .{
|
||||
output_dir,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
const graph_file = try std.fs.cwd().createFile(graph_filename, .{});
|
||||
defer graph_file.close();
|
||||
try graph_file.writeAll(graph_html);
|
||||
|
||||
debug("Graph visualizer HTML written to: {s}", .{graph_filename});
|
||||
|
||||
// Generate code flow visualizer
|
||||
const flow_html = @embedFile("./code_flow_visualizer.html");
|
||||
const flow_filename = try std.fmt.allocPrint(allocator, "{s}/code_flow_{d}.html", .{
|
||||
output_dir,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
const flow_file = try std.fs.cwd().createFile(flow_filename, .{});
|
||||
defer flow_file.close();
|
||||
try flow_file.writeAll(flow_html);
|
||||
|
||||
debug("Code flow visualizer HTML written to: {s}", .{flow_filename});
|
||||
}
|
||||
};
|
||||
@@ -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:
|
||||
|
||||
@@ -258,6 +258,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,
|
||||
.{
|
||||
|
||||
@@ -488,6 +488,16 @@ pub fn toAST(
|
||||
value: Type,
|
||||
) 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 => {
|
||||
@@ -536,7 +546,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 = .init(exprs) }, logger.Loc.Empty);
|
||||
},
|
||||
else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
|
||||
},
|
||||
@@ -548,9 +558,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 = .init(exprs) }, logger.Loc.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
const fields: []const std.builtin.Type.StructField = Struct.fields;
|
||||
var properties = try allocator.alloc(js_ast.G.Property, fields.len);
|
||||
var property_i: usize = 0;
|
||||
|
||||
Reference in New Issue
Block a user