Last active
October 12, 2024 11:00
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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