Skip to content

Instantly share code, notes, and snippets.

@so0k
Created April 6, 2026 04:05
Show Gist options
  • Select an option

  • Save so0k/90a0c71e0081d240824a5d479a8a52aa to your computer and use it in GitHub Desktop.

Select an option

Save so0k/90a0c71e0081d240824a5d479a8a52aa to your computer and use it in GitHub Desktop.
project scaffolding landscape

Modern Repository Bootstrapping & Scaffolding Tools

Research Report — April 2026


Executive Summary

The project scaffolding landscape has evolved significantly since Cookiecutter’s 2013 debut. The critical differentiator you’ve identified — continued template updates post-bootstrap — narrows the field dramatically. Only a handful of tools treat the template as a living relationship rather than a one-time copy. This report evaluates tools across your key criteria: day-one setup, ongoing template sync, monorepo compatibility, supply-chain security rollouts, and language-specific ecosystems.


Part 1: Cross-Language Scaffolding Tools

1.1 Copier — The Projen Alternative You Probably Want

What it is: A Python-based CLI and library for rendering project templates from Git repos using Jinja2/YAML. Language-agnostic despite being written in Python.

Why it matters for your use case:

Copier is the strongest candidate for replacing Projen’s “continued updates” capability without its JSII/Node coupling. The update mechanism works through a .copier-answers.yml file tracked in each generated project. When the source template evolves (e.g., adding zizmor workflow scans), you run copier update in each downstream project. Copier replays the original generation, applies the new template version, and produces a Git-mergeable diff — preserving project-specific customizations through a three-way merge.

The update lifecycle works as follows: template authors tag releases in the template repo, and consumers run copier update (or copier update --vcs-ref=v2.0.0 for a specific version). The tool also supports copier check-update for automation — you can wire this into CI to detect when projects have drifted from the latest template.

Advantages over Projen:

  • No JSII dependency. Templates are plain directories with a copier.yml config — any language, any runtime.
  • The update mechanism is Git-native (diffs, merge conflicts) rather than a proprietary synth pipeline.
  • Works naturally with monorepos: you can run copier update in any subdirectory. The template doesn’t assume it owns the repo root or the .github/workflows directory.
  • Template authoring uses YAML + Jinja2 rather than imperative TypeScript class hierarchies. This is dramatically simpler to reason about and customize.
  • Supports conditional questions via when: — template consumers only see questions relevant to their choices, reducing prompt fatigue.

Limitations:

  • Requires Python 3.10+ and Git 2.27+ on the machine running updates (though uvx copier handles this cleanly via Astral’s uv).
  • The merge algorithm occasionally struggles with deeply restructured templates — copier recopy exists as a fallback but loses the smart diff.
  • No built-in task runner — you define _tasks in copier.yml (post-generation hooks), but ongoing task orchestration is left to Makefiles, Just, or Taskfiles.

Fleet-wide rollout pattern for security fixes:

# In each project repo (or scripted across many):
uvx copier update --skip-tasks --defaults
git diff  # review changes
git add -A && git commit -m "chore: sync template v2.1.0 (adds zizmor scanning)"

For organizations managing dozens/hundreds of repos, this can be scripted with gh repo list + a loop. Some teams use Renovate or Dependabot-style bots that open PRs when the template repo publishes a new tag.

Real-world adoption: Diamond Light Source uses Copier + uv for all their Python projects with automated template sync. The Substrate project (formerly Poetry Cookiecutter) migrated from Cookiecutter to Copier specifically for the update capability.

1.2 Cookiecutter — Still Alive, but Frozen in Time

Cookiecutter remains the most widely-known scaffolding tool with thousands of community templates. However, it fundamentally lacks update capability — once you generate a project, the template relationship is severed. Cruft was created as a wrapper to add update support on top of Cookiecutter templates, but it’s a bolt-on solution that doesn’t match Copier’s native design.

For new projects in 2026, Cookiecutter is hard to recommend unless you’re consuming an existing community template that hasn’t been ported to Copier.

1.3 Scaffold (hay-kot) — Interesting but No Update Story

A Go-based Cookiecutter alternative that adds in-project scaffolding (generating components/controllers within an existing project via a .scaffolds directory). This is useful for day-two code generation but lacks the template update mechanism entirely. Best treated as a complement to Copier rather than a replacement.

1.4 Yeoman — Legacy, Avoid for New Projects

Node.js-based, npm ecosystem dependent, no template update mechanism. The generator ecosystem is stale. Not recommended for 2026 projects.


