← Back to portfolio
OCT 2026 · 8 MIN READ CRITICAL REVIEW / ARCHITECTURE

Can You Run a Code Generator Twice? A Critical Review of Jostraca

Code generators tend to follow an unwritten rule: they run once during project setup, or they own the target folder completely. The moment a developer edits a generated file by hand, automated regeneration becomes destructive. Re-running the tool either wipes out manual edits or requires complicated configuration to protect specific lines.

Jostraca takes direct aim at this dilemma. Created by Richard Rodger and used beneath the Voxgig SDK generator, Jostraca bills itself as “a code generator you can run twice.” Its central pitch is that because it builds a complete in-memory model of the target file tree before writing anything to disk, it can compare intended output against existing files and apply one of five reconciliation strategies: write, preserve, present, diff, or three-way merge.

What Jostraca Gets Right

The define-build split avoids custom templating DSLs, keeping generator logic testable with native JS functions and in-memory tree models.

Where The Thesis Breaks Down

Text-level three-way merging cannot understand AST semantics, and gitignored .jostraca/ caches fail in ephemeral CI environments.

The promise of automated three-way merging between generated code and manual edits is bold. Having examined how Jostraca works in practice, its mechanics reveal genuine strengths as well as distinct operational limits.

The Define-Build Split

Most file-generation scripts interleave calculation with disk input and output. A loop iterates over a schema, formats a string, and calls fs.writeFileSync. Jostraca separates this process into two distinct phases.

During the define phase, Jostraca executes a JavaScript callback where components represent structural filesystem entities. Nothing touches the storage layer yet:

define-phase.js JavaScript
import { Jostraca, Project, Folder, File, Content } from 'jostraca'

const jostraca = Jostraca()

await jostraca.generate({ folder: './dist' }, () => {
  Project({ folder: 'service' }, () => {
    File({ name: 'package.json' }, () => {
      Content(JSON.stringify({ name: 'service', version: '1.0.0' }, null, 2))
    })
    Folder({ name: 'src' }, () => {
      File({ name: 'index.js' }, () => {
        Content('console.log("service ready");\n')
      })
    })
  })
})

The component vocabulary is small. Ten primitives (Project, Folder, File, Content, Line, Fragment, Slot, Inject, Copy, and List) represent directories, files, and text chunks. Because components are standard JavaScript function calls rather than template tags, loops and conditionals rely on regular language features:

dynamic-routes.js JavaScript
import { Jostraca, Project, File, Content } from 'jostraca'

const routes = [
  { path: '/health', method: 'GET' },
  { path: '/users', method: 'POST' }
]

await Jostraca().generate({ folder: './dist' }, () => {
  Project({}, () => {
    for (const route of routes) {
      File({ name: `${route.path.slice(1)}.js` }, () => {
        Content(`export const method = "${route.method}";\n`)
      })
    }
  })
})

This approach avoids inventing another template language. There is no custom expression evaluator, and standard linters, formatters, and debuggers operate on the generator code without friction.

Merging Across Generations

The distinguishing capability of Jostraca is its handling of existing files. When configured with merge: true, Jostraca stores a copy of its output in a local .jostraca/ directory during each run. On subsequent executions, it uses that copy as a common base to perform a three-way merge against the current file on disk:

three-way-merge.js JavaScript
import { appendFileSync } from 'node:fs'
import { Jostraca, Project, File, Content } from 'jostraca'

const jostraca = Jostraca({
  existing: { sh: { write: true, merge: true } }
})

const writeConfig = (port) =>
  jostraca.generate({ folder: './out' }, () => {
    Project({}, () => {
      File({ name: 'env.sh' }, () => {
        Content(`PORT=${port}\nHOST=0.0.0.0\n`)
      })
    })
  })

// First run: writes base configuration
await writeConfig(3000)

// Developer appends a manual setting
appendFileSync('./out/env.sh', 'METRICS_ENABLED=true\n')

// Second run: generator updates port to 4000
await writeConfig(4000)

After the second run, ./out/env.sh contains:

./out/env.sh Output
PORT=4000
HOST=0.0.0.0
METRICS_ENABLED=true

