Jostraca code generation, made repeatable

Code generation after the first run

Generate it now. Generate it again.

Regenerate a file tree after manual edits. Jostraca calculates the intended output before writing files, then applies the mode you choose: write, preserve, present, diff, or merge.

Use ordinary code for loops and conditions. Template markers can sit inside the target language’s comments, so the surrounding file remains valid HTML, Go, or SQL.

2 implementations of one specification: TypeScript and Go, held to the same shared test corpus.

Or straight to TypeScript or Go.

Declare a tree. Jostraca writes it.

Components nest like directories. Project contains Folder, which contains File, which contains Content. They are ordinary function calls, so loops, conditionals, and parameters are ordinary JavaScript too. There is no template language waiting to become your next programming language.

Every name here is a plain string, because a component call is a function call and you already have a language for that. Content that varies with your data comes from a model, which the tutorial introduces.

gen.mjs
import { Jostraca, Project, Folder, File, Content } from 'jostraca'

await Jostraca().generate({ folder: './out' }, () => {
  Project({ folder: 'acme' }, () => {
    File({ name: 'README.md' }, () => {
      Content('# Acme\n')
    })
    Folder({ name: 'src' }, () => {
      File({ name: 'index.js' }, () => {
        Content('console.log("acme")\n')
      })
    })
  })
})
what it wrote
out/acme/README.md
  # Acme
out/acme/src/index.js
  console.log("acme")

No template syntax to learn

Two ways to produce a file. Neither one is a dialect.

Write code

Components are function calls in the language you are already in. Iteration is for. Branching is if. Reuse is a function. Your debugger, type checker, formatter, and test runner keep working, because nothing here is new to them.

Or mark up a real file

When a file reads better as a file, keep it as one. A marker is just a name you chose, sitting where the file already allows text, so the template stays valid HTML: or Go, or SQL, or YAML.

tpl/page.html: an ordinary HTML file
<!doctype html>
<html>
  <head>
    <title>APP_TITLE</title>
  </head>
  <body>
    <h1>APP_NAME</h1>
  </body>
</html>
gen.mjs
import { Jostraca, Project, File, Fragment } from 'jostraca'

await Jostraca().generate({ folder: './out' }, () => {
  Project({}, () => {
    File({ name: 'index.html' }, () => {
      Fragment({
        from: '../tpl/page.html',
        replace: {
          APP_TITLE: 'acme',
          APP_NAME: 'Acme',
        },
      })
    })
  })
})
what it wrote
out/index.html
  <!doctype html>
  <html>
    <head>
      <title>acme</title>
    </head>
    <body>
      <h1>Acme</h1>
    </body>
  </html>

tpl/page.html is valid HTML. A browser renders it, a formatter formats it, a linter checks it, and your editor highlights it, because every character in it belongs to HTML. APP_TITLE and APP_NAME are ordinary text that happens to be replaced. For a file where a bare name would not be valid, the replace guide covers the marker forms that hide inside a comment.

The same page written in a template language needs tooling for that dialect. Rendering the template resolves its loops and substitutions into the final HTML.

the same page in a template language, shown as plain text
<!doctype html>
<html>
  <head>
    {% block head %}{% endblock %}
  </head>
  <body>
    <h1>{{ app.name }}</h1>
  </body>
</html>

Use comment markers when you want to edit and validate a template with the target language’s tools.

The second run

A generator that only ever runs on an empty directory is a project template. Jostraca is built for the other case: the output is checked in, somebody has edited it, and you need to regenerate anyway. 5 modes decide what happens to a file that already exists. Binaries get the first three, because text diffs and merges do not apply to binary files. The file extension selects the set.

write
overwrite the file (the default)
preserve
overwrite, keeping the old bytes beside it
present
leave the file alone and write the new version next to it (needs write: false)
diff
write an annotated two-way diff instead
merge
three-way merge against the last generate
two runs, with a hand edit in between
import { appendFileSync } from 'node:fs'
import { Jostraca, Project, File, Content } from 'jostraca'

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

const run = (body) => jostraca.generate({ folder: './out' }, () => {
  Project({}, () => File({ name: 'config.sh' }, () => Content(body)))
})

// First run. Then somebody adds a line by hand. Then the generator
// changes a different line and runs again.
await run('PORT=8080\nHOST=localhost\n')
appendFileSync('./out/config.sh', 'DEBUG=1\n')
await run('PORT=9090\nHOST=localhost\n')
what it wrote
out/config.sh
  PORT=9090
  HOST=localhost
  DEBUG=1

A file carrying the string JOSTRACA_PROTECT is never overwritten, whatever the mode says, so a reader can take a generated file away from the generator without asking you first. The options reference specifies each mode; the how-to guides show them in use.

10 components

That is the whole vocabulary. Anything else you need is a function you write yourself and wrap with cmp(), which makes it a component like the rest.

Project
the root of one generated tree
Folder
a directory, nested as deep as you like
File
a file, with an optional POSIX mode
Content
text, with model values substituted in
Line
text, with the newline supplied for you
Fragment
a template file read in from disk
Slot
a named region of a fragment you fill
Inject
content placed between markers in a file that exists
Copy
a file or a whole directory, templated on the way through
List
one block of content per item in an array

Every prop and every default is in the component reference.

Two implementations, one output

TypeScript is canonical and Go is a maintained port. They are held to byte-identical output for the same logical input by a shared, language-neutral test corpus that both suites read, so a behaviour documented here means the same thing in either language. Correct defects in TypeScript first where needed, then bring Go into parity.

The Go API, including the places where Go idiom requires a different structure, is in the Go reference.

Install

npm
npm install jostraca@0.36.6
Go
go get github.com/jostraca/jostraca/go

MIT licensed. Source and issues are on GitHub.