Part 2: Projen — Deep Analysis of Strengths and Pain Points

2.1 What Projen Gets Right

Projen’s fundamental insight is correct: project configuration should be code, not copy-pasted YAML. By defining your project structure in a .projenrc.ts file and running npx projen to synthesize all managed files, you get:

  • Deterministic, reproducible project structure from a declarative definition.
  • The ability to publish “project types” as npm packages that encode organizational standards.
  • npx projen re-synthesizes on every run — upstream template changes propagate when you bump the project type version.

2.2 The JSII Problem

Projen’s multi-language support is built on JSII (the AWS CDK’s cross-language compilation layer). This means:

  • All project types are authored in TypeScript and cross-compiled to Python/Java/Go.
  • The cross-compiled APIs feel unnatural in target languages.
  • JSII has a limited type system that doesn’t support all TypeScript features (no union types, no overloads, limited generics).
  • Bun is not supported — JSII requires Node.js and npm/yarn. Deno support is similarly absent.
  • The JSII compilation step adds significant complexity to template authoring and debugging.

2.3 The Monorepo Problem

This is where Projen falls apart for your use case. The core issues:

Workflow assumption: Each Projen project assumes it controls .github/workflows/ at the repository root. In a monorepo, you have N projects but one .github/ directory. The AWS PDK’s MonorepoTsProject attempts to solve this by wrapping NX, but it introduces its own opinionated layer that conflicts with Turborepo.

Task runner collision: Projen defines its own task runner (npx projen <task>). In a Turborepo monorepo, you want turbo run build to orchestrate builds with caching. Having Projen tasks and Turbo tasks creates confusion — which task runner is authoritative? The Turbo pipeline needs to call Projen tasks, but Projen’s task graph doesn’t understand Turbo’s dependency topology.

The projen-turborepo community package (by moltar) exists but is experimental, requires opt-in flags for basic features (path mapping, project references, parallel workflows), and the maintainer acknowledges it’s not production-ready.

Independent versioning: In a monorepo, each package has its own version and release lifecycle. Projen’s release automation assumes a single project = single release. Multi-package version management needs Changesets, release-please, or semantic-release with monorepo plugins — tools that don’t integrate cleanly with Projen’s opinion about releases.

2.4 Verdict on Projen

Use Projen if: you’re building single-repo Node.js/TypeScript projects (especially AWS CDK apps) and your team is comfortable with the JSII ecosystem.

Avoid Projen if: you need Bun/Deno support, monorepo setups with Turborepo, polyglot projects, or if template authoring simplicity matters.


Part 3: Monorepo Tooling & the Scaffolding Intersection

3.1 Turborepo

Turborepo (now part of Vercel) is the dominant JS/TS monorepo task runner. Its scaffolding story is intentionally minimal — it delegates to Plop for code generation within a monorepo:

turbo gen   # Uses @turbo/gen, powered by Plop templates

This is a deliberate design choice: keep the task runner focused on task orchestration and caching, let a dedicated tool handle scaffolding. The turbo gen command creates new packages within an existing monorepo — it’s not for bootstrapping the monorepo itself.

For initial monorepo bootstrap: npx create-turbo@latest sets up a pnpm workspace with apps/packages directories, a turbo.json, and example packages. This is a one-time scaffold with no update mechanism.

Security rollout pattern: In a monorepo, security fixes (like adding zizmor scans) are simpler because all projects share a single .github/workflows/ directory. One PR updates CI for all packages. The challenge is per-package configuration (like package-specific lint rules or build flags) — Turborepo handles this through turbo.json pipeline definitions and per-package turbo.json overrides.

Versioning: Turborepo doesn’t handle versioning — you need Changesets (@changesets/cli) for independent package versioning, or release-please for conventional-commit-based releases. Git tags apply to the full repo, so you use scoped tags like @myorg/package-a@1.2.3.

3.2 Nx

Nx (by Nrwl, which acquired Lerna) offers more integrated scaffolding than Turborepo through its generator system. Nx generators can create new projects, components, and libraries within a monorepo with full awareness of the dependency graph. However, Nx is heavier and more opinionated — it wants to own your build pipeline.

3.3 Vite+ / Vite Task (Emerging)

