hardening

This commit is contained in:
Gregor Lohaus
2026-05-22 08:55:28 +02:00
parent ca4a02ab4e
commit 3110eefcbd
8 changed files with 293 additions and 173 deletions

View File

@@ -1,6 +1,6 @@
# 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.
Treat a directory as a template. File paths and text file 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. Binary files are copied through unchanged.
## Install
@@ -32,7 +32,7 @@ Where `index.html` contains:
Render it:
```ts
import { initRenderer } from "tdir"
import { initRenderer } from "@gregorlohaus/tdir"
import { z } from "zod"
const createRenderer = initRenderer("./templates")
@@ -58,7 +58,7 @@ render("./output", {
| Directive | Description |
|---|---|
| `<@if(context.x)>` | Conditional block — truthy check (must end with `<@endif>`) |
| `<@if(context.x)>` | Conditional block — boolean 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 |
@@ -70,7 +70,7 @@ render("./output", {
| Directive | Description |
|---|---|
| `<@if(context.x)>dirname` | Conditionally include directory/file (truthy check) |
| `<@if(context.x)>dirname` | Conditionally include directory/file (boolean check) |
| `<@if(eq(context.x,"value"))>dirname` | Conditionally include by string equality |
| `<@var(context.x)>` | Dynamic directory/file name |
@@ -81,7 +81,7 @@ These can be combined: `<@if(context.web.create)><@var(context.web.dir)>` create
`createRenderer` validates that your Zod schema matches the template variables. Mismatches throw `SchemaMismatchError`:
```ts
import { initRenderer, SchemaMismatchError } from "tdir"
import { initRenderer, SchemaMismatchError } from "@gregorlohaus/tdir"
import { z } from "zod"
const createRenderer = initRenderer("./templates")
@@ -92,14 +92,14 @@ 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()
// SchemaMismatchError: Schema doesn't 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
// SchemaMismatchError: Schema doesn't match used template variables: header: missing in schema
```
## Context validation
@@ -123,12 +123,14 @@ render("./output", { web: "not a boolean", header: { show: true, title: "Hi" } }
`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).
For safety, tdir refuses to render into the filesystem root, the current working directory, the home directory, or any directory that overlaps the template source. Dynamic file and directory names are also resolved against the output directory and cannot write outside it.
## Unmatched directives
A `<@if>` without a matching `<@endif>` throws at render time:
A `<@if>` without a matching `<@endif>` throws when the renderer is initialized:
```ts
// If a template file contains <@if(context.x)> with no <@endif>
render("./output", { x: true })
initRenderer("./templates")
// Error: Unmatched <@if> without <@endif>
```