What the Unminifier Does
The unminifier takes minified JavaScript (likeunderscore-umd-min.js) and produces readable code through five steps:
- Pretty-print with Prettier (deterministic)
- Convert UMD to ESM with ast-grep pattern matching (deterministic)
- Extract functions - identify all top-level functions with minified names (LLM)
- Analyze functions - suggest descriptive names for each function in parallel batches (LLM)
- Apply renames with Babel’s scope-aware renaming (deterministic)
The Schema Definitions
The LLM steps (3 and 4) use structured outputs defined with the@JSONSchema annotation:
FunctionListuses a nested structure to return multiple itemsFunctionAnalysisincludes aconfidencefield with literal union types for quality signalsFunctionAnalysisBatchwraps multiple analyses for efficient batched processing
Step 1: Pretty-Print with Prettier
The first step is purely mechanical - no LLM required. Use Prettier to make the minified code readable:Step 2: Convert UMD to ESM
UMD boilerplate follows a predictable pattern, so there’s no need for an LLM here. We use ast-grep to match the UMD wrapper and rewrite it in a single pass:!($IIFE)(this, function () { $$$BODY return $RET; }) matches the UMD IIFE wrapper. The $$$BODY metavariable captures all statements inside, and $RET captures the return value. The rewrite replaces the entire wrapper with just the body plus export default $RET;.
Once again, this demonstrates the benefits of using deterministic tools where they excel: ast-grep is convenient and effective for matching simple syntactic patterns. Not only that, it’s highly efficient and cheaper; transforming a large source file like Underscore with an LLM chews through quite a few tokens.
Step 3: Extract Function List
Next, ask the agent to identify all functions that need renaming:FunctionList output gives you an array you can iterate over in TypeScript - bridging the gap between natural language analysis and programmatic control flow.
Step 4: Parallel Batch Analysis
Analyzing functions one at a time would be slow. Instead, batch them and process in parallel with a concurrency limit:p-limitavoids the risk of rate-limiting or API bans_.chunkgroups functions into manageable batchesFunctionAnalysisBatchreturns multiple analyses per request, reducing round trips- Sending full source code as context helps the LLM understand function interdependencies
Step 5: Apply Renames
Renaming is another task that doesn’t need an LLM — Babel’s scope-aware renaming handles it correctly and instantly:scope.rename() is scope-aware, so it correctly renames function definitions and all their usages without touching unrelated identifiers that happen to share the same name. This is more reliable than asking an LLM to regenerate the entire file — and runs in milliseconds.
The Complete Pipeline
Here’s how it all fits together — notice how LLM and deterministic steps interleave naturally:Running the Example
Key Takeaways
- Use deterministic tools where they excel - Prettier for formatting, ast-grep for structural rewrites, Babel for scope-aware renaming
- Reserve the LLM for semantic understanding - only steps 3 and 4 (function extraction and naming) actually need AI
- Parallel processing with
p-limitkeeps LLM-powered steps efficient without exhausting account limits - Batch schemas like
FunctionAnalysisBatchreduce round trips for repeated LLM operations - The
.code()method clearly delineates code blocks in prompts - Deterministic steps are faster and more reliable - the ast-grep rewrite runs in ~130ms vs ~200s for LLM regeneration