VoidZero’s Vite+ (alpha as of early 2026) bundles Vite, Vitest, Oxlint, Oxfmt, Rolldown, and a task runner into a single CLI (vp). The vp create command scaffolds projects with all tooling pre-configured in a single vite.config.ts. This eliminates the “wire up ESLint + Prettier + Vitest” problem, but it’s early — no monorepo support, no Bun support, and the alpha has DX rough edges.

Worth watching closely, especially since Turborepo itself has migrated to Oxlint and Oxfmt internally.


Part 4: Language-Specific Ecosystems

4.1 JavaScript / TypeScript

The Modern Vite/Vitest/Oxc Stack

For new TypeScript projects in 2026, the recommended toolchain is converging on:

Concern Tool Notes
Bundler / Dev Server Vite 8 (with Rolldown) 1.6–7.7x faster production builds than Vite 7
Test Runner Vitest Native Vite integration, fast watch mode
Linter Oxlint 50–100x faster than ESLint, type-aware linting in alpha
Formatter Oxfmt 30x faster than Prettier, 100% Prettier-compatible
Type Checking tsc –noEmit (or ty from Astral, beta) Still needed — none of the above do type checking

Type stripping on Node.js 24+: Node 24 (LTS since October 2025) enables type stripping by default for .ts files. This means node app.ts just works — types are erased at parse time, no compilation step. The recommended tsconfig.json:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true,
    "rewriteRelativeImportExtensions": true,
    "noEmit": true
  }
}

Critical limitation for CDK projects: Type stripping only works with “erasable” TypeScript syntax. The following features require --experimental-transform-types or a full compiler:

  • enum declarations (require JavaScript code generation)
  • Constructor parameter properties (constructor(private name: string))
  • Legacy decorators (experimentalDecorators: true) — used heavily by NestJS, TypeORM
  • Namespace declarations

AWS CDK constructs use enum extensively, and CDK custom constructs often use parameter properties. For CDK projects, you still need tsc or esbuild/tsx for compilation. For non-CDK TypeScript projects on Node 24+, type stripping eliminates the build step for development and scripts.

Framework-Specific Scaffolding (create-*)

The npm create <framework> pattern (which calls create-<framework>) remains the standard for framework-specific bootstrapping:

  • npm create vite@latest — Vite + React/Vue/Svelte/etc.
  • npm create next-app@latest — Next.js
  • npm create nuxt@latest — Nuxt
  • npm create astro@latest — Astro

These are one-time scaffolds with no update mechanism. For organizations wanting controlled templates, Copier or a custom create-* package wrapping your opinionated stack is the way forward.

Bun Ecosystem

Bun provides bun create <template> which clones from GitHub repos. It also supports bun init for minimal project setup. Bun has its own test runner (bun test), bundler (bun build), and package manager. The Bun ecosystem is viable for applications but less mature for library publishing — npm compatibility is good but not perfect.

Key consideration: Bun doesn’t support JSII (ruling out Projen). Bun’s built-in TypeScript support is full transpilation (not just type stripping), so it handles enums, decorators, and parameter properties natively.

4.2 Go

Single Module

Go has no official scaffolding tool. go mod init creates a go.mod file and that’s it. The community tools are:

  • Autostrada — a web-based generator that produces customized Go application scaffolds (HTTP framework choice, database, migrations, etc.). Produces a one-time download, no update mechanism.
  • go-scaffold — Go template engine for generating projects from directory templates. No update mechanism.
  • Copier — works perfectly for Go templates. You define a template repo with go.mod.jinja, main.go.jinja, etc., and Copier handles generation and updates.

The idiomatic Go approach is minimalist: go mod init, create your directory layout (follow the community conventions from golang-standards/project-layout or the simpler flat layout), and add tooling config manually. Scaffolding tools are less common in Go because the language’s stdlib-first philosophy means less boilerplate configuration.

Workspaces (Multi-Module)

Go workspaces (go.work) have been stable since Go 1.18 and are well-supported in Go 1.26. The go.work file at the repo root lists member modules:

go 1.26

use (
    ./api
    ./shared
    ./worker
)

Key points:

  • go.work replaces the old replace directive hack for local development.
  • go work use -r . recursively discovers all modules.
  • go work sync synchronizes dependency versions across workspace modules.
  • go.work should NOT be committed in most cases — it’s a local development convenience. For CI, each module should be independently buildable from its own go.mod.

Scaffolding a Go workspace: No dedicated tool exists. A Copier template that generates the root go.work, creates subdirectories with go.mod files, and sets up a Makefile/Taskfile is the practical approach.

Go 1.26 Specifics

