Gregor Lohaus b9f675d1b9 scoped package
2026-04-08 01:16:32 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:16:32 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:16:32 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:05:23 +02:00
2026-04-08 01:16:32 +02:00
2026-04-08 01:05:23 +02:00

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

bun install tdir zod

Quick start

Given a template directory:

templates/
  <@if(context.web)>web/
    index.html

Where index.html contains:

<html>
  <@if(context.header.show)>
    <head><@var(context.header.title)></head>
  <@endif>
  <body></body>
</html>

Render it:

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 (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:

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:

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:

// If a template file contains <@if(context.x)> with no <@endif>
render("./output", { x: true })
// Error: Unmatched <@if> without <@endif>
Description
No description provided
Readme 54 KiB
Languages
TypeScript 99.3%
Nix 0.7%