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.
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')
})
})
})
}) 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.
<!doctype html>
<html>
<head>
<title>APP_TITLE</title>
</head>
<body>
<h1>APP_NAME</h1>
</body>
</html> 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',
},
})
})
})
}) 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.
<!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
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') 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 install jostraca@0.36.6 go get github.com/jostraca/jostraca/go MIT licensed. Source and issues are on GitHub.