Go 1.26 (released early 2026) introduces syntax improvements (type-safe errors.AsType, pointer creation with new()) but doesn’t change the workspace model. GoLand 2026.1 adds tooling to detect and apply Go 1.26 syntax updates across a codebase — useful for template maintenance.

4.3 Python (uv from Astral)

uv as the Foundation

uv from Astral has become the default Python project manager in 2026, replacing Poetry, pip-tools, and pyenv in a single Rust binary. For scaffolding:

uv init my-project           # Application (no build system)
uv init my-lib --lib          # Library (with build system)
uv init my-pkg --package      # Distributable package
uv init my-ext --build-backend maturin  # Rust extension module

uv init creates: pyproject.toml, main.py or src/ layout, .python-version, .gitignore, and README. It’s intentionally minimal — no CI config, no linter setup, no test configuration.

Copier + uv = The Modern Python Stack

The recommended pattern for Python is:

  1. Copier template that encodes your organizational standards: pyproject.toml with uv as the build backend, Ruff for linting, ty (or mypy/pyright) for type checking, pytest for testing, GitHub Actions workflows (including zizmor scanning), pre-commit config, etc.
  2. uv for day-to-day dependency management, virtual environments, and Python version management.
  3. copier update to propagate template changes (new Ruff rules, updated CI workflows, security fixes).

The Astral ecosystem in 2026:

  • uv — package/project manager (stable, dominant)
  • Ruff — linter/formatter (stable, replaces Flake8/isort/Black)
  • ty — type checker (beta, aims to replace mypy/pyright, full LSP support)
  • pyx — private package registry (early beta/waitlist)

uv Workspaces

uv supports workspaces natively for monorepo-style Python projects:

# pyproject.toml at root
[tool.uv.workspace]
members = ["packages/*"]

Each member has its own pyproject.toml and can depend on siblings. uv lock creates a single lockfile for the entire workspace. This is simpler than Go workspaces (committed by default, single lockfile) but less mature for independent versioning of packages.

4.4 Rust

cargo-generate — The Standard

cargo-generate is the established Rust scaffolding tool. It uses Liquid templates (Shopify’s template language) and Rhai for hook scripts:

cargo generate gh:username/my-template

Templates are Git repos with cargo-generate.toml config. The tool supports:

  • Interactive prompts with defaults
  • Conditional file inclusion
  • Template variables in both file content and file/directory names
  • Pre/post-generation hooks via Rhai scripts
  • Template composition (sub-templates)

No update mechanism. Once generated, the project is independent. For Rust, this matters less because Cargo.toml is simpler than package.json — there’s less boilerplate configuration to keep in sync.

cargo-scaffold — The Alternative

Similar to cargo-generate but uses Handlebars templates and TOML configuration. Less widely adopted, but also language-agnostic (despite the cargo prefix).

Cargo Workspaces (Monorepo)

Cargo has native workspace support via Cargo.toml:

[workspace]
members = ["crates/*"]
resolver = "2"

Cargo workspaces are the most mature of any language’s multi-package solution:

  • Shared Cargo.lock across all members
  • cargo build --workspace builds everything
  • cargo test --workspace tests everything
  • Dependencies can be defined at workspace level and inherited by members
  • cargo publish handles individual crate publishing

For scaffolding new workspace members: No built-in generator. cargo new crates/my-new-crate creates a basic crate, but without workspace-aware templates. A Copier template or a custom script is needed for opinionated crate scaffolds within a workspace.


Part 5: Supply Chain Security & Template Update Rollouts

5.1 The Problem

The tj-actions/changed-files incident (March 2025) demonstrated that GitHub Actions workflows are a critical supply chain attack surface. Unpinned action references, overly broad permissions, and template injection vulnerabilities are pervasive. Tools like zizmor (static analysis for GitHub Actions) can detect these issues, but the challenge is rolling out fixes across many repositories.

5.2 zizmor Integration Pattern

zizmor is a Rust-based static analysis tool that catches ~24 audit rules including:

  • Unpinned action versions (should use commit SHA, not tags)
  • Template injection via ${{ }} expressions
  • Excessive workflow permissions
  • Dangerous triggers (pull_request_target, workflow_run)
  • Known-vulnerable actions
  • Archived/unmaintained action dependencies

Rolling out zizmor via template updates:

For polyrepo fleets using Copier:

# In your Copier template's .github/workflows/security.yml.jinja
name: Security Scan
on: [push, pull_request]
permissions:
  contents: read
  security-events: write
