Skip to content

Instantly share code, notes, and snippets.

@JonathanXDR
Last active October 12, 2024 11:00
Show Gist options
  • Save JonathanXDR/feb0c394bd47534add80bf46cd08ca33 to your computer and use it in GitHub Desktop.
Save JonathanXDR/feb0c394bd47534add80bf46cd08ca33 to your computer and use it in GitHub Desktop.
A Bun script, which extracts JavaScript modules and their dependencies from a bundle.
interface ModuleInfo {
[index: number]: [Function, { [key: string]: number }];
}
interface DependencyNode {
moduleId: number;
dependencies: DependencyNode[];
}
async function parseModules(jsContent: string): Promise<ModuleInfo> {
// Extract the modules object
const modulesMatch = jsContent.match(/\)\(\s*\{([\s\S]*?)\},\s*\{\},\s*\[/);
if (!modulesMatch) {
throw new Error('Could not find modules object in the JavaScript file.');
}
const modulesStr = '{' + modulesMatch[1] + '}';
// Evaluate the modules object safely using Function
const modulesFunc = new Function('return ' + modulesStr);
const modules = modulesFunc() as ModuleInfo;
console.log(`Parsed ${Object.keys(modules).length} modules.`);
return modules;
}
function buildDependencyGraph(modules: ModuleInfo): Record<number, number[]> {
const graph: Record<number, number[]> = {};
for (const moduleIdStr in modules) {
const moduleId = parseInt(moduleIdStr, 10);
const moduleInfo = modules[moduleId];
if (!Array.isArray(moduleInfo)) {
console.warn(`Module ${moduleId} info is not an array.`);
graph[moduleId] = [];
continue;
}
const dependencies = moduleInfo[1] || {};
if (typeof dependencies !== 'object') {
console.warn(`Module ${moduleId} dependencies is not an object.`);
graph[moduleId] = [];
continue;
}
const depValues = Object.values(dependencies).filter(
(depId): depId is number => depId != null
);
graph[moduleId] = depValues;
}
return graph;
}
function buildDependencyTree(
moduleId: number,
graph: Record<number, number[]>,
visited: Set<number> = new Set()
): DependencyNode {
visited.add(moduleId);
const dependencies = graph[moduleId] || [];
const dependencyNodes: DependencyNode[] = [];
for (const depId of dependencies) {
if (!visited.has(depId)) {
const childNode = buildDependencyTree(depId, graph, visited);
dependencyNodes.push(childNode);
} else {
// Handle circular dependencies
dependencyNodes.push({
moduleId: depId,
dependencies: [],
});
}
}
return {
moduleId,
dependencies: dependencyNodes,
};
}
function printDependencyTree(
node: DependencyNode,
modules: ModuleInfo,
indent: string = ''
): void {
const moduleName = getModuleName(node.moduleId, modules);
console.log(
`${indent}- Module ${node.moduleId}${
moduleName ? ` (${moduleName})` : ''
}`
);
for (const child of node.dependencies) {
printDependencyTree(child, modules, indent + ' ');
}
}
function getModuleName(moduleId: number, modules: ModuleInfo): string | null {
const moduleInfo = modules[moduleId];
if (!moduleInfo) return null;
// Try to extract module name from the function code
const moduleFunc = moduleInfo[0];
const funcString = moduleFunc.toString();
const match = funcString.match(/e\.exports\s*=\s*(\w+)/);
if (match) {
return match[1];
}
return null;
}
async function extractComponentCode(
modules: ModuleInfo,
moduleId: number,
dependencies: Set<number>,
processedModules: Set<number>
): Promise<string> {
let code = '';
const allModuleIds = [moduleId, ...Array.from(dependencies)].sort(
(a, b) => a - b
);
for (const id of allModuleIds) {
if (processedModules.has(id)) {
continue; // Skip modules that have already been processed
}
const moduleInfo = modules[id];
if (!moduleInfo) {
console.warn(`Module ${id} not found in modules.`);
continue;
}
const moduleFunc = moduleInfo[0];
const moduleCode = moduleFunc.toString();
code += `\n// Module ID: ${id}\n${moduleCode}\n`;
processedModules.add(id);
}
return code;
}
// Helper function to collect all dependencies from the dependency tree
function collectDependencies(
node: DependencyNode,
collected: Set<number> = new Set()
): Set<number> {
for (const child of node.dependencies) {
if (!collected.has(child.moduleId)) {
collected.add(child.moduleId);
collectDependencies(child, collected);
}
}
return collected;
}
// Helper function to parse arguments into options and params
function parseArguments(args: string[]) {
const options: Record<string, string | boolean> = {};
const params: string[] = [];
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg.startsWith('--')) {
const optionName = arg.substring(2);
let optionValue: string | boolean = true;
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
optionValue = args[i + 1];
i += 1;
}
options[optionName] = optionValue;
} else {
params.push(arg);
}
i += 1;
}
return { options, params };
}
async function main() {
// Parse command-line arguments
const args = process.argv.slice(2);
if (args.length < 2) {
console.error(
'Usage: bun extract_components.ts [options] <path_to_js_bundle> <component_module_id[:component_name]> [<component_module_id[:component_name]> ...]'
);
console.error('Options:');
console.error(' --show-tree Display the dependency tree for each component');
console.error(' --output <filename> Output all modules into a single JS file');
console.error(
'Example: bun extract_components.ts --show-tree --output all_modules.js main.built.js 404:SequencePlayer 399:HeroSequence'
);
process.exit(1);
}
const { options, params } = parseArguments(args);
const showTree = !!options['show-tree'];
const outputFileName = options['output'] ? String(options['output']) : null;
if (params.length < 2) {
console.error('Error: Missing required arguments.');
process.exit(1);
}
const jsFilePath = params[0];
try {
// Check if the file exists
const jsFileExists = await Bun.file(jsFilePath).exists();
if (!jsFileExists) {
console.error(`Error: File '${jsFilePath}' does not exist.`);
process.exit(1);
}
} catch (error) {
console.error(`Error accessing file '${jsFilePath}':`, error);
process.exit(1);
}
// Read the JavaScript bundle using Bun's API
const jsFile = Bun.file(jsFilePath);
const jsContent = await jsFile.text();
// Parse modules from the JavaScript bundle
const modules = await parseModules(jsContent);
// Build the dependency graph
const dependencyGraph = buildDependencyGraph(modules);
// Parse component module IDs and names from arguments
const componentModuleMap: Record<string, number> = {};
for (const arg of params.slice(1)) {
const [moduleIdStr, componentName] = arg.split(':');
const moduleId = parseInt(moduleIdStr, 10);
if (isNaN(moduleId)) {
console.error(`Invalid module ID: ${moduleIdStr}`);
continue;
}
const name = componentName || `Component_${moduleId}`;
componentModuleMap[name] = moduleId;
}
let accumulatedCode = '';
const globalProcessedModules = outputFileName ? new Set<number>() : null;
for (const [componentName, moduleId] of Object.entries(componentModuleMap)) {
console.log(
`\n=== Processing component '${componentName}' (module ID: ${moduleId}) ===`
);
// Build the dependency tree
const dependencyTree = buildDependencyTree(moduleId, dependencyGraph);
if (showTree) {
console.log(`\nDependency Tree for module ${moduleId}:`);
printDependencyTree(dependencyTree, modules);
}
// Collect all dependencies into a flat set
const dependencies = collectDependencies(dependencyTree);
console.log(`\nTotal dependencies for '${componentName}': ${dependencies.size}`);
console.log(`Module IDs: ${[moduleId, ...Array.from(dependencies)].join(', ')}`);
// Use processedModules accordingly
const processedModules = outputFileName ? globalProcessedModules! : new Set<number>();
// Extract the code for the component and its dependencies
const componentCode = await extractComponentCode(
modules,
moduleId,
dependencies,
processedModules
);
if (outputFileName) {
accumulatedCode += `\n/* Component: ${componentName} (Module ID: ${moduleId}) */\n`;
accumulatedCode += componentCode + '\n';
} else {
// Save the code to a file using Bun's API
const filename = `${componentName}.js`;
await Bun.write(filename, componentCode);
console.log(`Extracted code for '${componentName}' to '${filename}'`);
console.log('-------------------------------------------');
}
}
if (outputFileName) {
await Bun.write(outputFileName, accumulatedCode);
console.log(`\nExtracted code for all components to '${outputFileName}'`);
}
}
main().catch((error) => {
console.error('An error occurred:', error);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment