foundryvtt-module-scaffold

v2026.09.24

Scaffold a new FoundryVTT v13 module repo (Vite + TS + bun + biome, CI, release-please) with basic/app/libwrapper variants. Use when bootstrapping or init-ing a foundry module.

GitHub
Install command
npx skhub add laurigates/foundryvtt-module-scaffold
Markdown
SKILL.md

foundryvtt-module-scaffold

Bootstrap a new FoundryVTT v13 module repo built with Vite + TypeScript + bun + biome, leaving only the actual module logic to implement. The generated repo passes just check (typecheck + build + lint + test) from the first commit and distributes via the GitHub-release manifest-URL convention.

When to Use This Skill

Use this skill when...Use the alternative when...
Starting a new FoundryVTT module repo — CI-green TS toolchain, release-please, and a basic/app/libwrapper skeleton before writing module logicYou want the full pipeline (repo created + seeded + gitops-adopted) → foundryvtt-module
Spinning up a FoundryVTT module backlog ideaAdding a feature to an existing module — this creates a new repo

The architecture it scaffolds

TypeScript source in src/ (entry src/module.ts), built to dist/<id>.mjs via Vite library mode. vite-plugin-static-copy places module.json, lang/, styles/ (and templates/ for the app variant) into dist/, which Foundry serves as the module root. tsc --noEmit type-checks; Vite emits — decoupled.

  • Type gate: bun run typecheck → tsc --noEmit. Foundry globals are typed by loose local ambient shims (src/foundry-shims.d.ts), so the build is self-contained and CI-green without the (beta, git-only) fvtt-types. Verify the real Foundry API before relying on a shape — the shims are deliberately loose. Opt into fvtt-types later if you want richer types.
  • Build: bun run build → vite build → dist/<id>.mjs + copied assets.
  • Dev: bun run dev → Vite dev server on :30001 proxying everything to Foundry on :30000 except the module's own files (served with HMR).
  • Distribute via GitHub release: module.json manifest → releases/latest/download/module.json, download → releases/latest/download/<id>.zip. release-please bumps $.version in both package.json and module.json; the release job builds, zips dist/, and attaches the assets. No foundryvtt.com submission is needed to install by URL (only to be listed in the in-app package browser).

Three variants

VariantUse whenAdds on top of basic
basic (default)Settings + lifecycle behavior — the minimal well-formed module.init/ready hooks, a registered world setting, i18n, scoped CSS.
appThe module has a UI panel.An ApplicationV2/HandlebarsApplicationMixin window (src/app.ts + templates/app.hbs) and a game.settings.registerMenu button that opens it.
libwrapperThe module patches a core/system method.src/patches.ts with a libWrapper.register(...) call and a manual monkey-patch fallback when lib-wrapper is absent; a relationships.recommends entry for lib-wrapper.

Decision rule: basic for behavior driven by hooks/settings; app when the module surfaces a window or dialog; libwrapper when it overrides a core method (the conflict-safe way to do that on Foundry). Variants compose conceptually — start from the closest one and add the rest by hand.

How to run

scaffold.py is stdlib-only. Run from the workspace where the module should land (e.g. repos/laurigates/foundryvtt-dev/).

Basic settings+hooks module:

python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-initiative-tweaks --display "Initiative Tweaks" --desc "Small quality-of-life tweaks to the combat initiative tracker."

Module with an ApplicationV2 UI panel:

python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-party-overview --display "Party Overview" --desc "A dockable party status panel for GMs." --variant app

Module that patches a core method via libWrapper:

python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-token-vision-tweak --display "Token Vision Tweak" --desc "Adjusts token vision drawing via a libWrapper-guarded patch." --variant libwrapper

Flags: --name (GitHub repo, e.g. foundryvtt-x), --id (Foundry module id; default = --name minus the leading foundryvtt-), --display (title), --desc, --variant {basic,app,libwrapper}, --fvtt-min / --fvtt-verified (compatibility, default 12 / 13), --publisher (default laurigates), --author, --dir (parent dir, default cwd).

It refuses to overwrite an existing directory.

Verifying a module (--verify)

python3 ${CLAUDE_SKILL_DIR}/scaffold.py --verify foundryvtt-initiative-tweaks

Re-runs the finishing-pass audit against an existing module and emits a machine verdict (MODULE_ID_MATCH=, ESMODULES=, MANIFEST_URL=, DOWNLOAD_URL=, RELEASE_ZIP=, DECLARED_ASSETS=, LOCKFILE=, then ISSUE_COUNT= and STATUS=). Exit 1 on ERROR.

STATUS=Meaning
ERRORFoundry cannot install, load, or update the published module — the id disagrees across module.json / vite.config.ts / src/constants.ts, esmodules names a file the build never emits, or an install URL resolves to a release asset nobody uploads.
WARNIt loads, but something is unfinished (a declared asset that 404s, no bun.lock yet). A fresh scaffold is WARN until bun install runs.
OKNothing outstanding.

The generated repo asserts the same invariants in tests/manifest.test.ts, which its own test CI job runs on every PR — so a rename that drifts one of the five places the module id appears fails a PR, not a user's install.

