Skip to content

Instantly share code, notes, and snippets.

@paralin
Last active July 6, 2025 02:36
Show Gist options
  • Select an option

  • Save paralin/9de2163433cc82ec93951ffd304b3c87 to your computer and use it in GitHub Desktop.

Select an option

Save paralin/9de2163433cc82ec93951ffd304b3c87 to your computer and use it in GitHub Desktop.
analyze esbuild metadata.json
// find-importers.js
const fs = require('fs/promises');
/**
* Finds and lists all modules that import any module whose path contains a specific substring.
*
* @param {string} metafilePath - Path to the esbuild meta.json file.
* @param {string} targetSubstring - The substring to match in the imported paths (e.g., "/multiformats/dist").
*/
async function findImporters(metafilePath, targetSubstring) {
try {
// 1. Validate inputs
if (!metafilePath || !targetSubstring) {
throw new Error('Both a metafile path and a target substring are required.');
}
// 2. Read and parse the metafile
console.log(`🔍 Reading metadata from: ${metafilePath}`);
const metafileContent = await fs.readFile(metafilePath, 'utf-8');
const meta = JSON.parse(metafileContent);
if (!meta.inputs) {
throw new Error('Metafile does not contain a valid "inputs" key.');
}
// A Set is used to automatically handle duplicate entries
const importers = new Set();
const matchedImports = new Set();
// 3. Iterate through all modules in the bundle
for (const importerPath in meta.inputs) {
const moduleDetails = meta.inputs[importerPath];
// Check if this module has any imports
if (moduleDetails.imports) {
// Check each import of the current module
for (const imp of moduleDetails.imports) {
// If the imported path contains the target substring...
if (imp.path.includes(targetSubstring)) {
// ...then the current module is an "importer" we care about.
importers.add(importerPath);
matchedImports.add(imp.path)
}
}
}
}
// 4. Report the results
if (importers.size === 0) {
console.log(`\n✅ No modules were found importing anything from "${targetSubstring}".`);
} else {
// Convert Set to Array and sort for consistent, readable output
const sortedImporters = Array.from(importers).sort();
console.log(`\nFound ${matchedImports.size} module(s) matching *"${targetSubstring}"*`);
console.log(`--------------------------------------------------`);
console.log(`These are imported by the following ${sortedImporters.length} module(s):`);
sortedImporters.forEach(importer => {
console.log(` -> ${importer}`);
});
}
} catch (error) {
console.error(`\n❌ ERROR: ${error.message}`);
process.exit(1);
}
}
// --- Main execution block ---
const args = process.argv.slice(2);
if (args.length !== 2) {
console.log('Usage: node find-importers.js <path/to/meta.json> <substring-to-find>');
console.log('Example: node find-importers.js ./meta.json "/multiformats/dist"');
process.exit(1);
}
const [metafilePath, targetSubstring] = args;
findImporters(metafilePath, targetSubstring);
// generate-graph.js
const fs = require('fs/promises');
const path = require('path');
/**
* Generates an HTML file with a dependency graph from an esbuild metafile.
* @param {string} metafilePath - Path to the esbuild meta.json file.
* @param {string} outputHtmlPath - Path for the output HTML file.
*/
async function createGraph(metafilePath, outputHtmlPath) {
try {
// 1. Read and parse the metafile
console.log(`Reading metafile: ${metafilePath}`);
const metafileContent = await fs.readFile(metafilePath, 'utf-8');
const meta = JSON.parse(metafileContent);
if (!meta.inputs) {
throw new Error('Metafile does not contain an "inputs" key.');
}
const nodes = [];
const edges = [];
const nodeIds = new Set();
// 2. Process inputs to create nodes and edges
for (const filePath in meta.inputs) {
const details = meta.inputs[filePath];
const nodeId = filePath;
// Add a node for the file if it doesn't exist
if (!nodeIds.has(nodeId)) {
nodes.push({
id: nodeId,
label: path.basename(nodeId), // Use the filename as the label
title: `${nodeId}\n${details.bytes} bytes`, // Tooltip
value: details.bytes, // Use bytes for node sizing
});
nodeIds.add(nodeId);
}
// Add edges for its imports
if (details.imports) {
for (const imp of details.imports) {
edges.push({
from: nodeId, // The file doing the importing
to: imp.path, // The file being imported
arrows: 'to', // Draw a directed arrow
});
}
}
}
console.log(`Found ${nodes.length} files and ${edges.length} imports.`);
// 3. Generate the HTML content
const htmlContent = generateHtml(nodes, edges);
// 4. Write the output file
await fs.writeFile(outputHtmlPath, htmlContent);
console.log(`✅ Success! Graph saved to ${outputHtmlPath}`);
} catch (error) {
console.error(`❌ Error: ${error.message}`);
process.exit(1);
}
}
/**
* Creates the full HTML page content.
* @param {Array} nodes - Array of node objects for vis-network.
* @param {Array} edges - Array of edge objects for vis-network.
* @returns {string} - A complete HTML string.
*/
function generateHtml(nodes, edges) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESBuild Dependency Graph</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style type="text/css">
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f7f7f7;
}
#graph-container {
width: 100%;
height: 100%;
border: 1px solid lightgray;
}
.info {
position: absolute;
top: 10px;
left: 10px;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
z-index: 10;
}
</style>
</head>
<body>
<div class="info">
<strong>ESBuild Dependency Graph</strong><br>
Nodes represent files, and edges represent imports.
</div>
<div id="graph-container"></div>
<script type="text/javascript">
// 1. Create data sets
const nodes = new vis.DataSet(${JSON.stringify(nodes, null, 2)});
const edges = new vis.DataSet(${JSON.stringify(edges, null, 2)});
// 2. Specify the container
const container = document.getElementById('graph-container');
// 3. Provide the data
const data = {
nodes: nodes,
edges: edges,
};
// 4. Set the options
const options = {
nodes: {
shape: 'box',
margin: 10,
font: {
size: 14,
color: '#333'
},
borderWidth: 1,
color: {
border: '#cccccc',
background: '#ffffff',
highlight: {
border: '#007bff',
background: '#e6f2ff'
}
},
// Node size scales with file size (bytes)
scaling: {
min: 20,
max: 100,
label: {
enabled: true,
min: 14,
max: 30
}
}
},
edges: {
width: 1,
color: {
color: '#999',
highlight: '#007bff'
},
smooth: {
type: 'cubicBezier',
forceDirection: 'vertical',
roundness: 0.4
}
},
layout: {
// A hierarchical layout is perfect for a DAG
hierarchical: {
direction: 'UD', // Up-Down
sortMethod: 'directed', // Tries to point edges downwards
shakeTowards: 'roots',
},
},
interaction: {
dragNodes: true,
zoomView: true,
dragView: true,
tooltipDelay: 200,
},
physics: {
// Disable physics for a static hierarchical layout
enabled: false,
},
};
// 5. Initialize the network
const network = new vis.Network(container, data, options);
</script>
</body>
</html>
`;
}
// --- Main execution ---
const args = process.argv.slice(2);
if (args.length !== 1) {
console.log('Usage: node generate-graph.js <path/to/metafile.json>');
process.exit(1);
}
const metafilePath = args[0];
const outputHtmlPath = 'graph.html';
createGraph(metafilePath, outputHtmlPath);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment