135 lines
3.7 KiB
Markdown
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>
|
|
```
|