Alternative: the cargo-generate template (pilot)

templates/foundryvtt-module/ is a cargo-generate port of scaffold.py whose emitted files are real files (so tsc/biome/actionlint can check them) rather than Python strings. Output is byte-identical, enforced by scripts/tests/test-template-parity.sh.

scaffold.py remains the default. Reach for the template to edit the scaffold itself, or to try the flow before it is promoted:

cargo generate --path ${CLAUDE_SKILL_DIR}/../../templates/foundryvtt-module --name foundryvtt-initiative-tweaks --vcs none --define 'display_name=Initiative Tweaks' --define 'description=…' --define variant=basic

Needs cargo-generate locally — not in the base image, and the main cost of the port. CI installs it from the release tarball so the parity gate actually runs (#2221). See templates/README.md for the comparison, the one deliberate divergence (a non-kebab-case name), the Liquid brace-collision fixes, and what promoting the template would take.

What you get

A repo where just check passes from the first commit: a real module.json manifest, package.json (bun scripts), vite.config.ts, strict tsconfig.json, biome.json, vitest.config.ts + a green Vitest smoke test (Foundry globals stubbed in tests/setup.ts) and tests/manifest.test.ts (the manifest-vs-build gate above), .github/workflows/ (ci.yml, release-please.yml), release-please-config.json + manifest, renovate.json, a justfile, src/module.ts + src/settings.ts + src/constants.ts + src/foundry-shims.d.ts, lang/en.json, styles/<id>.css, CLAUDE.md, README.md, LICENSE, and an ADR recording the toolchain decision. The app variant adds src/app.ts + templates/app.hbs; libwrapper adds src/patches.ts.

After scaffolding

The generator prints the exact next steps. In order:

cd foundryvtt-<name>
git init -b main
bun install
just check

Seed main directly (the repo is unprotected until gitops adopts it) — pushing a feature branch first would leave main missing on origin and force a rename + default-branch fixup later. bun install writes bun.lock, which the seed commit must include (CI uses --frozen-lockfile).

Then implement, and wire up infra:

  1. Implement the module — for basic, src/module.ts + src/settings.ts; for app, src/app.ts + templates/app.hbs; for libwrapper, replace the Token._draw example in src/patches.ts with the real target.
  2. Add the repo to gitops/repositories.tf with release_please = true and a foundryvtt topic (mirror the foundryvtt-mcp entry). On apply, gitops pushes the release-please App credentials.

Or skip steps entirely: run the /foundryvtt-module orchestrator, which chains scaffold → gh repo create → seed main → the gitops PR.

Hard rules baked into the output

  • id is the single source of truth. module.json id, the install folder, and the release zip name all derive from --id. Lowercase kebab-case only.
  • ESM-only, paths byte-match the manifest. esmodules references <id>.mjs; the Vite output filename is pinned to match. A mismatch is a silent load failure — tests/manifest.test.ts fails the PR that introduces it.
  • Target the harness-pinned Foundry version. Keep module.json compatibility.{minimum,verified} in sync with what you test against. Verify the Foundry API against https://foundryvtt.com/api/ or the live console — not memory.
  • Do not commit dist/. It is git-ignored and rebuilt; CI builds it for releases.
  • Scoped CSS. Every selector is prefixed with the module id — keep it that way so styles never clobber core or other modules.
  • Never hand-edit CHANGELOG.md or the version fields — release-please owns them (it bumps both package.json and module.json).

Agentic Optimizations

ContextCommand
Scaffold a basic modulepython3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-X --display "X" --desc "…"
Scaffold an app modulepython3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-X --display "X" --desc "…" --variant app
Verify a generated modulecd foundryvtt-X && bun install && just check
Gate the finishing pass (machine verdict)python3 ${CLAUDE_SKILL_DIR}/scaffold.py --verify foundryvtt-X
Check the pilot still matches scaffold.pybash ${CLAUDE_SKILL_DIR}/scripts/tests/test-template-parity.sh
Prove the finishing-pass gate still firesbash ${CLAUDE_SKILL_DIR}/scripts/tests/test-manifest-invariants.sh

Notes & deferrals

  • The biome pin is single-sourced in scaffold.py's BIOME_VERSION constant so biome.json and the CI setup-biome step never drift.
  • Action/tool versions in the generated workflows are current as of scaffolding; the account-wide gitops Renovate App reads the emitted renovate.json and bumps them. No repo-local renovate.yml is emitted: a second runner kept a second dependency dashboard under a second bot identity (#2708).
  • The generated module uses local ambient shims, not fvtt-types. This keeps the build green and self-contained; switch tsconfig types to fvtt-types (github:League-of-Foundry-Developers/foundry-vtt-types#main) for full API types once you need them.
  • Quench (in-Foundry Mocha runner) and Playwright integration tests against the harness are not scaffolded — add them when the module warrants runtime coverage.
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

foundryvtt-plugin/skills/foundryvtt-module-scaffold

Default branch

main

Latest commit

1668324

Tree SHA

b2d4cc3