Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

πŸ–ŒοΈ Sketch

Sketch is a tool designed to reduce boilerplate, increase productivity, and facilitate the creation and management of structured and easily reproducible workspaces.

Table of contents

Goals And Design Philosophy

Templating is mostly used to render webpages, but it can also be used to generate files used in day-to-day development, and even more, it can be used to define structures for entire projects in a way that can be standardized enough to provide structure and clarity, but also flexible enough as to make use of customized or even dynamically generated parameters.

While templating is a powerful technology on its own, it needs a frontend, a framework of sort, to make use of its flexible toolset, enhance it with with extra features, and to wrap it with an ergonomic API explicitely designed with the intent of simplifying most if not all activities related to creating and managing files and projects.

This is that Sketch aims to be, and it all starts with the concept of preset.

Presets

The basic concept about presets is that you should be able to define specific configurations that you find yourself using very often, and then use them to generate the files that you need or as the base for a more detailed preset.

Presets come in different forms and shapes and with different features.

Some presets are used as a way to aggregate other presets or templates into a single structure. For example, a git repo preset can use a gitignore or a pre-commit preset, it can aggregate customized templates templates to render inside the new repo, and it can also contain a list of commands (which also benefit from templating features) to execute before or after generation.

Other presets, such as those that belong to some of the most widely used configuration files such as Rust's Cargo.toml, or Typescript's tsconfig.json, are fully typed and documented (as part of the JSON schema for Sketch's configuration file), so that defining them comes with all of the benefits of IDE integration such as type safety and autocompletion.

Other presets are extensible, which means that you can define a base preset containing some common data (such as a list of networks or volumes in a compose.yaml file or a list of dependencies in a package.json file), and then create a preset that extends that base preset, so that you do not need to repeat inputs for all presets that share common settings.

Why Use Presets

Using presets helps in dealing with some of the most common pain points in software development:

1. Boilerplate Generation

The Problem

Writing boilerplate is an unrewarding and time consuming process. It causes unwanted cognitive load and kills productive momentum.

The Solution

When using presets, a single command can be used to generate a file or an entire project structure. It's orders of magnitude faster than doing it manually, while still being less complex than creating an ad-hoc script and more flexible than mere copy-pasting.

2. Structure And Reproducibility

The Problem

There are many cases in which you need to use a configuration file in multiple projects, such as a compose.yaml file, a github workflow, a setup file for a formatter or action runner, and so on. And chances are, you want to use this exact configuration in many if not all of your projects.

When you need to update this configuration file, you then need to update it in every other place where you are using it. If this is done manually, each copy may look slightly different than the other even for something as simple as ordering fields in a different way from one file to another.

While this may seem trivial, it causes mental friction and it can compound to outright confusion because each time your brain has to process a slightly different structure.

The Solution

If you use a preset, then every file generated with that preset will look the exact same. It removes the cognitive penalty caused by ever-slightly-different configuration files, and lets you focus on more important areas of your work.

And with extensible presets, you can keep a core preset where you define all of your shared configurations, and then extend it with more customized settings based on your project's needs.

3. Maintainability

The Problem

When you need to use a specific configuration in many different projects, that means that every change must be applied to every instance of the same configuration. This is a tedious and error-prone process, that eventually leads to projects being misconfigured and requiring manual review to bring them up to date with the new settings.

The Solution

With a preset, you can modify the preset itself and then update each instance programmatically (either by reusing or extending another preset). Even if the new changes are very specific and need manual review rather than being immediately applicable everywhere, it's still easier to maintain 1 preset (or even 2 or 10, for that matter) than maintaining 100s of individual files.

Supported Presets

The list of supported presets includes:

  • Git

  • Docker

    • Docker Compose file (extensible)
    • Docker Compose service (extensible)
  • Rust

    • Cargo.toml (extensible)
  • Typescript

    • pnpm-workspace.yaml (extensible)
    • package.json (extensible, with extra features)
    • tsconfig.json (extensible, with merging of values for the references, include, exclude and files fields)
    • .oxlintrc.json (extensible)
    • vitest (not a full configuration for vitest.config.ts, but a basic testing setup)

Enhanced Templating Toolset

While presets are designed to cover some of the most common use cases, custom templates can also be used to cover all sorts of scenarios. Custom templates can then be aggregated and used by other presets.

Sketch uses Tera as the templating engine to render custom templates, enhanced with a variety of extra features, such as:

  • Special variables that extract commonly used values such as the user directory or the host's operating system
  • Functions that perform actions such as:
    • Making a glob search in a directory
    • Generating a uuid
    • Transforming a relative path into an absolute path
    • Extracting capture groups from a regex

...and many other more, which you can find out about in more detail in the dedicated website.

Installation

Sketch can be installed in two ways:

  1. By downloading a pre-built binary from the github repository
  2. Via cargo (cargo install sketch-it)

Documentation

You can find out more about Sketch in the dedicated website.

The homepage will always show the docs for the latest version. Each minor version will have its own dedicated subroute under /versions.

Command-Line Help for sketch

This document contains the help content for the sketch command-line program.

Command Overview:

sketch

πŸ–ŒοΈ Templating made simple. A tool to define and generate files and reusable project structures

Usage: sketch [OPTIONS] <COMMAND>

Subcommands:
  • new β€” Generates a new config file
  • repo β€” Creates a new git repo from a preset
  • render β€” Renders a single template to a file or to stdout
  • render-preset β€” Renders a templating preset
  • exec β€” Renders a template and executes it as a shell command
  • gitignore β€” Generates a .gitignore file from a preset
  • gh-workflow β€” Generates a Github workflow
  • docker-compose β€” Generates a Docker Compose file from a preset
  • pre-commit β€” Generates a pre-commit config file from a preset
  • cargo-toml β€” Generates a Cargo.toml file from a preset
  • ts β€” Executes typescript-specific commands
  • package-json β€” Generates a package.json file from a preset
  • ts-config β€” Generates a tsconfig.json file from a preset
  • oxlint β€” Generates a .oxlintrc.json file from a preset
  • pnpm-workspace β€” Generates a pnpm-workspace.yaml file from a preset
  • license β€” Generates a license file
Options:
  • --print-config β€” Prints the full parsed config
  • --templates-dir <DIR> β€” The path to the templates directory
  • --no-overwrite β€” Do not overwrite existing files
  • -c, --config <FILE> β€” Sets a custom config file. Any file named sketch.{yaml,json,toml} in the cwd or in XDG_CONFIG_HOME/sketch will be detected automatically. If no file is found, the default settings are used
  • --ignore-config β€” Ignores any automatically detected config files, uses cli instructions and config file defined with --config
  • -s, --set <KEY=VALUE> β€” Sets a variable (as key=value) to use in templates. Overrides global and local variables. Values must be in valid JSON
  • --vars-file <VARS_FILES> β€” One or more paths to json, yaml or toml files to extract template variables from, in the given order

sketch new

Generates a new config file

Usage: sketch new [OUTPUT]

Arguments:
  • <OUTPUT> β€” The output file [default: sketch.yaml]

sketch repo

Creates a new git repo from a preset

Usage: sketch repo [OPTIONS] [DIR]

Arguments:
  • <DIR> β€” The directory where the new repo should be generated. [default: .]
Options:
  • -p, --preset <PRESET> β€” Selects a git preset from a configuration file

  • --no-pre-commit β€” Do not generate a pre-commit config

  • --pre-commit <PRE_COMMIT> β€” Selects a pre-commit preset

  • -g, --gitignore <GITIGNORE> β€” Selects a gitignore preset

  • -l, --license <LICENSE> β€” A license file to generate for the new repo

    Possible values:

    • apache2: Apache 2.0 license
    • gpl3: GNU GPL 3.0 license
    • agpl3: GNU AGPL 3.0 license
    • mit: MIT license
  • -w, --with-template <PRESET_ID|id=TEMPLATE_ID,output=PATH> β€” One or many individual templates or templating presets to render in the new repo

  • --workflow <id=PRESET_ID,file=PATH> β€” One or many workflow presets to use for the new repo. The file path will be joined to .github/workflows

  • -r, --remote <REMOTE> β€” The link of the git remote to use for the new repo

sketch render

Renders a single template to a file or to stdout

Usage: sketch render [OPTIONS] <OUTPUT_PATH|--stdout>

Arguments:
  • <OUTPUT_PATH> β€” The output path for the generated file
Options:
  • --stdout β€” Prints the result to stdout
  • -f, --file <FILE> β€” The path to the template file
  • -i, --id <ID> β€” The id of the template to use
  • -t, --template <TEMPLATE> β€” The path to a template file, starting from templates_dir
  • -c, --content <CONTENT> β€” The literal definition for the template

sketch render-preset

Renders a templating preset

Usage: sketch render-preset <ID> [OUT_DIR]

Arguments:
  • <ID> β€” The id of the preset
  • <OUT_DIR> β€” The base path to join to relative output paths. [default: .]

sketch exec

Renders a template and executes it as a shell command

Usage: sketch exec [OPTIONS] [CMD]

Arguments:
  • <CMD> β€” The literal definition for the template (incompatible with --file or --template)
Options:
  • --print-cmd β€” Prints the rendered command to stdout before executing it
  • -s, --shell <SHELL> β€” The shell to use for commands [default: cmd.exe on windows and sh elsewhere]
  • --cwd <CWD> β€” The cwd for the command to execute [default: .]
  • -f, --file <FILE> β€” The path to the command's template file, as an absolute path or relative to the cwd
  • -t, --template <TEMPLATE> β€” The id of the template to use (a name for config-defined templates, or a relative path to a file from templates_dir)

sketch gitignore

Generates a .gitignore file from a preset

Usage: sketch gitignore <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the new file [default: .gitignore]

sketch gh-workflow

Generates a Github workflow

Usage: sketch gh-workflow <PRESET> <OUTPUT>

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the new file

sketch docker-compose

Generates a Docker Compose file from a preset

Usage: sketch docker-compose [OPTIONS] <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the new file [default: compose.yaml]
Options:
  • -S, --service <SERVICES> β€” id=PRESET,name=NAME|ID

sketch pre-commit

Generates a pre-commit config file from a preset

Usage: sketch pre-commit <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the new file [default: .pre-commit-config.yaml]

sketch cargo-toml

Generates a Cargo.toml file from a preset

Usage: sketch cargo-toml [OPTIONS] <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the new file [default: Cargo.toml]
Options:
  • -n, --name <NAME> β€” Sets the name of the package in the new file. Overrides the value in the preset

sketch ts

Executes typescript-specific commands

Usage: sketch ts [OPTIONS] <COMMAND>

Subcommands:
  • monorepo β€” Generates a new typescript monorepo
  • package β€” Generates a new typescript package
  • barrel β€” Creates a barrel file
Options:
  • --package-manager <NAME> β€” The package manager being used. [default: pnpm]

    Possible values: pnpm, npm, deno, bun, yarn

  • --no-default-deps <NO_DEFAULT_DEPS> β€” Do not add default dependencies to new package.json files (typescript and oxlint, plus vitest if enabled)

    Possible values: true, false

  • --version-range <KIND> β€” The kind of version range to use for dependencies that are fetched automatically. [default: minor]

    Possible values: minor, patch, exact

  • --catalog <CATALOG> β€” Uses the default catalog (supported by pnpm and bun) for default dependencies, and automatically adds dependencies marked with catalog: to the catalog, if they are missing

    Possible values: true, false

  • --no-convert-latest <NO_CONVERT_LATEST_TO_RANGE> β€” Do not convert dependencies marked as latest to a version range

    Possible values: true, false

sketch ts monorepo

Generates a new typescript monorepo

Usage: sketch ts monorepo [OPTIONS] [DIR]

Arguments:
  • <DIR> β€” The root directory for the new monorepo. [default: ts_root]
Options:
  • -p, --pnpm <PRESET_ID> β€” The pnpm-workspace.yaml preset to use for the new monorepo. If it's unset and pnpm is the chosen package manager, the default preset will be used

  • -r, --root-package <PRESET_ID> β€” The id of the package preset to use for the root package. If unset, the default preset is used, along with the values set via cli flags

  • -n, --name <NAME> β€” The name of the new package. It defaults to the name of its directory

  • --ts-config <id=ID,output=PATH> β€” One or many tsconfig presets (with their output path) to use for this package (uses defaults if not provided)

  • --package-json <ID> β€” The package.json preset ID to use (uses defaults if not provided)

  • --license <LICENSE> β€” A license file to generate for the new package

    Possible values:

    • apache2: Apache 2.0 license
    • gpl3: GNU GPL 3.0 license
    • agpl3: GNU AGPL 3.0 license
    • mit: MIT license
  • --hook-pre <ID> β€” One or many IDs of templates to render and execute as commands before the package's creation

  • --hook-post <ID> β€” One or many IDs of templates to render and execute as commands after the package's creation

  • --oxlint <ID> β€” The oxlint preset to use. It can be set to default to use the default preset

  • -i, --install β€” Installs the dependencies with the chosen package manager

  • -w, --with-template <PRESET_ID|id=TEMPLATE_ID,output=PATH> β€” One or many templates or templating presets to generate in the new package's root

sketch ts package

Generates a new typescript package

Usage: sketch ts package [OPTIONS] [DIR]

Arguments:
  • <DIR> β€” The root directory for the new package. Defaults to the package name
Options:
  • -p, --preset <ID> β€” The package preset to use. If unset, the default preset is used, along with the values set via cli flags

  • -u, --update-tsconfig <UPDATE_TSCONFIG> β€” An optional list of tsconfig files where the new tsconfig file will be added as a reference

  • --oxlint <ID> β€” The oxlint preset to use. It can be set to default to use the default preset

  • -i, --install β€” Installs the dependencies with the chosen package manager

  • -w, --with-template <PRESET_ID|id=TEMPLATE_ID,output=PATH> β€” One or many templates or templating presets to generate in the new package's root

  • --vitest <ID> β€” The vitest preset to use. It can be set to default to use the default preset

  • -n, --name <NAME> β€” The name of the new package. It defaults to the name of its directory

  • --ts-config <id=ID,output=PATH> β€” One or many tsconfig presets (with their output path) to use for this package (uses defaults if not provided)

  • --package-json <ID> β€” The package.json preset ID to use (uses defaults if not provided)

  • --license <LICENSE> β€” A license file to generate for the new package

    Possible values:

    • apache2: Apache 2.0 license
    • gpl3: GNU GPL 3.0 license
    • agpl3: GNU AGPL 3.0 license
    • mit: MIT license
  • --hook-pre <ID> β€” One or many IDs of templates to render and execute as commands before the package's creation

  • --hook-post <ID> β€” One or many IDs of templates to render and execute as commands after the package's creation

sketch ts barrel

Creates a barrel file

Usage: sketch ts barrel [OPTIONS] [DIR]

Arguments:
  • <DIR> β€” The directory where to search recursively for the files and generate the barrel file [default: .]
Options:
  • -o, --output <OUTPUT> β€” The output path for the barrel file. It defaults to {dir}/index.ts
  • --keep-ext <EXT> β€” The file extensions that should be kept in export statements
  • --js-ext β€” Exports .ts files as .js. It assumes that js is among the file extensions to keep
  • --exclude <EXCLUDE> β€” One or more glob patterns to exclude from the imported modules

sketch package-json

Generates a package.json file from a preset

Usage: sketch package-json <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the generated file [default: package.json]

sketch ts-config

Generates a tsconfig.json file from a preset

Usage: sketch ts-config <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the generated file [default: tsconfig.json]

sketch oxlint

Generates a .oxlintrc.json file from a preset

Usage: sketch oxlint <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the generated file [default: .oxlintrc.json]

sketch pnpm-workspace

Generates a pnpm-workspace.yaml file from a preset

Usage: sketch pnpm-workspace <PRESET> [OUTPUT]

Arguments:
  • <PRESET> β€” The preset id
  • <OUTPUT> β€” The output path of the generated file [default: pnpm-workspace.yaml]

sketch license

Generates a license file

Usage: sketch license [OPTIONS] <LICENSE>

Arguments:
  • <LICENSE>

    Possible values:

    • apache2: Apache 2.0 license
    • gpl3: GNU GPL 3.0 license
    • agpl3: GNU AGPL 3.0 license
    • mit: MIT license
Options:
  • -o, --output <OUTPUT> β€” The path of the output file [default: LICENSE]

This document was generated automatically by clap-markdown.

Configuration

Sketch supports yaml, json and toml formats for its configuration file.

The strategy for detecting the configuration file works like this:

  • If a file is manually specified with --config, that is used.
  • Any file named sketch.{yaml, json, toml} in the cwd is used.
  • Any file named sketch.{yaml, json, toml} in XDG_CONFIG_HOME or $HOME/.config is used.
  • No file detected, using default settings.

Some of the values from configuration files can also be set via cli flags. When a value is set in a config file but also in a command, the value from the command has the higher priority.

You can also use the --ignore-config flag to temporarily ignore configuration files and only use cli-set values.

Generating Config Files

You can use the sketch new <OUTPUT> command to generate a new configuration file in the desired output file and format (the default output is sketch.yaml).

Extending Configurations

Configuration files can extend one another by using the extends field:

extends: ["other_config.yaml"]

Where the path being used can be either an absolute path or a relative path starting from the original config file.

The merging strategy for config files is the same as for all the other presets.

Lsp Integration

In order to ensure the best development experience, it is highly recommended to set up your IDE to load the json schema for Sketch's configuration file.

This will provide type-safety, autocompletion and additional documentation when writing config files.

Every time a new version is released, the json schema for the configuration file will be updated in the github repo. Each version will have its own distinct schema file, and the latest.json file will track the schema for the latest version.

You can then use this schema with the yaml, toml or json language servers to get autocompletion, type information and documentation for each element of the config file.

This can be done by configuring the specific LSP in your editor, or simply by using a special comment at the top of your file that links to the config's json schema.

Examples

Yaml:

# yaml-language-server: $schema=https://github.com/Rick-Phoenix/sketch/blob/main/schemas/latest.json

Json:

{
  "$schema": "https://github.com/Rick-Phoenix/sketch/blob/main/schemas/latest.json"
}

Toml:

#:schema https://github.com/Rick-Phoenix/sketch/blob/main/schemas/latest.json

External Configs

As of now, these external configurations defined with sketch are all fully typed and documented, so they benefit from the same lsp integration as the rest of the other settings:

  • .pre-commit-config.yaml
  • .oxlintrc.json
  • pnpm-workspace.yaml
  • package.json
  • tsconfig.json
  • compose.yaml (Docker)
  • github workflows

Templating

Sketch uses the Tera templating engine to render custom templates. Every template, whether it's defined in a file, in the config file, or inlined, can take advantage of all of Tera's functionalities, like using filters, functions, defining and using macros, performing operations and importing other templates.

Since all autoescaping is disabled in templates, you should always only use templates that you made or trust.

Context And Variables

Each template can be populated with context variables. These variables can be set at many different stages, with different degrees of priority (from lowest to highest):

  • Global variables
  • Cli-set variable files
  • Preset context
  • Single template context
  • Cli-set variables
Variables defined with the --set flag must be formatted in valid json. This means that, for example, strings must be wrapped in escaped quotes.

Special Variables

Sketch provides some special variables to get access to commonly used values such as the home directory.

All of the following variables are available in templates, prefixed with sketch_ (i.e. sketch_os and so on).

  • cwd
  • tmp_dir (from env::temp_dir)
  • home (from env::home_dir)
  • os (from CARGO_CFG_TARGET_OS or env OS)
  • os_family (from CARGO_CFG_TARGET_FAMILY)
  • arch (from CARGO_CFG_TARGET_ARCH or env HOSTTYPE)
  • user (env USER)
  • hostname (env HOSTNAME)
  • xdg_config (env XDG_CONFIG_HOME)
  • xdg_data (env XDG_DATA_HOME)
  • xdg_state (env XDG_STATE_HOME)
  • xdg_cache (env XDG_CACHE_HOME)
  • is_unix (from cfg!(unix))
  • is_linux (from cfg!(target_os = "linux"))
  • is_macos (from cfg!(target_os = "macos"))
  • is_wsl (checks /proc/sys/kernel/osrelease)
  • is_windows (from cfg!(windows))

Filters And Functions

All of the builtin functionalities for Tera are available.

On top of that, Sketch adds some extra filters and functions.

Functions

  • uuid (generates a v4 UUID)

Filters

Strings

  • capture(regex=REGEX) (matches a regex once and returns the named capture groups)
  • capture_many(regex=REGEX) (matches a regex repetitively and returns the list of named capture groups)
  • semver (parses a cargo-style semver and returns the segments)
  • matches_semver(target=TARGET) (checks if a cargo-style semver matches a target)
  • strip_prefix(prefix=PREFIX) (strips a prefix from a string, if present)
  • strip_suffix(suffix=SUFFIX) (strips a suffix from a string, if present)

Filesystem

  • basename (gets the basename of a directory/file)
  • parent_dir (gets the parent directory of a directory/file)
  • is_file (checks if a path is a file)
  • is_dir (checks if a path is a directory)
  • is_absolute (checks if a path is absolute)
  • is_relative (checks if a path is relative)
  • absolute (makes a path absolute)
  • relative(from=PATH) (returns the relative path between two paths)
  • read_dir (returns the list of the files contained in a directory and its subdirectories)
  • glob(pattern=GLOB) (returns the glob matching entries in a directory and its subdirectories)
  • matches_glob(pattern=GLOB) (checks if a path matches a glob pattern)

Serialization

  • to_yaml (serializes input into yaml)
  • to_toml (serializes input into prettified toml)

Examples

Template:

{# Using special variables -#}

Current arch is: {{ sketch_arch }}
Current os is: {{ sketch_os }}
Current os family is: {{ sketch_os_family }}
Is unix: {{ sketch_is_unix }}

{# Functions and filters -#}

A random uuid is: {{ uuid() }}

{# Generating a random alphanumeric string -#}
A random string is: {{ uuid() | truncate(length=6, end="") }}

{# Generating a random number -#}
A random number is: {{ get_random(start=100000, end=9999999) }}

Now is {{ now(utc=true) }}

{# Assigning an env to a variable -#}
{%- set greeting = get_env(name="GREETING") -%}

{#- Manipulating data -#}
{%- set segments = greeting | split(pat=",") -%}
First segment is: {{ segments[0] }}
Second segment is: {{ segments[1] }}

{% set some_path = "mydir/myfile" -%}
Basename is: {{ some_path | basename }}
Parent dir is: {{ some_path | parent_dir }}

{% set dir = "../sketch" -%}

{% if dir | is_dir -%}
It's a dir!
{% endif -%}

{% set file = "Cargo.toml" -%}

{% if file | is_file -%}
It's a file!
{%- endif %}

Matches glob: {{ file | matches_glob(pattern="**/*.toml") }}

{%- set captures = file | capture(regex="(?<path>.*)\.(?<extension>\w+)") %}

Path is: {{ captures['path'] }}
Extension is: {{ captures['extension'] }}

In yaml form:
{{ captures | to_yaml }}

In toml form:
{{ captures | to_toml }}

{% set words = "They're taking the hobbits to Isengard" | capture_many(regex="(?<word>[\w']+)") -%}

{% for capture in words -%}
{{ capture['word'] -}} {% if not loop.last %}{{ " " }}{% endif -%}
{%- endfor -%}!

{% set version = "v0.1.0" -%}

{% set segments = version | semver -%}
Major is: {{ segments['major'] }}
Minor is: {{ segments['minor'] }}
Patch is: {{ segments['patch'] }}

Version matches >=0.1.0: {{ version | matches_semver(target=">=0.1.0") }}
Version matches >=0.2.0: {{ version | matches_semver(target=">=0.2.0") }}

To camelCase: {{ "my_var" | camel }}
To snake_case: {{ "myVar" | snake }}
To SCREAMING_CASE: {{ "myVar" | upper_snake }}
To PascalCase: {{ "myVar" | pascal }}

Luke, I am your {{ "grandfather" | strip_prefix(prefix="grand") }}!

{% set dir = "../examples/templating/templates" -%}

{% for entry in dir | glob(pattern="example.j2") -%}
Entry: {{ entry }}
{% endfor %}

Cmd:

GREETING="hello,world" sketch render --template example.j2 --stdout

Output:

Current arch is: x86_64
Current os is: linux
Current os family is: unix
Is unix: true

A random uuid is: fd61daf5-83bf-46c6-afe3-5fcac0d6212c

A random string is: 0de4ed

A random number is: 3574820

Now is 2025-10-27T19:13:47.625250487+00:00

First segment is: hello
Second segment is: world

Basename is: myfile
Parent dir is: mydir

It's a dir!
It's a file!

Matches glob: true

Path is: Cargo
Extension is: toml

In yaml form:
path: Cargo
extension: toml


In toml form:
path = "Cargo"
extension = "toml"


They're taking the hobbits to Isengard!

Major is: 0
Minor is: 1
Patch is: 0

Version matches >=0.1.0: true
Version matches >=0.2.0: false

To camelCase: myVar
To snake_case: my_var
To SCREAMING_CASE: MY_VAR
To PascalCase: MyVar

Luke, I am your father!

Entry: example.j2

Rendering A Template

We can render a template to a file or to stdout with the command sketch render.

From A Configuration File

A template can be defined as simple text in a config file, inside the templates map. In this case, you can refer to this template by using its map key. This is the easiest method, but it also means slightly worse IDE integration for things like snippets and syntax highlighting.

templates_dir: templates

vars:
  location: Isengard
  greeting: hello
  general: kenobi
  meal: breakfast

templates:
  hobbits: "they're taking the hobbits to {{ location }}!"
  breakfast: "what about second {{ meal }}?"

Command:

sketch render --id hobbits tests/output/custom_templates/from_template_id.txt

From templates_dir

The best method is to use a file inside templates_dir. With this method, the template's ID becomes the relative path from templates_dir. This method provides better IDE integration if you have set up snippets or syntax highlighting for jinja files (Tera, the templating engine used by sketch is based on jinja).

Example

Tree structure of templates_dir:

β”œβ”€β”€ example.j2
└── subdir
    β”œβ”€β”€ nested
    β”‚Β Β  └── more_nested_file.j2
    └── nested_file.j2

Command:

sketch render --template subdir/nested_file.j2 tests/output/custom_templates/from_template_file.txt

ℹ️ You can also use the -f flag to render a template from any file, even outside templates_dir.

ℹ️ --id and --template are aliases for one another, and they can be used to refer to a file inside templates_dir, or to a literal template in the config file.

From Literal Definition

Alternatively, a template can also be defined directly within a command:

sketch render --content "they're taking the hobbits to {{ location }}!" tests/output/custom_templates/from_literal.txt

Executing Templates

With the sketch exec command, you can render a template and then execute it as a shell command.

To do this, you can use a regular file, a template with an id (either defined in the config file or inside the templates_dir), or even one defined within the command itself.

Examples

From Template

Let's say that we have a file named cmd_template.j2 inside our templates_dir, and it looks like this:

echo "{{ category }} engine... {{ category }}... argh!" > output_from_templates_dir.txt

ℹ️ You do not have to use .j2 as an extension for the template files. Any extension can be used.

We can refer to it by its id:

sketch --set category="gp2" tests/output/commands_tests -t cmd_template.j2

This will trigger the command and create a file containing

gp2 engine... gp2... argh!

From File

We can also use a file that is not inside templates_dir by using the -f flag and providing the path to said file. Relative paths will be resolved starting from the cwd.

So let's say that we have this template file:

echo "all the time you have to leave the {{ something }}!" > output_from_file.txt

Command:

sketch --set something="space" tests/output/commands_tests -f tests/commands_tests/cmd_from_file.j2

Output:

all the time you have to leave the space!

From Literal Template

Templates can also be defined directly as part of the command.

Command:

sketch --set condition="slower" tests/output/commands_tests 'echo "engine feels good... much {{ condition }} than before... amazing" > command_output.txt'

Output:

engine feels good... much slower than before... amazing

Hooks

Templates can be executed as commands as pre or post hooks in a git repo, typecript monorepo or typescript package presets:

    # Commands to run before generation, from the root of the new project
    hooks_pre:
      - command:
          # Inlining a new template definition here,
          # but as always, we can use stored templates too
          name: pre_hook
          content: "echo '{{ greeting }}' > pre.txt"
        context:
          greeting: hi

    # Commands to run after generation, from the root of the new project
    hooks_post:
      - command:
          name: post_hook
          content: "echo '{{ greeting }}' > post.txt"
        context:
          greeting: hi

Presets

Sketch supports various kinds of presets, which are designed to serve different functionalities. Some presets serve as aggregators for other presets, others provide extensibility, while others provide type safety and lsp integration.

As of now, these presets are available:

  • Templating

    • Templating presets (extensible)
  • Docker

    • Docker Compose file (extensible)
    • Docker Compose service (extensible)
  • Git

  • Rust

    • Cargo.toml (extensible, with merging of features for dependencies)
  • Typescript

    • Typescript package
    • pnpm-workspace.yaml (extensible)
    • package.json (extensible, with extra features)
    • tsconfig.json (extensible, with merging of values for the references, include, exclude and files fields)
    • .oxlintrc.json (extensible)
    • vitest (not a full configuration for vitest.config.ts, but a basic testing setup)

These can be generated individually, or as part of another preset. You can find more information about each command in the cli reference.

Extending Presets

Some presets can extend other presets. When a preset is extended, the merging strategy for their fields works like this:

  • Collections are merged and, in almost all cases, also deduped and sorted (except for cases where order matters such as a list of command arguments). If the collection is a map and the values in it are also maps (as is the case for the catalogs field in package.json or pnpm-workspace.yaml), the inner maps will be merged.
  • Values that are also extensible (such as compilerOptions in a tsconfig preset) will be merged with the same rules as above
  • All other values are overwritten, except if the previous value was present and the new value is null. This is to avoid merging values that come from partially-defined presets, where the missing fields are all unset. Generally speaking, the correct strategy to extend presets is to define a base and then add elements to it, rather than replacing other values.

Examples

This is a detailed example of the various kinds of presets that are available:


# `Cargo.toml` presets
cargo_toml_presets:
  base:
    package:
      version: "0.1.0"
      edition: "2024"

  cli-custom:
    extends_presets:
      - base
      - cli-tools
      - serde-ordered
    package:
      name: cli-custom
    dependencies:
      clap:
        features:
          - derive
      owo-colors:
        features:
          - supports-colors

  cli-tools:
    dependencies:
      ratatui: "0.29"
      clap: "4.5"
      serde: "1"
      owo-colors: "4"

  serde-ordered:
    dependencies:
      serde:
        version: "1"
        features:
          - preserve_order
      indexmap:
        version: "2.11"
        features:
          - serde

# Docker Compose presets
docker:
  service_presets:
    # A base service preset with common configurations
    base_service:
      environment:
        TZ: Europe/Berlin
      networks:
        - my_network

    # A preset for a db service
    database:
      extends_presets:
        - base_service
      image: postgres

    caddy:
      extends_presets:
        - base_service
      image: lucaslorentz/caddy-docker-proxy:ci-alpine
      networks:
        - my_network
      ports:
        - 80:80
        - 443:443

  compose_presets:
    # Base preset for a compose file
    base:
      volumes:
        my_volume:
          external: true
      networks:
        my_network:
          external: true

    extended:
      extends_presets:
        - base
      services:
        # Using the service preset by ID
        db: database
        my_service:
          networks:
            - my_network
          volumes:
            - my_volume:/target
      volumes:
        my_other_volume:
          external: true

# Gitignore presets
gitignore_presets:
  # Can be a string
  base:
    content: |
      *.env
      dist
  # Or a list of strings
  ts:
    extends_presets:
      # When being merged, the new entries are placed at the top
      - base
    content:
      - "*.tsBuildInfo"
      - node_modules

# Pre-commit presets
pre_commit_presets:
  # Commonly used repo
  base:
    repos:
      - repo: https://github.com/gitleaks/gitleaks
        rev: v8.28.0
        hooks:
          - id: gitleaks

  # Hooks specific to typescript projects
  typescript:
    # Extending the base preset
    extends_presets:
      - base
    repos:
      - repo: local
        hooks:
          - id: oxlint
            name: oxlint
            entry: oxlint
            language: system
            files: '\.svelte$|\.js$|\.ts$'
            types: [file]

# Git presets
git_presets:
  ts_package:
    workflows:
      - file_name: my_workflow.yaml
        # Preset ID
        id: extended
    # License file to generate
    license: Apache-2.0
    # Selecting a preset
    gitignore: ts
    pre_commit: typescript

    # Templates that will be generated with this preset
    # starting from the new repo's root
    with_templates:
      - id: dockerfile

    # Commands to run before generation, from the root of the new project
    hooks_pre:
      - command:
          # Inlining a new template definition here,
          # but as always, we can use stored templates too
          name: pre_hook
          content: "echo '{{ greeting }}' > pre.txt"
        context:
          greeting: hi

    # Commands to run after generation, from the root of the new project
    hooks_post:
      - command:
          name: post_hook
          content: "echo '{{ greeting }}' > post.txt"
        context:
          greeting: hi

typescript:
  # By default, typescript and oxlint are added to
  # the dependencies
  no_default_deps: true

  # By default, all versions marked with `latest` are converted to
  # a version range starting from the current version
  no_convert_latest_to_range: true

  # Version range to use when converting from `latest`
  version_range: patch

  # `pnpm-workspace.yaml` presets
  pnpm_presets:
    base:
      # All the dirs listed here will be created automatically
      # when new monorepos are created
      packages:
        - packages/*
        - apps/*
      onlyBuiltDependencies:
        - esbuild
      minimumReleaseAge: 1440

  # `package.json` presets
  package_json_presets:
    # Defining some reusable fields such as some common scripts and dependencies
    frontend:
      description: I am the base preset
      scripts:
        build: vite build
        dev: vite dev
      devDependencies:
        tailwindcss: "*"
        vite: "*"

    svelte_frontend:
      extends_presets: ["frontend"]
      # Adding new fields
      license: MIT
      # Overriding others
      description: I am the frontend preset
      # And merging dependencies
      devDependencies:
        svelte: "*"


  # Tsconfig presets
  ts_config_presets:
    base:
      references:
        - path: /some/path
      include:
        - src
      compilerOptions:
        noEmit: true
        verbatimModuleSyntax: true

    extended:
      extends_presets:
        - base
      # Unlike in real tsconfigs with `extends`, references are merged here,
      # and so are all other collections such as `files` or `include`.
      references:
        - path: /other/path
      include:
        - tests
      compilerOptions:
        # Whereas other fields are overwritten
        noEmit: false


  # Oxlint presets
  oxlint_presets:
    base:
      ignorePatterns:
        - "**/node_modules/**"

    extended:
      extends_presets:
        - base
      ignorePatterns:
        - .cache

  # Vitest presets
  vitest_presets:
    base:
      # Where the config file should be generated, starting from
      # the root of the package.
      # It defaults to the value of `tests_dir`
      out_dir: null

      # The directory to use for tests, from the root of the package
      tests_dir: tests

      # The directory, inside `tests_dir`, that contains the setup files for tests.
      # A file named `tests_setup.ts`, containing some basic testing boilerplate,
      # will be generated inside of it.
      setup_dir: setup

      # A list of setup files (the paths will be joined to the `setup_dir`)
      # `tests_setup.ts` will be added automatically.
      setup_files:
        - somefile.ts
        - anotherfile.ts
      environment: jsdom
      silent: passed-only


  # Package presets
  package_presets:
    example:
      name: example

      # For every preset, we can either refer to them
      # by their id...
      package_json: svelte_frontend

      # ...or we can inline a preset definition
      oxlint:
        extends_presets:
          - extended
        ignorePatterns:
          - .output

      # We can define one or many tsconfig files
      ts_config:
        # Can be omitted, would default to `tsconfig.json`
        - output: tsconfig.json
          config:
            # Once again creating a new config here with some extras
            extends_presets:
              - extended
            include:
              - scripts

      vitest: base

Templating Presets

Rendering a single template is for cases when we need a simpler or more flexible setup. But things become more interesting with templating presets, where different kinds of templates can be aggregated and used in order to define easily reproducible project structures.

Templating presets can be rendered with the render-preset command, or as part of another preset, such as a git repo preset.

A templating preset contains an optional context (which overrides the global context), and one or many of these items:

1. Individual Templates

  • Individual templates, which provide a manually controlled output path and their own local context

Example

templating_presets:

  lotr:
    # Group context, lower priority
    context: {}
    templates:
      # Selecting individual templates
      - template: hobbits
        output: hobbits.txt
        # Individual context, higher priority
        context: {}
      - template: breakfast
        output: subdir/breakfast.txt

Command:

sketch render-preset lotr

Tree output:

β”œβ”€β”€ hobbits.txt
└── subdir
    └── breakfast.txt

2. Template Directory

  • A path to a directory inside templates_dir, where all templates will be recursively rendered in the output directory, with the same file tree structure

ℹ️ Any template files that end with the .j2, .jinja or .jinja2 extensions will have them automatically removed. So myfile.json.j2 will just become myfile.json.

Example

Let's say that this is our templates_dir:

β”œβ”€β”€ example.j2
└── subdir
    β”œβ”€β”€ nested
    β”‚Β Β  └── more_nested_file.j2
    └── nested_file.j2

And we define this preset, which is meant to reproduce the entire file structure of subdir in the target directory.

templating_presets:
  structured:
    # Selecting all the files inside this directory
    # and its descendants
    templates:
      - dir: subdir
        # Glob patterns to exclude certain templates
        exclude:
          - "some/glob/pattern/*"

Command:

sketch render-preset structured

Tree output:

β”œβ”€β”€ nested
β”‚Β Β  └── more_nested_file
└── nested_file

3. Remote Template

  • A special kind of template which points to a git repository. Every file inside of it will be rendered in the output directory.
templating_presets:
  remote:
    templates:
      - repo: https://github.com/Rick-Phoenix/sketch-remote-preset-example
        # Glob patterns to exclude
        exclude: [.gitignore]

Example

We start from this basic example

Command:

sketch --set 'continuation="gp2 engine... gp2!"' render-preset remote

Tree output:

β”œβ”€β”€ some_file
└── subdir
    └── nested
        └── nested_file

File output for some_file:

Roses are red, violets are blue, gp2 engine... gp2!

Extending Templating Presets

Templating presets are extensible. When a preset is being extended, its templates will be added to the receiving preset, and the two context maps will be merged, with the new context overwriting the previous context in case of conflicting variables.

  extended:
    extends_presets:
      - lotr

Generating A Git Repo

The sketch repo command allows you to generate a new git repository, starting from a preset stored in one of your configuration files.

A git preset uses (or defines) a preset for its gitignore file and, optionally, for pre-commit, as well as a list of templates that will be generated inside the root of the new repo when the command is triggered.

# Gitignore presets
gitignore_presets:
  # Can be a string
  base:
    content: |
      *.env
      dist
  # Or a list of strings
  ts:
    extends_presets:
      # When being merged, the new entries are placed at the top
      - base
    content:
      - "*.tsBuildInfo"
      - node_modules

# Pre-commit presets
pre_commit_presets:
  # Commonly used repo
  base:
    repos:
      - repo: https://github.com/gitleaks/gitleaks
        rev: v8.28.0
        hooks:
          - id: gitleaks

  # Hooks specific to typescript projects
  typescript:
    # Extending the base preset
    extends_presets:
      - base
    repos:
      - repo: local
        hooks:
          - id: oxlint
            name: oxlint
            entry: oxlint
            language: system
            files: '\.svelte$|\.js$|\.ts$'
            types: [file]

# Git presets
git_presets:
  ts_package:
    workflows:
      - file_name: my_workflow.yaml
        # Preset ID
        id: extended
    # License file to generate
    license: Apache-2.0
    # Selecting a preset
    gitignore: ts
    pre_commit: typescript

Adding Workflows

We can also include some Github workflows by using their presets:

    workflows:
      - file_name: my_workflow.yaml
        # Preset ID
        id: extended

Adding Templates

We can use the with_templates setting to add a group of templates to a git preset. Let's say that we want to automatically generate a basic docker setup whenever we use this preset:

# We define a template in a file or in a config file...
templates:
  dockerfile: |
    FROM node:23-alpine

    COPY . .
    EXPOSE {{ docker_dev_port | default(value=5173) }}
    CMD ["npm", "run", "dev"]

# Templating presets
templating_presets:
  dockerfile:
    context:
      # Group context, lower priority
      docker_dev_port: 9530
    # List of templates
    templates:
      # Single template
      - output: Dockerfile
        template: dockerfile
        # Local context with higher priority
        context:
          docker_dev_port: 5173


# ...and then we add it to a preset

# Git presets
git_presets:
  ts_package:
    workflows:
      - file_name: my_workflow.yaml
        # Preset ID
        id: extended
    # License file to generate
    license: Apache-2.0
    # Selecting a preset
    gitignore: ts
    pre_commit: typescript

    # Templates that will be generated with this preset
    # starting from the new repo's root
    with_templates:
      - id: dockerfile

Hooks

We can define some commands (which can also be templates) to execute before and/or after generating the new repo:

    # Commands to run before generation, from the root of the new project
    hooks_pre:
      - command:
          # Inlining a new template definition here,
          # but as always, we can use stored templates too
          name: pre_hook
          content: "echo '{{ greeting }}' > pre.txt"
        context:
          greeting: hi

    # Commands to run after generation, from the root of the new project
    hooks_post:
      - command:
          name: post_hook
          content: "echo '{{ greeting }}' > post.txt"
        context:
          greeting: hi

Example

Starting from this config, we can run this command:

sketch repo --preset ts_package

To get this tree output:

β”œβ”€β”€ .github
β”‚Β Β  └── workflows
β”‚Β Β      └── my_workflow.yaml
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .pre-commit-config.yaml
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ LICENSE
β”œβ”€β”€ post.txt
└── pre.txt

ℹ️ With cli flags, we can override the gitignore and pre-commit presets, as well as adding new templates or hooks to run or generate when the preset is being used.

pre-commit-config.yaml output
repos:
- repo: local
  hooks:
  - id: oxlint
    entry: oxlint
    files: \.svelte$|\.js$|\.ts$
    language: system
    name: oxlint
    types:
    - file
- repo: https://github.com/gitleaks/gitleaks
  rev: v8.28.0
  hooks:
  - id: gitleaks
gitignore output
*.tsBuildInfo
node_modules
*.env
dist

Github Workflow Presets

Sketch supports presets for Github workflows, as well as individual components in a workflow, such as a job or a step.

Example

We use this configuration:

github:
  workflow_presets:
    # Common settings for all workflows
    base:
      defaults:
        run:
          shell: bash
      env:
        my_env: somevalue
        another_env: anothervalue

    extended:
      extends_presets:
        - base
      on:
        push:
          branches:
            - main
      jobs:
        # Adding a job by using its preset ID
        check_main_branch: check_main_branch
        do_something:
          extends_presets:
            - base
          if: needs.check_branch.outputs.is_on_main == 'true'
          name: Do something while on main branch
          steps:
            - run: echo "Done something from main branch!"

  workflow_job_presets:
    # Common settings for all jobs
    base:
      runs-on: ubuntu-latest
      timeout-minutes: 25
      continue-on-error: false
      env:
        my_env: somevalue
        another_env: anothervalue

    check_main_branch:
      extends_presets:
        - base
      env:
        another_other_value: yetanothervalue
      outputs:
        is_on_main: ${{ steps.branch_check.outputs.is_on_main }}
      steps:
        # Using a step preset
        - check_main

  steps_presets:
    # Reusable step preset
    check_main:
      name: Check if a tag is on the main branch
      id: branch_check
      run: |
        if git branch -r --contains ${{ github.ref }} | grep -q 'origin/main'; then
        echo "On main branch. Proceeding with the workflow..."
        echo "is_on_main=true" >> "$GITHUB_OUTPUT"
        else
        echo "Not on main branch. Skipping workflow..."
        fi

Run the command

sketch gh-workflow extended

And get this output:

on:
  push:
    branches:
    - main
env:
  another_env: anothervalue
  my_env: somevalue
defaults:
  run:
    shell: bash
jobs:
  check_main_branch:
    runs-on: ubuntu-latest
    outputs:
      is_on_main: ${{ steps.branch_check.outputs.is_on_main }}
    env:
      another_env: anothervalue
      another_other_value: yetanothervalue
      my_env: somevalue
    timeout-minutes: 25
    continue-on-error: false
    steps:
    - name: Check if a tag is on the main branch
      id: branch_check
      run: |
        if git branch -r --contains ${{ github.ref }} | grep -q 'origin/main'; then
        echo "On main branch. Proceeding with the workflow..."
        echo "is_on_main=true" >> "$GITHUB_OUTPUT"
        else
        echo "Not on main branch. Skipping workflow..."
        fi
  do_something:
    runs-on: ubuntu-latest
    name: Do something while on main branch
    if: needs.check_branch.outputs.is_on_main == 'true'
    env:
      another_env: anothervalue
      my_env: somevalue
    timeout-minutes: 25
    continue-on-error: false
    steps:
    - run: echo "Done something from main branch!"

Cargo.toml Presets

You can use sketch to define and generate extensible Cargo.toml presets.

When presets are extended, each dependency will also get merged according to the merging rules.

Example

Config:

cargo_toml_presets:
  base:
    package:
      version: "0.1.0"
      edition: "2024"

  cli-custom:
    extends_presets:
      - base
      - cli-tools
      - serde-ordered
    package:
      name: cli-custom
    dependencies:
      clap:
        features:
          - derive
      owo-colors:
        features:
          - supports-colors

  cli-tools:
    dependencies:
      ratatui: "0.29"
      clap: "4.5"
      serde: "1"
      owo-colors: "4"

  serde-ordered:
    dependencies:
      serde:
        version: "1"
        features:
          - preserve_order
      indexmap:
        version: "2.11"
        features:
          - serde

Command:

sketch cargo-toml cli-custom

Output:

[package]
name = "cli-custom"
version = "0.1.0"
edition = "2024"

[dependencies]
ratatui = "0.29"

[dependencies.clap]
features = ["derive"]

[dependencies.indexmap]
features = ["serde"]

[dependencies.owo-colors]
features = ["supports-colors"]

[dependencies.serde]
features = ["preserve_order"]

Docker Compose Presets

Sketch can be used to create extensible presets for entire compose files as well as individual services.

Example

Config:

docker:
  service_presets:
    # A base service preset with common configurations
    base_service:
      environment:
        TZ: Europe/Berlin
      networks:
        - my_network

    # A preset for a db service
    database:
      extends_presets:
        - base_service
      image: postgres

    caddy:
      extends_presets:
        - base_service
      image: lucaslorentz/caddy-docker-proxy:ci-alpine
      networks:
        - my_network
      ports:
        - 80:80
        - 443:443

  compose_presets:
    # Base preset for a compose file
    base:
      volumes:
        my_volume:
          external: true
      networks:
        my_network:
          external: true

    extended:
      extends_presets:
        - base
      services:
        # Using the service preset by ID
        db: database
        my_service:
          networks:
            - my_network
          volumes:
            - my_volume:/target
      volumes:
        my_other_volume:
          external: true

Command:

sketch docker-compose --service caddy extended

ℹ️ With the --service flag, extra service presets can be added to the output file.

Output:

services:
  caddy:
    image: lucaslorentz/caddy-docker-proxy:ci-alpine
    environment:
      TZ: Europe/Berlin
    networks:
    - my_network
    ports:
    - 443:443
    - 80:80
  db:
    image: postgres
    environment:
      TZ: Europe/Berlin
    networks:
    - my_network
  my_service:
    networks:
    - my_network
    volumes:
    - my_volume:/target
networks:
  my_network:
    external: true
volumes:
  my_other_volume:
    external: true
  my_volume:
    external: true

Typescript Presets

Many typical components of a typescript project have their own dedicated preset.

Package.json Presets

  package_json_presets:
    # Defining some reusable fields such as some common scripts and dependencies
    frontend:
      description: I am the base preset
      scripts:
        build: vite build
        dev: vite dev
      devDependencies:
        tailwindcss: "*"
        vite: "*"

    svelte_frontend:
      extends_presets: ["frontend"]
      # Adding new fields
      license: MIT
      # Overriding others
      description: I am the frontend preset
      # And merging dependencies
      devDependencies:
        svelte: "*"

Just like in actual package.json files, custom fields are allowed.

ℹ️ package.json presets come with extra features. Dedicated section

Tsconfig Presets

  ts_config_presets:
    base:
      references:
        - path: /some/path
      include:
        - src
      compilerOptions:
        noEmit: true
        verbatimModuleSyntax: true

    extended:
      extends_presets:
        - base
      # Unlike in real tsconfigs with `extends`, references are merged here,
      # and so are all other collections such as `files` or `include`.
      references:
        - path: /other/path
      include:
        - tests
      compilerOptions:
        # Whereas other fields are overwritten
        noEmit: false

Unlike what happens when you merge two tsconfig files by using the extends field, extending presets will merge all collections, including files, include, exclude and references, which would normally be overwritten, rather than merged.

Oxlint Presets

  oxlint_presets:
    base:
      ignorePatterns:
        - "**/node_modules/**"

    extended:
      extends_presets:
        - base
      ignorePatterns:
        - .cache

Pnpm-workspace Presets

  pnpm_presets:
    base:
      # All the dirs listed here will be created automatically
      # when new monorepos are created
      packages:
        - packages/*
      onlyBuiltDependencies:
        - esbuild
      minimumReleaseAge: 1440

Vitest Presets

  vitest_presets:
    base:
      # Where the config file should be generated, starting from
      # the root of the package.
      # It defaults to the value of `tests_dir`
      out_dir: null

      # The directory to use for tests, from the root of the package
      tests_dir: tests

      # The directory, inside `tests_dir`, that contains the setup files for tests.
      # A file named `tests_setup.ts`, containing some basic testing boilerplate,
      # will be generated inside of it.
      setup_dir: setup

      # A list of setup files (the paths will be joined to the `setup_dir`)
      # `tests_setup.ts` will be added automatically.
      setup_files:
        - somefile.ts
        - anotherfile.ts
      environment: jsdom
      silent: passed-only

Package Presets

A package preset can be used to collect other presets, such as in this example:

  package_presets:
    example:
      name: example

      # For every preset, we can either refer to them
      # by their id...
      package_json: svelte_frontend

      # ...or we can inline a preset definition
      oxlint:
        extends_presets:
          - extended
        ignorePatterns:
          - .output

      # We can define one or many tsconfig files
      ts_config:
        # Can be omitted, would default to `tsconfig.json`
        - output: tsconfig.json
          config:
            # Once again creating a new config here with some extras
            extends_presets:
              - extended
            include:
              - scripts

      vitest: base
Tsconfig output
{
  "include": [
    "scripts",
    "src",
    "tests"
  ],
  "references": [
    {
      "path": "/other/path"
    },
    {
      "path": "/some/path"
    }
  ],
  "compilerOptions": {
    "noEmit": false,
    "verbatimModuleSyntax": true
  }
}
Package.json output
{
  "name": "example",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "build": "vite build",
    "dev": "vite dev"
  },
  "description": "I am the frontend preset",
  "license": "MIT",
  "packageManager": "pnpm",
  "dependencies": {},
  "devDependencies": {
    "svelte": "*",
    "tailwindcss": "*",
    "vite": "*"
  }
}
Oxlintrc.json output
{
  "plugins": [
    "oxc",
    "typescript",
    "unicorn"
  ],
  "ignorePatterns": [
    "**/node_modules/**",
    ".cache",
    ".output"
  ]
}
vitest.config.ts output
import { defineConfig } from "vitest/config";
import { resolve, join } from "node:path";

const j = join;
const r = resolve;

const dir = import.meta.dirname;
const setupDir = r(dir, "setup");

export default defineConfig({
  plugins: [],
  resolve: {
    alias: {
      '@': r(dir, "../src"),
    },
  },

  test: { 
    setupFiles: [ 
      j(setupDir, "tests_setup.ts"),
      j(setupDir, "somefile.ts"),
      j(setupDir, "anotherfile.ts")
    ], 
    globals: true,
    environment: "jsdom",
    silent: "passed-only",
    sequence: {
      setupFiles: "list",
    },
  },
});

Smart Features

Sketch provides some additional features that may come in handy when dealing with a typescript project.

Converting latest to a range

By default, all versions marked as latest will be converted to a version range (minor by default, but can be customized) that starts from the actual latest version for that package.

This means that you can easily reuse your presets over time to start new projects, without needing to manually bump all versions, and while also avoiding latest, which is not suitable for stability.

Sketch uses the npm api to fetch the latest version for any given package. It sends a maximum of 10 parallel requests, but depending on how many requests are made in a given timeframe, you might still be rate limited by the api, causing an error.

typescript:
  # Default
  no_convert_latest_to_range: false

  # Default
  version_range: minor

Adding dependencies to the catalog

If you set up your package manager to be pnpm (which is the default) or bun and set catalog to true, whenever you try to generate a new package that has dependencies marked with catalog:, each package that is not present in the target catalog (inside pnpm-workspace.yaml for pnpm, and inside package.json for bun) will be added automatically to it.

Example

Let's say that we are starting with a basic pnpm-workspace.yaml config like this one:

  pnpm_presets:
    base:
      # All the dirs listed here will be created automatically
      # when new monorepos are created
      packages:
        - packages/*
      onlyBuiltDependencies:
        - esbuild
      minimumReleaseAge: 1440

and we generate a package that has catalog dependencies, which are currently absent from their target catalogs:

typescript:
  no_default_deps: true

  catalog: true

  # Default
  package_manager: pnpm

  package_presets:
    with_catalog:
      name: with_catalog
      package_json:
        devDependencies:
          # Named catalog
          svelte: catalog:svelte
          # Default catalog
          hono: "catalog:"

After running

sketch ts package --preset with_catalog

We can see that the pnpm-workspace.yaml file has been updated with the new named catalog and dependencies:

packages:
- packages/*
catalog:
  hono: ^4.10.3
catalogs:
  svelte:
    svelte: ^5.42.2
onlyBuiltDependencies:
- esbuild
minimumReleaseAge: 1440

Storing And Reusing Contributors' Data

When working with a team on a monorepo that is made of several individual packages, it's very common that the same people have to be added to the author, contributors and maintainers fields in the package.json files for these packages.

To make this job a bit easier, you can use the people field in the typescript config to store information about contributors which you can then simply refer to by their ID.

Example

typescript:
  no_default_deps: true

  people:
    # Person ID
    bruce:
      name: Bruce Wayne
      email: bruce@gotham.com
      url: brucewayne.com

  package_json_presets:
    people-example:
      author: bruce # Using the id from the store
      maintainers:
        # Literal definition
        - name: Clark Kent
          url: clarkkent.com
          email: clark-kent@dailyplanet.com
        - bruce
      contributors:
        - bruce

    # Since package.json presets are extensible,
    # you can use the above as a base for other package.json files,
    # if the author or contributors are always the same
    some-other-project:
      extends_presets:
        - people-example
      scripts:
        say-hi: echo hi!

  package_presets:
    people-example:
      package_json: people-example

After we run

sketch ts package --preset people-example

We get this package.json file in the root of the new package:

{
  "name": "people-example",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {},
  "author": {
    "name": "Bruce Wayne",
    "email": "bruce@gotham.com",
    "url": "brucewayne.com"
  },
  "packageManager": "pnpm",
  "dependencies": {},
  "devDependencies": {},
  "contributors": [
    {
      "name": "Bruce Wayne",
      "email": "bruce@gotham.com",
      "url": "brucewayne.com"
    }
  ],
  "maintainers": [
    {
      "name": "Bruce Wayne",
      "email": "bruce@gotham.com",
      "url": "brucewayne.com"
    },
    {
      "name": "Clark Kent",
      "email": "clark-kent@dailyplanet.com",
      "url": "clarkkent.com"
    }
  ]
}

Typescript Projects

Sketch was initially conceived as a typescript-centric tool, as I needed a way to deal with the chaotic typescript ecosystem in a structured and easily reproducible way.

It supports a rich feature set dedicated to typescript, with dedicated preset categories (such as tsconfig.json and package.json presets) and commands (such as generating individual config files from presets, or generating barrel files).

These are the top level configuration settings for Typescript projects, with their default values:

typescript:
  # Convert `latest` to a version range for dependencies
  no_convert_latest_to_range: false

  # Use this kind of version range when converting from `latest`
  version_range: minor

  # Do not add default dependencies to new `package.json` files (typescript and oxlint, plus vitest if enabled)
  no_default_deps: false

  # Use the pnpm catalog for default dependencies
  # and add missing dependencies to it automatically
  catalog: false

  package_manager: pnpm

  # Configuration for the generated pnpm-workspace.yaml file
  pnpm:
    # All dirs listed here will be created automatically
    packages: ["packages/*"]

  # A map containing extensible package.json presets
  package_json_presets: {}

  # A map containing extensible package presets
  package_presets: {}

  # A map containing extensible tsconfig presets
  ts_config_presets: {}

  # A map containing extensible oxlint presets
  oxlint_presets: {}

  # A map of contributors and maintainers, which can be referenced
  # in `package.json` presets with their ID
  people: {}

Monorepo Generation

To set up a basic monorepo with default settings, we can just run the command sketch ts monorepo. But we can also create a package preset and use that for the root package of the monorepo.

This is a more customized configuration for the root package:

typescript:
  # By default, typescript and oxlint would be added to
  # the root package's dependencies
  no_default_deps: true

  # Pnpm is the default, and if selected, it will generate
  # a pnpm-workspace.yaml file
  package_manager: pnpm

  pnpm_presets:
    base:
      # All the dirs listed here will be created automatically
      # when new monorepos are created
      packages:
        - packages/*
      onlyBuiltDependencies:
        - esbuild
      minimumReleaseAge: 1440

  package_presets:
    # Defining a preset for the root package
    root:
      name: workspace

      package_json:
        workspaces:
          # All dirs listed here are also created automatically
          # unless they contain '{', '[', '?' or '!'
          - apps/**/test
        license: Apache-2.0
        devDependencies:
          husky: ^9.0.0

      # The tsconfig files to generate at the root.
      # If left empty, defaults will be used.
      ts_config:
        - output: tsconfig.options.json
          config:
            compilerOptions:
              verbatimModuleSyntax: true
              strict: true

        - output: tsconfig.json
          config:
            files: []

      # Literal oxlint configuration to generate at the root
      # of the workspace.
      # Can be set to `true` to use defaults.
      oxlint:
        plugins: ["unicorn", "typescript", "oxc"]

Adding Templates

You can also use the with_templates field to automatically generate a certain file structure when the monorepo is generated.

Let's say, for example, that every time that you create a new monorepo, you always want to create a docker directory with a basic dev.dockerfile inside of it, so that you can quickly create a dev environment using docker.

To do that, we add this to the root package's definition:

      with_templates:
        - templates:
            # Relative path from the root of the package
            - output: docker/dev.dockerfile
              template:
                name: dev_dockerfile
                content: |
                  FROM node:23-alpine

                  COPY . .
                  EXPOSE {{ docker_dev_port }}
                  CMD ["npm", "run", "dev"]

vars:
  docker_dev_port: 5173

ℹ️ You can also use --with-template <PRESET_ID|id=TEMPLATE_ID,output=PATH> as a flag to add more templates or templating presets when generating a new package.

Hooks

We can define some commands (which can also be templates) to execute before and/or after generating the new monorepo:

    # Commands to run before generation, from the root of the new project
    hooks_pre:
      - command:
          # Inlining a new template definition here,
          # but as always, we can use stored templates too
          name: pre_hook
          content: "echo '{{ greeting }}' > pre.txt"
        context:
          greeting: hi

    # Commands to run after generation, from the root of the new project
    hooks_post:
      - command:
          name: post_hook
          content: "echo '{{ greeting }}' > post.txt"
        context:
          greeting: hi

Example

So after setting everything up, we run the command

sketch ts monorepo --root-package root --pnpm base

And get this tree output:

β”œβ”€β”€ .oxlintrc.json
β”œβ”€β”€ apps
β”‚Β Β  └── test
β”œβ”€β”€ docker
β”‚Β Β  └── dev.dockerfile
β”œβ”€β”€ package.json
β”œβ”€β”€ packages
β”œβ”€β”€ pnpm-workspace.yaml
β”œβ”€β”€ tsconfig.json
└── tsconfig.options.json

ℹ️ You can use the -i flag to install dependencies for the root package after creating the new monorepo.

Generating A Package

You can use the command sketch ts package to generate a new Typescript package.

Hooks

We can define some commands (which can also be templates) to execute before and/or after generating the new package:

    # Commands to run before generation, from the root of the new project
    hooks_pre:
      - command:
          # Inlining a new template definition here,
          # but as always, we can use stored templates too
          name: pre_hook
          content: "echo '{{ greeting }}' > pre.txt"
        context:
          greeting: hi

    # Commands to run after generation, from the root of the new project
    hooks_post:
      - command:
          name: post_hook
          content: "echo '{{ greeting }}' > post.txt"
        context:
          greeting: hi

Adding Templates

You can also use the with_templates field (or the --with-template cli flag) to specify a list of templates or templating presets that should be generated whenever a package preset is being used.

Let's say for example that in a type of package, you always need a schemas directory, where you import some common schemas from a shared package and define new ones.

You can use this feature to generate a file inside src/schemas/index.ts automatically like this:

      # Inside the package preset configuration
      with_templates:
        # We can refer to a preset by id or create a new one locally
        - templates:
            - output: src/schemas/index.ts
              # This can be a template defined in the config file
              # or file path inside templates_dir
              template: schemas

templates:
  schemas: |
    import z from "zod";
    import { sharedSchemas } from "{{ schemas_package }}";

vars:
  schemas_package: "@myproject/schemas"

Example

We start from this configuration:

typescript:
  # By default, typescript and oxlint are added to
  # the dependencies
  no_default_deps: true

  package_presets:
    frontend:
      # The name of the package. If this is unset,
      # the name of the output directory will be used.
      name: "@myproject/frontend"

      # The `package.json` config for this package. Can be a preset id
      # or a literal configuration.
      package_json:
        devDependencies:
          tailwindcss: "^4.0.0"

      # Generates an oxlint config file in the root.
      # Can be a boolean, a preset id, or
      # a literal configuration.
      oxlint: false

      # Create a vitest setup.
      # Can be a boolean, a preset id, or
      # a literal configuration.
      vitest:
        # Where the config file should be generated, starting from
        # the root of the package.
        # It defaults to the value of `tests_dir`
        out_dir: null

        # The directory to use for tests
        tests_dir: tests

        # The directory containing the setup files for tests.
        # (starting from tests_dir)
        setup_dir: setup

And then run

sketch ts package --preset frontend

To obtain this output in the designated directory:

β”œβ”€β”€ package.json
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ index.ts
β”‚Β Β  └── schemas
β”‚Β Β      └── index.ts
β”œβ”€β”€ tests
β”‚Β Β  β”œβ”€β”€ setup
β”‚Β Β  β”‚Β Β  └── tests_setup.ts
β”‚Β Β  └── vitest.config.ts
└── tsconfig.json

ℹ️ You can also use the -i flag to automatically install the dependencies with your selected package manager whenever a new package is created.

Generating A Barrel File

Sometimes it is useful to keep one or many barrel files in a typescript project, to make it easier to import/export elements from a group of modules.

Sketch has a dedicated command to make this process easier.

Examples

Target directory:

tests/ts_barrel
└── nested
    β”œβ”€β”€ file1.ts
    └── nested2
        └── file2.ts

Command:

sketch ts barrel

Output:

export * from "nested/file1";
export * from "nested/nested2/file2";