scaffold

package module
v0.0.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 24, 2026 License: Apache-2.0 Imports: 16 Imported by: 3

README

Form and Scaffold Utilities for Golang

This project contains 2 main components:

  • scaffold generates files or entire directory hierarchies from complex data
  • forms is a guided input form system for the terminal that can generate arbitrarily nested complex data

Together they allow building Wizard-style file generators.

See App Builder for these projects in use in an end-user product.

CLI Usage

A small CLI tool in scaffold can render scaffolds from the command line.

scaffold render <scaffold-dir> <target-dir> [key=value ...]

Data can be provided as command-line key=value pairs, from a JSON file, or interactively via a form:

# Key-value pairs
scaffold render ./templates ./output Name=myproject

# JSON file
scaffold render ./templates ./output --json data.json

# Interactive form
scaffold render ./templates ./output --form form.yaml

A working example is included in the example directory with a form (example_form.yaml) and a scaffold (address/) that renders contact information to a text file:

scaffold render example/address /tmp/output --form example/example_form.yaml

Scaffold

The scaffold package renders directory trees from templates. Templates can be supplied from a directory on disk or as in-memory maps. Both Go text/template and Jet template engines are supported.

Basic usage

s, err := scaffold.New(scaffold.Config{
    TargetDirectory: "/tmp/myproject",
    Source: map[string]any{
        "README.md": "# {{ .Name }}\n",
        "src": map[string]any{
            "main.go": "package {{ .Package }}\n",
        },
    },
}, template.FuncMap{})
if err != nil {
    return err
}

err = s.Render(map[string]any{
    "Name":    "My Project",
    "Package": "main",
})

This produces:

/tmp/myproject/
  README.md
  src/
    main.go

To use the Jet engine instead, call scaffold.NewJet with map[string]jet.Func in place of template.FuncMap.

Source directory

Instead of in-memory maps, templates can be read from a directory. The directory structure is mirrored into the target:

s, err := scaffold.New(scaffold.Config{
    TargetDirectory: "/tmp/myproject",
    SourceDirectory: "/path/to/templates",
}, template.FuncMap{})

Configuration options

Field Description
target Output directory (required)
source_directory Read templates from this directory (mutually exclusive with source)
source In-memory template map (mutually exclusive with source_directory)
merge_target_directory Allow writing into an existing target directory
skip_empty Omit files that are empty after rendering
left_delimiter / right_delimiter Custom template delimiters (both must be set)
post Post-processing commands matched by file glob

Built-in template functions

All Sprig functions are available when using the Go template engine. Two additional functions are provided in both engines:

write creates an extra file in the target directory from within a template:

{{/* Go template */}}
{{ write "extra.txt" "file content" }}
{{/* Jet template */}}
{{ write("extra.txt", "file content") }}

render evaluates another template file from the source directory and returns its output as a string. The partial is rendered using the same engine as the calling template:

{{/* Go template */}}
{{ render "_partials/header.txt" . }}
{{/* Jet template */}}
{{ render("_partials/header.txt", .) }}

Partials

Any directory named _partials is excluded from the rendered output. Files inside _partials are available to the render function but are not copied to the target.

Post-processing

The post configuration runs commands on rendered files that match a glob pattern. Use {} as a placeholder for the file path; if omitted the path is appended as the last argument:

Post: []map[string]string{
    {"*.go": "gofmt -w {}"},
    {"*.sh": "chmod +x"},
},

Merging into existing directories

Set merge_target_directory to render into a directory that already exists. Only files whose content has changed are written; unchanged files are left untouched.

Changed files

After rendering, ChangedFiles() returns the list of files that were created or modified, with paths relative to the target directory:

err = s.Render(data)
for _, f := range s.ChangedFiles() {
    fmt.Println("changed:", f)
}

Dry-run with RenderNoop

RenderNoop performs a full render into a temporary directory and compares the result against the real target without modifying it. Each file is reported with an action:

plan, err := s.RenderNoop(data)
for _, f := range plan {
    fmt.Printf("%s %s\n", f.Action, f.Path) // add, update, equal, or remove
}

See the Go package documentation for full API details.

Forms

The forms package provides interactive terminal forms that collect structured user input. Forms are defined in YAML and produce map[string]any results suitable for passing directly to the scaffold renderer.

Defining a form

A form has a name, description, and a list of properties. Each property becomes a prompt in the terminal.

name: project
description: Create a new project
properties:
  - name: project_name
    description: The name of the project
    type: string
    required: true

  - name: license
    description: Choose a license
    type: string
    enum:
      - Apache-2.0
      - MIT
      - GPL-3.0
    default: Apache-2.0

  - name: port
    description: Default listen port
    type: integer
    default: "8080"
    validation: "value > 0 && value < 65536"

  - name: enable_tls
    description: Enable TLS support
    type: bool
    default: "true"

  - name: authors
    description: Project authors
    type: array

  - name: database
    description: Database connection settings
    properties:
      - name: host
        description: Database hostname
        type: string
        default: localhost
      - name: port
        description: Database port
        type: integer
        default: "5432"

Property types

Type Description
string Free-text input, optionally limited by enum
password Masked string input
integer Whole number, validated automatically
float Decimal number, validated automatically
bool Yes/no confirmation
array Collects multiple values, including objects
object Named group of nested properties

Property options

Field Description
required Prompt cannot be skipped
default Pre-filled value shown to the user
enum Restrict input to a list of choices (string type)
help Extended help text shown on demand
validation An expr expression; value holds the input
conditional An expr expression controlling whether the property is shown; input holds answers so far
empty Behaviour when the answer is empty: absent omits the key, array or object sets an empty container
properties Nested properties for object and array types

Processing a form

// From a YAML file
data, err := forms.ProcessFile("form.yaml", env)

