# 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 <@if(context.header.show)> <@var(context.header.title)> <@endif> ``` 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 Hello ``` ## Template directives ### In file contents | Directive | Description | |---|---| | `<@if(context.x)>` | Conditional block (must end with `<@endif>`) | | `<@elseif(context.y)>` | Else-if branch | | `<@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 | | `<@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" ``` ## 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> ```