#!/usr/bin/env bun /** * Extract source files from sourcemaps into proper directory structure * * Usage: * bun scripts/extract-sources.ts [options] * * Options: * --output, -o Output directory (default: anyclip) * --input, -i Sourcemaps directory (default: sourcemaps) * --verbose, -v Verbose output * --no-clean Don't delete output directory first */ import { mkdir, rm, stat, unlink } from "fs/promises"; import { dirname, join, normalize, basename } from "path"; import { parseArgs } from "util"; // Parse CLI arguments const { values: args } = parseArgs({ args: Bun.argv.slice(2), options: { output: { type: "string", short: "o", default: "anyclip" }, input: { type: "string", short: "i", default: "sourcemaps" }, verbose: { type: "boolean", short: "v", default: false }, "no-clean": { type: "boolean", default: false }, }, }); const OUTPUT_DIR = args.output!; const INPUT_DIR = args.input!; const VERBOSE = args.verbose!; const NO_CLEAN = args["no-clean"]!; interface SourceMap { version: number; sources: string[]; sourcesContent: (string | null)[]; names?: string[]; mappings?: string; } // Collect all files first, then write (to handle conflicts) const filesToWrite = new Map(); /** * Sanitize a source path from a sourcemap * Inspired by sourcemapper and shuji */ function sanitizePath(source: string): string | null { let cleanPath = source // Remove webpack:// prefix (webpack://package-name/path or webpack:///path) .replace(/^webpack:\/\/[^/]*\//, "") // Remove leading ./ .replace(/^\.\//, "") // Remove query strings (?v=123, ?module, etc) .replace(/\?[^/]*$/, "") // Remove Windows-illegal characters .replace(/[?%*|:"<>]/g, "") // Replace spaces with hyphens .replace(/ /g, "-"); // Skip empty paths if (!cleanPath || cleanPath === "") return null; // Skip node_modules and external dependencies if (cleanPath.includes("node_modules") || cleanPath.startsWith("external ")) { return null; } // Handle (webpack) paths - put in webpack/ directory if (cleanPath.includes("(webpack)")) { cleanPath = "webpack/" + cleanPath.replace(/\(webpack\)\//g, ""); } // Normalize path: resolve ../ sequences safely // Prepend a fake root to prevent escaping, then normalize and strip it const withRoot = "/" + cleanPath; const normalized = normalize(withRoot).slice(1); // Remove leading / // Handle webpack runtime files (no extension) - add .js if (normalized.startsWith("webpack/") && !basename(normalized).includes(".")) { return normalized + ".js"; } // Skip paths without file extension (likely directories) if (!basename(normalized).includes(".")) { return null; } return normalized; } async function extractSourcemap(mapFile: string): Promise { if (VERBOSE) console.log(`Processing ${mapFile}...`); const fileContent = await Bun.file(mapFile).text(); let sourcemap: SourceMap; try { sourcemap = JSON.parse(fileContent); } catch (e) { if (VERBOSE) console.log(` Failed to parse: ${e}`); return 0; } if (!sourcemap.sources || !sourcemap.sourcesContent) { if (VERBOSE) console.log(` No sources found`); return 0; } let collected = 0; for (let i = 0; i < sourcemap.sources.length; i++) { const source = sourcemap.sources[i]; const content = sourcemap.sourcesContent[i]; if (!content) continue; const cleanPath = sanitizePath(source); if (!cleanPath) continue; const outputPath = join(OUTPUT_DIR, cleanPath); // Keep the larger file if there's a conflict (more complete source) if (!filesToWrite.has(outputPath) || content.length > filesToWrite.get(outputPath)!.length) { filesToWrite.set(outputPath, content); collected++; } } if (VERBOSE) console.log(` Collected ${collected} files`); return collected; } // Delete existing output directory (unless --no-clean) if (!NO_CLEAN) { console.log(`Cleaning ${OUTPUT_DIR}/...`); await rm(OUTPUT_DIR, { recursive: true, force: true }); } // Process all sourcemaps const sourcemaps = await Array.fromAsync(new Bun.Glob("*.map").scan(INPUT_DIR)); console.log(`Found ${sourcemaps.length} sourcemaps in ${INPUT_DIR}/`); for (const map of sourcemaps) { await extractSourcemap(join(INPUT_DIR, map)); } // Write all files console.log(`\nWriting ${filesToWrite.size} files to ${OUTPUT_DIR}/...`); let written = 0; let skipped = 0; let errors = 0; for (const [outputPath, content] of filesToWrite) { try { const dir = dirname(outputPath); // Handle file/directory conflicts const parts = dir.split("/"); let currentPath = ""; for (const part of parts) { currentPath = currentPath ? `${currentPath}/${part}` : part; try { const s = await stat(currentPath); if (s.isFile()) { await unlink(currentPath); } } catch { // Path doesn't exist yet } } await mkdir(dir, { recursive: true, mode: 0o755 }); await Bun.write(outputPath, content); written++; if (VERBOSE) console.log(` ✓ ${outputPath}`); } catch (e) { errors++; if (VERBOSE || errors <= 3) { console.log(` ✗ ${outputPath}: ${e}`); } } } if (!VERBOSE && errors > 3) { console.log(` ... and ${errors - 3} more errors`); } console.log(`\n✅ Extracted ${written} files to ${OUTPUT_DIR}/`); if (errors > 0) console.log(`⚠️ ${errors} errors`); // Show directory summary const dirs = new Set([...filesToWrite.keys()].map(p => p.split("/").slice(0, 2).join("/"))); console.log(`\nDirectories: ${[...dirs].sort().join(", ")}`);