Jostraca identified that PORT changed in the generator, while METRICS_ENABLED was added by the developer, combining both modifications without intervention.

Where the Abstraction Breaks Down

While the configuration snippet above resolves cleanly, applying three-way merges to programming language source code introduces major complications.

1. Line-Oriented Diffing Versus Syntax Trees

Jostraca relies on standard text-level three-way merging. Text merges work adequately for independent configuration keys or append-only log files. However, programming languages are structured as abstract syntax trees with lexical scopes, imports, and grammatical constraints.

Consider a TypeScript file where a developer modifies an inner function body, while an updated generator changes the enclosing class signature or imports:

order-service-evolution.ts TypeScript
// Initial generated code
export class OrderService {
  process(order: Order) {
    return repo.save(order)
  }
}

// Developer adds validation inside process()
export class OrderService {
  process(order: Order) {
    if (!order.id) throw new Error("Missing ID")
    return repo.save(order)
  }
}

// Generator updates signature to support context
export class OrderService {
  async process(order: Order, ctx: Context) {
    return repo.save(order, ctx)
  }
}

A line-level merge tool cannot parse that repo.save now requires ctx, or whether the developer's asynchronous checks align with the updated return type. In complex scenarios, the merge tool either inserts Git conflict markers (<<<<<<<, =======) directly into the file or silently creates code that fails compilation.

This explains why even the worked examples in the Voxgig SDK repository switch off three-way merges in favor of explicit overwrites. In production SDK generation, deterministic overwrites combined with clean file separation prove far more dependable than probabilistic text merges.

2. State Retention and Ephemeral Environments

Jostraca's merge mode depends entirely on the contents of the local .jostraca/ directory. That directory stores the baseline text from the prior run.

Because .jostraca/ is typically added to .gitignore, it does not persist across machines. When another developer clones the repository, or when a continuous integration runner executes a build in a fresh container, no merge base exists. Without a base snapshot, Jostraca cannot distinguish between an intentional manual edit and stale generated code. The merge either degrades to a standard overwrite or requires manual conflict resolution.

A code generation workflow that functions differently on an individual workstation than it does in a clean continuous integration checkout creates maintenance hazards for teams.

3. Implicit Context in Callbacks

Jostraca builds its tree using nested function callbacks:

callback-nesting.js JavaScript
Project({}, () => {
  Folder({ name: 'src' }, () => {
    File({ name: 'index.ts' }, () => {
      Content('export const active = true;')
    })
  })
})

These component calls do not accept parent references as explicit arguments. Instead, they rely on an implicit execution context maintained by the library during the callback loop.

If a developer introduces an asynchronous call or a rejected promise inside one of these callbacks, the execution context can become detached. Constructing trees via pure data structures or returning explicit node objects would make composition and error handling more predictable.

4. String Substitution Without Formatting Awareness

Jostraca encourages embedding markers inside comment syntax so that template files remain valid source:

slot-markers.html & slot-markers.ts Template Slots
<!-- <[SLOT:meta]> -->
// <[SLOT:imports]>

Keeping template files syntactically valid allows editor linters to inspect them. However, replacing slot markers with arbitrary strings frequently corrupts indentation and whitespace. Inserting a multi-line code fragment into a slot often yields mismatched tabs or misaligned indentation levels. To produce acceptable code, developers must run a secondary formatter like Prettier or gofmt after every generation step.

SYNTHESIS & VERDICT

Assessment

Jostraca gets several fundamentals right. Avoiding a custom templating language in favor of standard JavaScript and Go keeps generator logic testable and straightforward. The define-build separation provides clean visibility into what a generator intends to write before filesystem changes take place. For scaffolding static file hierarchies or applying discrete string updates across structured folders, the library is fast and coherent.

The difficulty lies in its central thesis: that automated three-way merging solves the second-run problem for source code. In practice, merging arbitrary developer changes into regenerated code requires semantic understanding of the target language, not line-based diffing. Teams generating code across multiple releases generally fare better by keeping generated files strictly separated from handwritten extension hooks, rather than attempting to merge both into the same file on disk.

Reetik Rana

Software Engineer focusing on AI systems, compilers & developer tooling. Written in review of Voxgig / Jostraca architecture.

← Return to portfolio