Files
tdir/README.md
Gregor Lohaus 0fd19254e9 eq directive
2026-04-15 22:37:13 +02:00

135 lines
3.7 KiB
Markdown

# tdir
Treat a directory as a template. File paths and contents support conditionals (`@if`/`@elseif`/`@else`) and variable substitution (`@var`). Provide a Zod schema and tdir validates it matches the template at setup time, then validates context at render time.
## Install
```sh
bun add @gregorlohaus/tdir zod
```
## Quick start
Given a template directory:
```
templates/
<@if(context.web)>web/
index.html
```
Where `index.html` contains:
```html
<html>
<@if(context.header.show)>
<head><@var(context.header.title)></head>
<@endif>
<body></body>
</html>
```
Render it:
```ts
import { initRenderer } from "tdir"
import { z } from "zod"
const createRenderer = initRenderer("./templates")
const render = createRenderer(z.object({
web: z.boolean(),
header: z.object({
show: z.boolean(),
title: z.string()
})
}))
render("./output", {
web: true,
header: { show: true, title: "Hello" }
})
// Creates: output/web/index.html with <head>Hello</head>
```
## Template directives
### In file contents
| Directive | Description |
|---|---|
| `<@if(context.x)>` | Conditional block — truthy check (must end with `<@endif>`) |
| `<@if(eq(context.x,"value"))>` | Conditional block — string equality check |
| `<@elseif(context.y)>` | Else-if branch (same forms as `@if`) |
| `<@else>` | Else branch |
| `<@endif>` | End conditional block |
| `<@var(context.x)>` | Substitute with context value (default type: `string`) |
| `<@var(context.x:number)>` | Substitute with explicit type |
### In directory/file names
| Directive | Description |
|---|---|
| `<@if(context.x)>dirname` | Conditionally include directory/file (truthy check) |
| `<@if(eq(context.x,"value"))>dirname` | Conditionally include by string equality |
| `<@var(context.x)>` | Dynamic directory/file name |
These can be combined: `<@if(context.web.create)><@var(context.web.dir)>` creates a directory named by `context.web.dir` only if `context.web.create` is true.
## Schema validation
`createRenderer` validates that your Zod schema matches the template variables. Mismatches throw `SchemaMismatchError`:
```ts
import { initRenderer, SchemaMismatchError } from "tdir"
import { z } from "zod"
const createRenderer = initRenderer("./templates")
// Template uses <@if(context.web)> which requires a boolean,
// but schema declares string -- throws SchemaMismatchError
createRenderer(z.object({
web: z.string(), // wrong type
header: z.object({ show: z.boolean(), title: z.string() })
}))
// SchemaMismatchError: Shema doesnt match used template variables: web: expected z.boolean() but schema has z.string()
// Schema is missing fields used in templates -- throws SchemaMismatchError
createRenderer(z.object({
web: z.boolean()
// missing header
}))
// SchemaMismatchError: Shema doesnt match used template variables: header: missing in schema
```
## Context validation
At render time, the context is validated by Zod. Invalid context throws `z.ZodError`:
```ts
const render = createRenderer(z.object({
web: z.boolean(),
header: z.object({ show: z.boolean(), title: z.string() })
}))
render("./output", {})
// ZodError: required at "web", required at "header"
render("./output", { web: "not a boolean", header: { show: true, title: "Hi" } })
// ZodError: expected boolean, received string at "web"
```
## Re-rendering
`render(target, context)` clears `target` before writing, so rendering the same template into the same directory with different contexts always produces a clean result (files/paths excluded by conditionals won't linger from a previous run).
## Unmatched directives
A `<@if>` without a matching `<@endif>` throws at render time:
```ts
// If a template file contains <@if(context.x)> with no <@endif>
render("./output", { x: true })
// Error: Unmatched <@if> without <@endif>
```