// From bytes
data, err := forms.ProcessBytes(yamlBytes, env)

// From a parsed Form struct
data, err := forms.ProcessForm(form, env)

The env map is passed to template expressions in descriptions and is also available in conditional expressions. The returned map[string]any contains all collected answers keyed by property name.

Descriptions with templates and color

Property descriptions support Go templates with Sprig functions and a simple color markup syntax:

description: >
  Configure the {green}{{ .ProjectName }}{/green} database connection.

Available colors: black, red, green, yellow, blue, magenta, cyan, white, and their hi variants (e.g. hired). bold is also supported.

See the Go package documentation for full API details.

Documentation

Overview

Package scaffold renders directory trees from Go or Jet templates.

Templates can be supplied as an on-disk source directory or as an in-memory map of filenames to content. The rendered output is written atomically to a target directory; only files whose content has changed are copied, making repeated renders safe for use with existing targets when MergeTargetDirectory is enabled.

Built-in template functions include the Sprig library, a write() function that creates additional files from within a template, and a render() function that evaluates a partial template from the _partials subdirectory. Custom delimiters, post-processing commands, and caller-supplied template functions are all supported.

Example
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"text/template"

	"github.com/choria-io/scaffold"
)

func main() {
	base, _ := os.MkdirTemp("", "scaffold-example-")
	defer os.RemoveAll(base)
	target := filepath.Join(base, "output")

	s, err := scaffold.New(scaffold.Config{
		TargetDirectory: target,
		SkipEmpty:       true,
		Source: map[string]any{
			"README.md": "# {{ .Name }}\n\n{{ .Description }}\n",
			"lib": map[string]any{
				"main.go": `package {{ .Package }}
`,
			},
			"empty.txt": "{{ if .Include }}content{{ end }}",
		},
	}, template.FuncMap{})
	if err != nil {
		panic(err)
	}

	result, err := s.Render(map[string]any{
		"Name":        "My Project",
		"Description": "A scaffolded project.",
		"Package":     "main",
		"Include":     false,
	})
	if err != nil {
		panic(err)
	}

	readme, _ := os.ReadFile(filepath.Join(target, "README.md"))
	fmt.Println(string(readme))

	main, _ := os.ReadFile(filepath.Join(target, "lib", "main.go"))
	fmt.Println(string(main))

	_, statErr := os.Stat(filepath.Join(target, "empty.txt"))
	fmt.Println("empty.txt exists:", !os.IsNotExist(statErr))

	for _, f := range result {
		fmt.Printf("%s: %s\n", f.Action, f.Path)
	}

}
Output:

# My Project

A scaffolded project.

package main

empty.txt exists: false
add: README.md
add: lib/main.go

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// TargetDirectory is where to place the resulting rendered files, must not exist
	TargetDirectory string `yaml:"target"`
	// SourceDirectory reads templates from a directory, mutually exclusive with Source
	SourceDirectory string `yaml:"source_directory"`
	// MergeTargetDirectory writes into existing target directories
	MergeTargetDirectory bool `yaml:"merge_target_directory"`
	// Source reads templates from in-process memory
	Source map[string]any `yaml:"source"`
	// Post configures post-processing of files using filepath globs
	Post []map[string]string `yaml:"post"`
	// SkipEmpty skips files that are 0 bytes after rendering
	SkipEmpty bool `yaml:"skip_empty"`
	// Sets a custom template delimiter, useful for generating templates from templates
	CustomLeftDelimiter string `yaml:"left_delimiter"`
	// Sets a custom template delimiter, useful for generating templates from templates
	CustomRightDelimiter string `yaml:"right_delimiter"`
}

Config configures a scaffolding operation

type FileAction added in v0.0.7

type FileAction string

FileAction represents the type of change a file would undergo during rendering

const (
	FileActionAdd    FileAction = "add"
	FileActionUpdate FileAction = "update"
	FileActionEqual  FileAction = "equal"
	FileActionRemove FileAction = "remove"
)

type Logger

type Logger interface {
	Debugf(format string, v ...any)
	Infof(format string, v ...any)
}

type ManagedFile added in v0.0.9

type ManagedFile struct {
	Path   string
	Action FileAction
}

ManagedFile represents a file and the action that would be taken on it during rendering

type Scaffold

type Scaffold struct {
	// contains filtered or unexported fields
}

func New

func New(cfg Config, funcs template.FuncMap) (*Scaffold, error)

New creates a new scaffold instance

func NewJet added in v0.0.6

func NewJet(cfg Config, funcs map[string]jet.Func) (*Scaffold, error)

NewJet creates a new scaffold instance using the Jet template engine

func (*Scaffold) Logger

func (s *Scaffold) Logger(log Logger)

Logger configures a logger to use, no logging is done without this

func (*Scaffold) Render

func (s *Scaffold) Render(data any) ([]ManagedFile, error)

Render creates the target directory and places all files into it after template processing and post-processing. Files are rendered into a temporary directory first, then atomically copied to the real target. The returned slice describes every managed file and the action taken (add, update, equal).

func (*Scaffold) RenderNoop added in v0.0.7

func (s *Scaffold) RenderNoop(data any) ([]ManagedFile, error)

RenderNoop performs a full render into a temporary directory and compares the result against the real target directory. It returns a list of files with their planned action (add, update, equal, remove) without modifying the real target.

func (*Scaffold) RenderString added in v0.0.2

func (s *Scaffold) RenderString(str string, data any) (string, error)

RenderString renders a string using the same functions and behavior as the scaffold, including custom delimiters

Directories

Path Synopsis
Package forms implements interactive terminal forms that collect user input and produce structured data.
Package forms implements interactive terminal forms that collect user input and produce structured data.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL