Skip to main content
This example demonstrates how to build a pipeline that transforms minified JavaScript into readable code, mixing LLM-powered analysis with deterministic transformations. It showcases Thinkwell’s core pattern: use deterministic tools where they excel, and delegate semantic understanding to the AI agent.

What the Unminifier Does

The unminifier takes minified JavaScript (like underscore-umd-min.js) and produces readable code through five steps:
  1. Pretty-print with Prettier (deterministic)
  2. Convert UMD to ESM with ast-grep pattern matching (deterministic)
  3. Extract functions - identify all top-level functions with minified names (LLM)
  4. Analyze functions - suggest descriptive names for each function in parallel batches (LLM)
  5. Apply renames with Babel’s scope-aware renaming (deterministic)
This pipeline illustrates a key insight: some transformations are purely mechanical (formatting, module conversion, renaming), while others require semantic understanding (identifying and naming functions). Thinkwell lets you mix both approaches naturally.

The Schema Definitions

The LLM steps (3 and 4) use structured outputs defined with the @JSONSchema annotation:
Notice how each schema serves the LLM steps:
  • FunctionList uses a nested structure to return multiple items
  • FunctionAnalysis includes a confidence field with literal union types for quality signals
  • FunctionAnalysisBatch wraps 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:
This demonstrates an important pattern: use deterministic tools where they excel. Prettier handles formatting perfectly — there’s no need to involve an LLM.

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:
The ast-grep pattern !($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:
The structured 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:
Key patterns here:
  • p-limit avoids the risk of rate-limiting or API bans
  • _.chunk groups functions into manageable batches
  • FunctionAnalysisBatch returns 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:
Babel’s 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

Sample output:

Key Takeaways

  1. Use deterministic tools where they excel - Prettier for formatting, ast-grep for structural rewrites, Babel for scope-aware renaming
  2. Reserve the LLM for semantic understanding - only steps 3 and 4 (function extraction and naming) actually need AI
  3. Parallel processing with p-limit keeps LLM-powered steps efficient without exhausting account limits
  4. Batch schemas like FunctionAnalysisBatch reduce round trips for repeated LLM operations
  5. The .code() method clearly delineates code blocks in prompts
  6. Deterministic steps are faster and more reliable - the ast-grep rewrite runs in ~130ms vs ~200s for LLM regeneration