jobs:
  zizmor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false
      - uses: zizmorcore/zizmor-action@e2e1c8af5826ec9e651206e2e18f6aa7a0f1a53e # v1

When this is added to the template:

  1. Tag a new template version
  2. Run copier update across all fleet repos (scripted)
  3. Each repo gets a PR with the new workflow
  4. Review, merge, and zizmor starts scanning

For monorepos: single .github/workflows/ directory means one PR updates security scanning for all packages.

Grafana’s approach: After their 2025 GitHub Actions incident, Grafana deployed zizmor across 2,000+ repos using GitHub Organization rulesets. They created a reusable workflow that runs zizmor with SARIF output uploaded to GitHub Advanced Security, then used org-level rulesets to require this workflow on all repos — no per-repo configuration needed.

5.3 Monorepo vs. Polyrepo for Security

Concern Monorepo Polyrepo + Copier
CI workflow updates Single PR N PRs (scriptable)
Dependency updates Single lockfile, one Dependabot config N lockfiles, N Dependabot configs
zizmor scanning One workflow covers all Template-propagated workflow per repo
Version management Needs Changesets/release-please Git tags per repo (simpler)
Audit surface One repo to audit N repos to audit
Blast radius One compromised workflow = all packages Isolated per repo

Part 6: Recommendations

6.1 Cross-Language Scaffolding with Updates → Copier

Copier is the clear winner for your requirements. It’s language-agnostic, has native Git-based update support, works in monorepo subdirectories, and is installable via uvx copier (no Python project setup needed). Template authoring is YAML + Jinja2 — dramatically simpler than Projen’s TypeScript class hierarchies.

6.2 JavaScript/TypeScript

For new non-CDK projects: Vite + Vitest + Oxlint + Oxfmt, running on Node 24+ with native type stripping. Wrap this in a Copier template for organizational standards.

For CDK projects: Continue using tsc or esbuild for compilation (enums and decorators aren’t erasable). Consider Projen only if you’re deep in the AWS CDK ecosystem and willing to accept its monorepo limitations.

For monorepos: Turborepo with pnpm workspaces. Use Changesets for versioning. Scaffold new packages with turbo gen (Plop-based) or a Copier sub-template.

Watch: Vite+ (vp) for a unified toolchain, but don’t adopt until it exits alpha and adds monorepo support.

6.3 Go

uv + Copier template for standardized project layout. Use go work for multi-module development. No dedicated Go scaffolding tool is needed — the language’s simplicity makes template-based generation via Copier sufficient.

6.4 Python

Copier + uv is the modern standard. The combination gives you template updates and best-in-class dependency/environment management. Add Ruff for linting, ty (beta) or mypy for type checking, pytest for tests.

6.5 Rust

cargo-generate for initial scaffolding, Cargo workspaces for monorepo structure. Rust’s tooling is the most self-contained — cargo handles building, testing, publishing, and workspace management natively. Security tooling (like cargo-audit, cargo-deny) can be added via a Copier-managed CI workflow.

6.6 Supply Chain Security

Adopt zizmor in CI for all repos. For polyrepo fleets, propagate the zizmor workflow via Copier template updates. For monorepos, add it once to the shared workflow. Consider GitHub Organization rulesets (Grafana’s pattern) for enforcement at scale without per-repo configuration.


Appendix: Tool Comparison Matrix

Tool Language Template Updates Monorepo Support Template Authoring Runtime Dependency
Copier Any ✅ Native (Git-based) ✅ Subdirectory-aware YAML + Jinja2 Python 3.10+
Projen JS/TS (JSII for others) ✅ Via npm version bump ⚠️ Partial (NX-based) TypeScript classes Node.js (no Bun)
Cookiecutter Any ❌ (Cruft adds partial) Jinja2 Python
Scaffold (hay-kot) Any ✅ In-project scaffolds Go templates Go binary
cargo-generate Rust (primarily) Liquid + Rhai Rust/cargo
Yeoman Any (JS-centric) JavaScript Node.js
Vite+ (vp create) JS/TS ❌ (planned) Config-based Node.js
create-* (npm) JS/TS Varies Framework-specific Node.js
uv init Python ✅ (workspaces) N/A (minimal) uv binary

This research reflects the state of tooling as of April 2026. The JavaScript tooling ecosystem moves quickly — Vite+, Oxlint type-aware rules, and Astral’s ty are all in active development and may significantly change recommendations within 6–12 months.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment