@doikayt/typescript-build-config
v0.1.32
Published
Shared build configuration for TypeScript projects — ESLint, Prettier, tsconfig, and Playwright presets
Maintainers
Readme
@doikayt/typescript-build-config
Shared build configuration presets for TypeScript-based projects.
- @doikayt/typescript-build-config
Purpose
This package centralises common build tooling configuration, release policy, and content assets
across all TypeScript projects maintained under the @doikayt scope. The goal is a
single source of truth for such settings that should be held constant across
projects, avoiding drift between repos over time.
The plugin encapsulates common build policy and release workflow logic
via the pipeline files it installs into each consumer repo and the canonical release process in
docs/RELEASE-PROCESS.md.
Quick start
These steps set up — in a fresh "new-developer" environment — a local git repo with a skeleton demo app plus all the standard Doikayt build configuration: tooling that auto-documents the code in the repo and lets it be pushed to GitHub, built on GitHub's CI servers, and (for a library) published to npm. Steps are minimal here — each links to the section that explains what and why.
1. One-time machine setup (once per machine)
Clone this base repo and install the shared team
shell aliases. This only sets up the aliases
(dk-scaffold, mkrepo); it does not configure any project:
git clone https://github.com/doikayt/typescript-build-config.git # HTTPS: no SSH key needed
cd typescript-build-config
./assets/shell/install.sh # adds `source assets/shell/aliases.sh` to your rc (idempotent)
exec $SHELL # reload2. Create a new project (repeatable)
The two paths below are alternatives that reach the same result — a project wired to this base package.
dk-scaffold
dk-scaffold is the one-command wrapper that also (optionally) creates the
GitHub repo and pushes it. Just run it with a first argument naming your project
and a second indicating lib (a published library) or app (a standalone
app/utility, the default) — see library vs app and
Packaging concepts:
dk-scaffold my-project libRun dk-scaffold's steps individually
The steps below mirror the work dk-scaffold does under the hood — either for a
brand-new project or to adopt Doikayt standard build config into an existing one.
(Installation and Usage explain each in detail.)
npx @doikayt/typescript-build-config new # npm init -y + @doikayt scope
npm install --save-dev @doikayt/typescript-build-config # postinstall seeds configs + pipeline
npx @doikayt/typescript-build-config init # scripts, deps, publish config, demo
npm install # fetch declared devDependenciesTrying it out
dk-scaffold does a lot under the hood, so the best way to get familiar with it
is to run it at the two levels it supports — each requiring a little more setup
than the last, and revealing a little more of the process. Each level scaffolds a
throwaway app (nothing publishes) unless noted. Do the one-time
machine setup first so dk-scaffold
is on your PATH.
Level 0 — local (no external GitHub/npm accounts needed). Scaffold and run the same gate CI runs — no GitHub repo, no push:
dk-scaffold my-demo --localThat single command runs init (demo=yes, app, no Playwright) then npm run ci.
You'll see the vitest demo tests pass, npm run build emit dist/ + type
declarations, and update-all-format fill the README's TOC + UML markers
(mermaid source — rendered at Level 1). npm publish --dry-run refuses —
proof the app archetype never publishes.
Level 1 — push to your own GitHub. Bring a GitHub account and the gh CLI.
Sign in, point the wrapper at your account, and drop --local:
gh auth login # one-time: sign in to GitHub
export REPO_OWNER=<your-github-username> # target owner for the repo
dk-scaffold my-demo # app (private) — no npm credsNow it creates the repo and pushes — and you see what Level 0 couldn't: the
mermaid diagrams rendered (example
here)
and the CI job go green in Actions (example:
this project's runs).
Push a follow-up feat: commit and the release job version-bumps + tags —
nothing published (app). (The initial chore: scaffold push releases nothing;
feat: / fix: triggers it.)
The screen cast below shows the process for Level 1. Note:
- When we log in to our test account (associated with a dummy GitHub account,
not doikayt) we see:
- no repos initially
- no doikayt aliases registered in
.bashrc
- After we run
dk-scaffold demo, we need to input 'y' to install, then we see:- lots of output showing the project being configured and built
- a push to GitHub
- a build and release triggered on GitHub
- since we did not select
lib, we got an app build profile, for which there is no publish to npm
After we ran the screen cast steps we were able to screenshot how our push triggered a build o onthe Github CI servers.
Design Goals
The package housed in this repo is designed to be 'upstream-most' -- i.e., all other
packages depend on it (possibly transitively) and this package has no other
dependencies on doikait packages (except ONE pinned version specific dependency on build_tools
which we rely on for documentation generation. -- Cycles are generally bad, but this one works!)
Two goals drive every design decision made for this package, and they pull in different directions:
- Consistency enforcement — every
@doikaytTypeScript repo should expose the same command surface (ciandupdate-all-format) and share the same lint / format / release policy, so tooling, CI, and contributors can assume an identical shape in any repo. - Minimal per-project setup — standing up a new repo, or absorbing a policy change into an old one, should cost as close to zero manual wiring as possible.
This package reconciles these two via one architectural choice: it both
defines the conventions and distributes them. A plain template repo would
copy conventions once and let them drift; a linter would enforce them but set
nothing up. The npm package defined in this repo does both — it is the single
artifact that decides "every repo has a ci gate" and also installs and
re-checks it. That single source of truth is what keeps the ecosystem coherent
as it grows.
What each component is for
Each artifact this package ships exists to serve one or both goals. (See Delivery Model for how each is delivered; the table below is why.)
| Component | Value it delivers | Serves |
| --- | --- | --- |
| Presets — src/eslint.js, Prettier, base tsconfig, living in node_modules | One lint / format / TS ruleset; change it once here and every repo picks it up on npm update | Consistency |
| Stubs — src/top-level/* seeded into the project root | Zero-config entry points that only extends the presets | Minimal setup |
| Pipeline — src/pipeline/* copied on install | A working release + changeset workflow with no hand-wiring; drift is diff-warned | Both |
| Convention checks — postinstall warns on missing ci / update-all-format | An install-time nudge toward the shared command surface | Consistency |
| ci gate — release.yml runs npm run ci | Fail-closed enforcement: a repo that ignores the convention cannot release | Consistency (hard teeth) |
| Policy doc — docs/RELEASE-PROCESS.md, linked never copied | One canonical release policy, impossible to drift | Consistency |
| Assets — assets/image/* seeded into docs/assets/ | Shared brand logos with no per-repo copies to maintain | Minimal setup |
| Shell aliases — assets/shell/aliases.sh, cloned + sourced (never shipped to npm) | One standard way for the team to create and scaffold repos (mkrepo, dk-*) | Consistency |
The two goals map onto two lifecycle phases — delivery (getting canonical artifacts into a consuming repo) and enforcement (keeping that repo conformant over time). Each has its own top-level section below.
Installation
npm install --save-dev @doikayt/typescript-build-configThe postinstall script copies starter config files into your project root and sets up the release pipeline (see below).
Usage
After installing, run the init scaffolder to write the
canonical npm-script set, declare the dev
dependencies those scripts need — vitest, @doikayt/autogen-markdown-doc,
typescript, and @changesets/cli (the release pipeline needs it) — and seed a
commented vitest.config.ts. init is interactive and idempotent — it only
adds what is missing and never overwrites a script or config you already have, so
it is safe to re-run.
The canonical script set
Every project gets the same named scripts, so any repo responds identically to
npm run ci, npm run update-all-format, and so on. The aggregates chain to
small single-purpose scripts, and the check side mirrors the write side:
| Script | What it does |
| --- | --- |
| ci | The release gate: check-all-format then test (plus test:e2e for UI). |
| build | Compile TypeScript to dist/ (tsc). |
| test | Unit / integration tests (vitest run). |
| test:e2e | End-to-end tests (playwright test) — UI projects only. |
| prepack | Runs build before packing, so changeset publish ships compiled dist/ — libraries only. |
| update-all-format | Reformat everything: code + markdown docs (write). |
| check-all-format | Verify formatting: code + markdown docs (used by ci). |
| update-code-formatting / check-code-formatting | Prettier write / check. |
| update-markdown-docs / check-markdown-docs | Regenerate / verify generated markdown (TOC, UML, …). |
init also asks whether the project is a publishable library. That answer
sets the package.json fields that control publishing — main, types,
exports, files, prepack, private — non-destructively (it merges, it
doesn't overwrite your other keys):
- Library —
main/types/exports→dist/index.js,files: ["dist"], andprepack, so it publishes a compiled, typed ESM package on push.initalso prompts for the package name (default@doikayt/<name>; a bare name is scoped to@doikayt, an already-scoped name is kept). - App / CLI (the default) — marked
private: true, so a release versions and tags it but never publishes to npm.
type: "module" and build are set either way. New to these terms (compiled
dist/, publish fields, library vs app)? See
Packaging concepts for the background.
Optional Playwright config for Web / UI testing
The above section covers what init decides; the two recipes below are how
you run it. They differ only in the Playwright? answer — n for a
console/CLI or plain library, y for a UI / web project that needs end-to-end
tests. Library-vs-app is an orthogonal choice, so either shape can still be
published or kept private.
Console / CLI or plain library — answer n:
npx @doikayt/typescript-build-config new # npm init -y + @doikayt scope
npm install --save-dev @doikayt/typescript-build-config
npx @doikayt/typescript-build-config init # answer "n" to Playwright; "n" (app) or "y" (library)
npm install # fetch the declared devDependenciesUI / web — answer y: injects the end-to-end setup — adds a test:e2e
script and folds it into ci (so the release gate runs e2e), declares
@playwright/test, and seeds a playwright.config.ts template.
npx @doikayt/typescript-build-config new # npm init -y + @doikayt scope
npm install --save-dev @doikayt/typescript-build-config
npx @doikayt/typescript-build-config init # answer "y" to the Playwright prompt
npm install
# then fill in the TODOs in the generated playwright.config.ts (webServer, baseURL)Browsers are installed on demand by the bundled doikayt-playwright-install
wrapper the test:e2e script calls — no manual playwright install step,
locally or in CI.
Existing project
Skip init if you prefer. On install, postinstall warns about any missing
required targets (ci, update-all-format) until you add them — either by
running init to adopt the full canonical set, or by defining the two scripts
by hand (see Conventions).
Dependency Strategy
eslint and prettier are listed as dependencies and are pulled in
automatically. The @typescript-eslint/* plugins are listed in both
dependencies and peerDependencies — the peer declaration pins the minimum
version to ^8.57.1 to avoid a ts-api-utils incompatibility with TS 5.4+.
This package targets new projects. Use in existing projects that pin older versions of these tools may produce peer dependency conflicts.
Current Contents
newcommand (npx @doikayt/typescript-build-config new) — runsnpm init -ythen scopes the package name to@doikayt(an existing scope is kept)initscaffolder CLI (npx @doikayt/typescript-build-config init) — writes the canonical npm-script set, declares dev dependencies, and seeds config templates (see Usage)doikayt-playwright-installbin — env-gated Playwright browser installer used by a UI project'stest:e2escript (adds--with-depsonly on CI+Linux)ESLint config
Prettier config
TypeScript config (
tsconfig.json,tsconfig.test.json,tsconfig.eslint.json)Postinstall script that copies starter top-level config files into your project root, extending the configs installed under
node_modulesGitHub Actions release workflow (
.github/workflows/release.yml)Changeset config (
.changeset/config.json)Auto-changeset script (
scripts/auto-changeset.sh)NPM token diagnostic workflow (
.github/workflows/verify-npm-token.yml) — see Troubleshooting Publish AuthPlaywright config helper (
@doikayt/typescript-build-config/playwright) —nixChromiumLaunchOptions()discovers the system Chromium on quirky NixOS;definePlaywrightConfig()wrapsdefineConfigand merges the result in. Transparent no-op on platforms where Playwright's bundled browser works.@playwright/testis an optional peer dependency — it is not installed automatically. Any project that imports from@doikayt/typescript-build-config/playwrightmust declare it explicitly in its owndevDependencies:npm install --save-dev @playwright/testimport { definePlaywrightConfig } from "@doikayt/typescript-build-config/playwright"; export default definePlaywrightConfig({ testDir: "code/tests/e2e", testMatch: "**/*.spec.ts", use: { baseURL: "http://localhost:8080" }, });Brand assets (
assets/image/doikayt-logo.png,assets/image/doikayt-logo.svg) — shared logos for use across all@doikaytproject READMEs and documentation
Delivery Model
Phase 1 of 2: getting canonical artifacts into a repo. Keeping a repo conformant over time is the Enforcement Model.
This package standardizes downstream repos by delivering five components through three channels — so as you read the diagram, note that the component count and the channel count deliberately differ; several components share a channel. The components are the presets, stubs, pipeline, and assets that implement the common policy, plus a canonical document that states it. Each reaches the consumer via one of three channels:
- Referenced in place — presets (stay in
node_modules) - Copied on install — stubs, pipeline, assets
- Linked, never copied — policy
The diagram below numbers those three channels; the terms after it define the five components.
┌─────────────────────────────────────────────────┐
│ @doikayt/typescript-build-config │
│ │
│ presets eslint prettier tsconfig playwright│
│ stubs src/top-level/* │
│ pipeline src/pipeline/* │
│ assets assets/image/* │
│ policy docs/RELEASE-PROCESS.md │
└─────────┬───────────────┬───────────────┬───────┘
│ │ │
1 REFERENCED │ 2 COPIED │ 3 LINKED, │
IN PLACE │ ON INSTALL │ NEVER COPIED │
│ │ │
presets stay in │ postinstall │ cited by URL │
node_modules — │ seeds stubs, │ only; one │
updates flow │ pipeline, and │ canonical │
with npm update │ assets once; │ copy — drift │
│ consumer owns │ is impossible │
│ them; drift ⇒ │ │
│ diff warning │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ consumer repo │
│ │
│ seeded stubs: eslint.config.js tsconfig.json │
│ │ prettier.config.js … │
│ └────extends────► presets living in │
│ node_modules (chan. 1) │
│ owned pipeline: .github/workflows/release.yml │
│ scripts/auto-changeset.sh │
│ owned assets: docs/assets/doikayt-logo.* │
│ CONTRIBUTING.md ─cites URL─► RELEASE-PROCESS.md│
└─────────────────────────────────────────────────┘Terms:
- Presets — the real configuration content: the ESLint rules (
src/eslint.js), Prettier options, and base tsconfig this package exports. They stay insidenode_modulesand are never copied — which is what makes them ecosystem-wide levers: change a lint rule once here, publish, and every downstream repo receives the new rule on its nextnpm update, with no per-repo edits. - Stubs — thin files seeded once into the consumer's project root by the postinstall
script (
eslint.config.js,tsconfig.json, …). Their only job is toextends/import the presets: channel 2 seeds these static, copied-once files, and they point back at the evolving content that channel 1 keeps current innode_modules. - Owned pipeline — the release workflow and scripts, copied on install. "Owned" by the consumer repo: it may edit its copies, upstream never overwrites them, and postinstall prints a diff warning when a copy drifts from the canonical template.
- Assets — brand logos seeded once into
docs/assets/by the postinstall script. Same channel-2 treatment as the pipeline: copied if absent, skipped if identical, warning (without a text diff) if diverged. Consumer owns the copy; it will not be silently overwritten. - Policy — the release process itself, stated once in docs/RELEASE-PROCESS.md. Consumers reference it by URL from their own contributor docs (as the consumer box shows) — never copied, so it cannot drift.
Beyond consumer repos — the shell aliases. The five components above all reach
a consuming repo through npm (channels 1–3). The team
shell aliases are the exception: they target the
developer's machine, not a repo, through a fourth channel — cloned and
sourced (git clone this repo + source assets/shell/aliases.sh, via
assets/shell/install.sh). npm never touches them and they never land in a consumer's
package.json or tree. Same goal (a standard way to create repos), different
destination — which is why they sit outside the five-components / three-channels
model rather than as a sixth row in it.
Enforcement Model
Phase 2 of 2: keeping a repo conformant over time, once delivery has seeded it. Getting artifacts in is the Delivery Model above.
Delivery gets canonical artifacts into a consuming repo; enforcement keeps that repo aligned
as both it and the upstream evolve. The two phases share machinery:
postinstall is the workhorse of both — it delivers on first install
(copying absent files) and enforces on every subsequent run that rebuilds this
package into node_modules — a fresh npm install, an npm ci (which always
rebuilds the whole tree), or an npm update that bumps this package — warning on
drift and on missing conventions. A no-op npm install with an unchanged tree
does not re-trigger it.
Enforcement runs on a spectrum from soft to hard:
- Soft — install-time warnings (name-level). On every
npm install/npm update, postinstall warns (non-fatal) when a required target (ci,update-all-format) is missing, and prints a diff when a copied pipeline or asset file has drifted from its canonical template. See Release Pipeline for the per-file drift behavior and Conventions Every Project Must Adhere To for the required targets. - Hard — the fail-closed
cigate. The release workflow runsnpm run ci; a repo without a workingcigate fails the build and cannot publish. This is the only enforcement with real teeth — see Theciscript.
One deliberate limitation: the soft checks are name-level only. They verify
a ci script exists, not that it runs anything meaningful — "ci": "echo ok"
satisfies the warning. Guaranteeing that ci actually does its job is left to
the fail-closed pipeline, where a broken gate surfaces as a red build. Put
differently: delivery is "deep" (full config seeded) while continuous
enforcement is "shallow" (presence, not behavior) — a gap worth knowing when you
rely on it.
Release Pipeline
On install, the postinstall script copies the release pipeline files into your
project automatically. auto-changeset.sh reads the package name from your
package.json at runtime, so renaming the package needs no pipeline edits.
On npm update, the behavior per file is:
- File absent — copied into place
- File identical to canonical — skipped silently
- File differs from canonical — warning printed with a diff; local copy is left untouched for manual review
This means local customisations are never silently overwritten, but you are notified when your copy has drifted from the upstream version.
The release pipeline requires an NPM secret stored at the GitHub organisation
level. All repos under the org inherit it automatically — no per-repo secret
configuration is needed.
Conventions Every Project Must Adhere To
Every project that installs this package must implement two named entry points.
postinstall warns whenever it re-runs (a fresh npm install, an npm ci, or
an npm update that bumps this package) until each is in place.
The ci script
Every project must expose a ci script in package.json. The release workflow
calls npm run ci as its CI gate. A project without a ci script will fail
the CI job — the correct signal that the convention has not been met.
ci is the single entry point for "everything that must pass before a release":
formatting checks, tests, linting — whatever the project requires. What it
calls internally is up to the project:
{
"scripts": { "ci": "prettier --check src/ && vitest run && playwright test" }
}npm run ci doubles as your local "simulate CI" command.
Invariant: if npm run ci passes locally, and you commit and push, the
remote CI job will pass.
NX projects
package.json is the source of truth — it is always present, and the release
workflow calls npm run ci directly. So an NX project keeps the real ci
command in package.json, exactly like any other project — it does not
delegate package.json to NX. If you want NX orchestration, add NX targets that
delegate to npm, never the reverse:
// package.json — the real command lives here
{ "scripts": { "ci": "npm run check-all-format && npm run test" } }// project.json (optional) — an NX target delegates to the npm script
{
"targets": {
"ci": {
"executor": "nx:run-commands",
"options": { "command": "npm run ci" }
}
}
}The release workflow is NX-agnostic — it calls npm run ci, never NX directly.
The update-all-format target
Every project must expose an update-all-format entry point — either a
package.json script or an NX project.json target. This is the single
command for "reformat everything before reviewing a diff": run it before
committing to keep diffs clean and reviewable.
The plugin enforces the name only, not the content:
{
"scripts": {
"update-all-format": "prettier --write src/ && npm run update-markdown-docs"
}
}Or as an NX target:
{
"targets": {
"update-all-format": {
"executor": "nx:run-commands",
"options": { "command": "prettier --write src/" }
}
}
}If neither is found, postinstall prints a warning (non-fatal) each time it
re-runs (a fresh npm install, an npm ci, or an npm update that bumps this
package) until the target is added.
NX projects may define the target in project.json instead of package.json
scripts — the postinstall check recognises both. There is no shim requirement
for update-all-format (unlike ci), because no external tooling calls it
directly.
Publishing
Releases are automated via Changesets and GitHub Actions. The full policy —
commit-prefix → bump mapping, forcing or suppressing a release, resolving
changeset status errors, verifying a release, troubleshooting publish auth —
is documented in docs/RELEASE-PROCESS.md. That
document is the canonical release policy for every repo that installs this
package.
Quick reference:
For a patch release — push a fix: commit to main. The workflow handles
the rest.
For a minor or major release — run npx changeset locally, choose the
bump level at the prompt, commit the generated .changeset/*.md file, then
push. The auto-generation step is skipped when a handwritten changeset is
present. For majors this is mandatory: a breaking-change commit (feat!: or
BREAKING CHANGE in the body) with no handwritten changeset fails the
release job until one is committed.
Manual publish (emergency): trigger the workflow manually via
GitHub Actions → CI / Release → Run workflow on the main branch.
Team shell aliases
Installed by Quick start step 1 (or manually: add
source <clone-path>/assets/shell/aliases.sh to your shell rc). They require gh
(authenticated) and node/npm.
| Alias | What it does |
| --- | --- |
| mkrepo <name> | Create a public repo in the doikayt org (guards against duplicates). |
| dk-new | npm init -y + @doikayt scope. |
| dk-init | Scaffold the build config into the current project. |
| dk-scaffold <name>\|. [lib\|app] | Create the repo, scaffold, and push — one command (defaults to app). Pass . to scaffold in the current directory, using its leaf name as @doikayt/<package-name>. |
For Maintainers
Clone and install
git clone [email protected]:doikayt/typescript-build-config.git
cd typescript-build-config
npm installThe postinstall script detects that it is running inside the plugin repo itself and exits immediately — no config stubs or pipeline files are copied.
Run the full CI suite
npm run ciThis is the same entry point mandated for every consumer repo. In this project it runs a Prettier format check followed by the full test suite:
npm run ci → prettier --check src/ → node --testA passing npm run ci locally means the remote CI job will pass.
To auto-fix formatting before verifying:
npm run update-all-format && npm run ciTo verify end-to-end that init scaffolds and ships both a library and an app —
by hand, with what to look for at each step — follow
docs/verification-runbook.md.
License
MIT
