@doikayt/typescript-build-config 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,10 +5,18 @@ Shared build configuration presets for TypeScript-based projects.
5
5
  <!-- TOC:START -->
6
6
  - [@doikayt/typescript-build-config](#doikayttypescript-build-config)
7
7
  - [Purpose](#purpose)
8
+ - [Design Goals](#design-goals)
9
+ - [What each component is for](#what-each-component-is-for)
8
10
  - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [The canonical script set](#the-canonical-script-set)
13
+ - [Console / CLI project](#console--cli-project)
14
+ - [UI / web project (with Playwright e2e)](#ui--web-project-with-playwright-e2e)
15
+ - [Existing project](#existing-project)
9
16
  - [Dependency Strategy](#dependency-strategy)
10
17
  - [Current Contents](#current-contents)
11
18
  - [Delivery Model](#delivery-model)
19
+ - [Enforcement Model](#enforcement-model)
12
20
  - [Release Pipeline](#release-pipeline)
13
21
  - [Conventions Every Project Must Adhere To](#conventions-every-project-must-adhere-to)
14
22
  - [The `ci` script](#the-ci-script)
@@ -31,6 +39,51 @@ The plugin encapsulates common build **policy** and release **workflow logic**
31
39
  via the pipeline files it installs into each consumer repo and the canonical release process in
32
40
  [docs/RELEASE-PROCESS.md](docs/RELEASE-PROCESS.md).
33
41
 
42
+ ## Design Goals
43
+
44
+ The package housed in this repo is designed to be 'upstream-most' -- i.e., all other
45
+ packages depend on it (possibly transitively) and this package has no other
46
+ dependencies in this repo.
47
+
48
+ Two goals drive every design decision made for this package, and they pull in different
49
+ directions:
50
+
51
+ 1. **Consistency enforcement** — every `@doikayt` TypeScript repo should expose
52
+ the same command surface (`ci` and `update-all-format`) and share the same
53
+ lint / format / release policy, so tooling, CI, and contributors can assume
54
+ an identical shape in any repo.
55
+ 2. **Minimal per-project setup** — standing up a new repo, or absorbing a policy
56
+ change into an old one, should cost as close to zero manual wiring as
57
+ possible.
58
+
59
+ This package reconciles these two via one architectural choice: **it both
60
+ _defines_ the conventions and _distributes_ them.** A plain template repo would
61
+ copy conventions once and let them drift; a linter would enforce them but set
62
+ nothing up. The npm package defined in this repo does both — it is the single
63
+ artifact that decides "every repo has a `ci` gate" and also installs and
64
+ re-checks it. That single source of truth is what keeps the ecosystem coherent
65
+ as it grows.
66
+
67
+ ### What each component is for
68
+
69
+ Each artifact this package ships exists to serve one or both goals. (See
70
+ [Delivery Model](#delivery-model) for _how_ each is delivered; the table below
71
+ is _why_.)
72
+
73
+ | Component | Value it delivers | Serves |
74
+ | --- | --- | --- |
75
+ | **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 |
76
+ | **Stubs** — `src/top-level/*` seeded into the project root | Zero-config entry points that only `extends` the presets | Minimal setup |
77
+ | **Pipeline** — `src/pipeline/*` copied on install | A working release + changeset workflow with no hand-wiring; drift is diff-warned | Both |
78
+ | **Convention checks** — postinstall warns on missing `ci` / `update-all-format` | An install-time nudge toward the shared command surface | Consistency |
79
+ | **`ci` gate** — `release.yml` runs `npm run ci` | Fail-closed enforcement: a repo that ignores the convention cannot release | Consistency (hard teeth) |
80
+ | **Policy doc** — `docs/RELEASE-PROCESS.md`, linked never copied | One canonical release policy, impossible to drift | Consistency |
81
+ | **Assets** — `assets/*` seeded into `docs/assets/` | Shared brand logos with no per-repo copies to maintain | Minimal setup |
82
+
83
+ The two goals map onto two lifecycle phases — **delivery** (getting canonical
84
+ artifacts into a consuming repo) and **enforcement** (keeping that repo conformant over
85
+ time). Each has its own top-level section below.
86
+
34
87
  ## Installation
35
88
 
36
89
  ```bash
@@ -40,6 +93,66 @@ npm install --save-dev @doikayt/typescript-build-config
40
93
  The postinstall script copies starter config files into your project root and
41
94
  sets up the release pipeline (see below).
42
95
 
96
+ ## Usage
97
+
98
+ After installing, run the `init` scaffolder to write the
99
+ [canonical npm-script set](#the-canonical-script-set) and declare the dev
100
+ dependencies those scripts need. `init` is interactive and **idempotent** — it
101
+ only adds what is missing and never overwrites a script or config you already
102
+ have, so it is safe to re-run.
103
+
104
+ ### The canonical script set
105
+
106
+ Every project gets the same named scripts, so any repo responds identically to
107
+ `npm run ci`, `npm run update-all-format`, and so on. The aggregates chain to
108
+ small single-purpose scripts, and the check side mirrors the write side:
109
+
110
+ | Script | What it does |
111
+ | --- | --- |
112
+ | `ci` | The release gate: `check-all-format` then `test` (plus `test:e2e` for UI). |
113
+ | `test` | Unit / integration tests (`vitest run`). |
114
+ | `test:e2e` | End-to-end tests (`playwright test`) — UI projects only. |
115
+ | `update-all-format` | Reformat everything: code + markdown docs (write). |
116
+ | `check-all-format` | Verify formatting: code + markdown docs (used by `ci`). |
117
+ | `update-code-formatting` / `check-code-formatting` | Prettier write / check. |
118
+ | `update-markdown-docs` / `check-markdown-docs` | Regenerate / verify generated markdown (TOC, UML, …). |
119
+
120
+ ### Console / CLI project
121
+
122
+ ```bash
123
+ npm init -y # if starting fresh
124
+ npm install --save-dev @doikayt/typescript-build-config
125
+ npx @doikayt/typescript-build-config init # answer "n" to the Playwright prompt
126
+ npm install # fetch the declared devDependencies
127
+ ```
128
+
129
+ `init` writes the canonical scripts (`ci`, `test`, `update-all-format`,
130
+ `check-all-format`, and their sub-tasks), declares `vitest` +
131
+ `@doikayt/autogen-markdown-doc`, and seeds a commented `vitest.config.ts`.
132
+
133
+ ### UI / web project (with Playwright e2e)
134
+
135
+ ```bash
136
+ npm init -y
137
+ npm install --save-dev @doikayt/typescript-build-config
138
+ npx @doikayt/typescript-build-config init # answer "y" to the Playwright prompt
139
+ npm install
140
+ # then fill in the TODOs in the generated playwright.config.ts (webServer, baseURL)
141
+ ```
142
+
143
+ Answering **yes** to the Playwright prompt additionally: adds a `test:e2e`
144
+ script and folds it into `ci` (so the release gate runs e2e), declares
145
+ `@playwright/test`, and seeds a `playwright.config.ts` template. Browsers are
146
+ installed on demand by the bundled `doikayt-playwright-install` wrapper the
147
+ `test:e2e` script calls — no manual `playwright install` step, locally or in CI.
148
+
149
+ ### Existing project
150
+
151
+ Skip `init` if you prefer. On install, `postinstall` warns about any missing
152
+ required targets (`ci`, `update-all-format`) until you add them — either by
153
+ running `init` to adopt the full canonical set, or by defining the two scripts
154
+ by hand (see [Conventions](#conventions-every-project-must-adhere-to)).
155
+
43
156
  ## Dependency Strategy
44
157
 
45
158
  `eslint` and `prettier` are listed as `dependencies` and are pulled in
@@ -52,6 +165,11 @@ versions of these tools may produce peer dependency conflicts.
52
165
 
53
166
  ## Current Contents
54
167
 
168
+ - `init` scaffolder CLI (`npx @doikayt/typescript-build-config init`) — writes
169
+ the canonical npm-script set, declares dev dependencies, and seeds config
170
+ templates (see [Usage](#usage))
171
+ - `doikayt-playwright-install` bin — env-gated Playwright browser installer used
172
+ by a UI project's `test:e2e` script (adds `--with-deps` only on CI+Linux)
55
173
  - ESLint config
56
174
  - Prettier config
57
175
  - TypeScript config (`tsconfig.json`, `tsconfig.test.json`, `tsconfig.eslint.json`)
@@ -87,9 +205,21 @@ versions of these tools may produce peer dependency conflicts.
87
205
 
88
206
  ## Delivery Model
89
207
 
90
- This package standardizes downstream repos through three delivery channels — script and
91
- config artifacts that _implement_ the common policy, and a canonical document that
92
- _states_ it:
208
+ _Phase 1 of 2: getting canonical artifacts **into** a repo. Keeping a repo
209
+ conformant over time is the [Enforcement Model](#enforcement-model)._
210
+
211
+ This package standardizes downstream repos by delivering **five components through three
212
+ channels** — so as you read the diagram, note that the component count and the channel
213
+ count deliberately differ; several components share a channel. The components are the
214
+ presets, stubs, pipeline, and assets that _implement_ the common policy, plus a canonical
215
+ document that _states_ it. Each reaches the consumer via one of three channels:
216
+
217
+ 1. **Referenced in place** — presets (stay in `node_modules`)
218
+ 2. **Copied on install** — stubs, pipeline, assets
219
+ 3. **Linked, never copied** — policy
220
+
221
+ The diagram below numbers those three channels; the terms after it define the five
222
+ components.
93
223
 
94
224
  ```
95
225
  ┌─────────────────────────────────────────────────┐
@@ -150,6 +280,42 @@ _states_ it:
150
280
  their own contributor docs (as the consumer box shows) — never copied, so it cannot
151
281
  drift.
152
282
 
283
+ ## Enforcement Model
284
+
285
+ _Phase 2 of 2: keeping a repo **conformant** over time, once delivery has seeded
286
+ it. Getting artifacts in is the [Delivery Model](#delivery-model) above._
287
+
288
+ Delivery gets canonical artifacts into a consuming repo; enforcement keeps that repo aligned
289
+ as both it and the upstream evolve. The two phases share machinery:
290
+ **`postinstall` is the workhorse of both** — it _delivers_ on first install
291
+ (copying absent files) and _enforces_ on every subsequent run that rebuilds this
292
+ package into `node_modules` — a fresh `npm install`, an `npm ci` (which always
293
+ rebuilds the whole tree), or an `npm update` that bumps this package — warning on
294
+ drift and on missing conventions. A no-op `npm install` with an unchanged tree
295
+ does not re-trigger it.
296
+
297
+ Enforcement runs on a spectrum from soft to hard:
298
+
299
+ - **Soft — install-time warnings (name-level).** On every `npm install` /
300
+ `npm update`, postinstall warns (non-fatal) when a required target
301
+ (`ci`, `update-all-format`) is missing, and prints a diff when a copied
302
+ pipeline or asset file has drifted from its canonical template. See
303
+ [Release Pipeline](#release-pipeline) for the per-file drift behavior and
304
+ [Conventions Every Project Must Adhere To](#conventions-every-project-must-adhere-to)
305
+ for the required targets.
306
+ - **Hard — the fail-closed `ci` gate.** The release workflow runs `npm run ci`;
307
+ a repo without a working `ci` gate fails the build and cannot publish. This is
308
+ the only enforcement with real teeth — see
309
+ [The `ci` script](#the-ci-script).
310
+
311
+ One deliberate limitation: the soft checks are **name-level only**. They verify
312
+ a `ci` script _exists_, not that it runs anything meaningful — `"ci": "echo ok"`
313
+ satisfies the warning. Guaranteeing that `ci` actually does its job is left to
314
+ the fail-closed pipeline, where a broken gate surfaces as a red build. Put
315
+ differently: delivery is "deep" (full config seeded) while continuous
316
+ enforcement is "shallow" (presence, not behavior) — a gap worth knowing when you
317
+ rely on it.
318
+
153
319
  ## Release Pipeline
154
320
 
155
321
  On install, the postinstall script copies the release pipeline files into your
@@ -173,8 +339,8 @@ configuration is needed.
173
339
  ## Conventions Every Project Must Adhere To
174
340
 
175
341
  Every project that installs this package must implement two named entry points.
176
- `postinstall` warns on every `npm install` or `npm update` until each is in
177
- place.
342
+ `postinstall` warns whenever it re-runs (a fresh `npm install`, an `npm ci`, or
343
+ an `npm update` that bumps this package) until each is in place.
178
344
 
179
345
  ### The `ci` script
180
346
 
@@ -199,11 +365,27 @@ remote CI job will pass.
199
365
 
200
366
  #### NX projects
201
367
 
202
- NX-based projects must still expose `ci` as a thin shim in `package.json`, even
203
- if it only delegates to NX targets:
368
+ `package.json` is the source of truth — it is always present, and the release
369
+ workflow calls `npm run ci` directly. So an NX project keeps the **real** `ci`
370
+ command in `package.json`, exactly like any other project — it does *not*
371
+ delegate package.json to NX. If you want NX orchestration, add NX targets that
372
+ delegate **to** npm, never the reverse:
204
373
 
205
374
  ```json
206
- { "scripts": { "ci": "nx run-many --target=ci --all" } }
375
+ // package.json — the real command lives here
376
+ { "scripts": { "ci": "npm run check-all-format && npm run test" } }
377
+ ```
378
+
379
+ ```json
380
+ // project.json (optional) — an NX target delegates to the npm script
381
+ {
382
+ "targets": {
383
+ "ci": {
384
+ "executor": "nx:run-commands",
385
+ "options": { "command": "npm run ci" }
386
+ }
387
+ }
388
+ }
207
389
  ```
208
390
 
209
391
  The release workflow is NX-agnostic — it calls `npm run ci`, never NX directly.
@@ -238,8 +420,9 @@ Or as an NX target:
238
420
  }
239
421
  ```
240
422
 
241
- If neither is found, `postinstall` prints a warning (non-fatal) on every
242
- `npm install` or `npm update` until the target is added.
423
+ If neither is found, `postinstall` prints a warning (non-fatal) each time it
424
+ re-runs (a fresh `npm install`, an `npm ci`, or an `npm update` that bumps this
425
+ package) until the target is added.
243
426
 
244
427
  NX projects may define the target in `project.json` instead of `package.json`
245
428
  scripts — the postinstall check recognises both. There is no shim requirement
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doikayt/typescript-build-config",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Shared build configuration for TypeScript projects — ESLint, Prettier, tsconfig, and Playwright presets",
5
5
  "_comments": {
6
6
  "exports": "This block lists the config files that will be extended by files in in src/toplevel - which are copied to consuming project root folder. An example: eslint.config.js imports from '@doikayt/typescript-build-config/eslint'.",
@@ -14,6 +14,10 @@
14
14
  "./tsconfig": "./src/tsconfig.base.json",
15
15
  "./playwright": "./src/playwright.js"
16
16
  },
17
+ "bin": {
18
+ "typescript-build-config": "src/cli.js",
19
+ "doikayt-playwright-install": "src/playwright-install.js"
20
+ },
17
21
  "files": [
18
22
  "src/",
19
23
  "src/top-level/.gitignore",
@@ -0,0 +1,34 @@
1
+ // The canonical npm-script set init writes into every consumer. package.json is
2
+ // the single source of truth (it is always present); NX projects, if any, add
3
+ // thin targets that delegate to `npm run <x>` — never the reverse. So this set
4
+ // is identical regardless of orchestrator; only the UI/console distinction
5
+ // changes it (Playwright e2e).
6
+
7
+ export function canonicalScripts({ ui = false } = {}) {
8
+ const ci = ui
9
+ ? "npm run check-all-format && npm run test && npm run test:e2e"
10
+ : "npm run check-all-format && npm run test";
11
+
12
+ const scripts = {
13
+ ci,
14
+ test: "vitest run",
15
+ "update-all-format":
16
+ "npm run update-code-formatting && npm run update-markdown-docs",
17
+ "update-code-formatting": "prettier --write src/",
18
+ "update-markdown-docs": "autogen-markdown-doc",
19
+ "check-all-format":
20
+ "npm run check-code-formatting && npm run check-markdown-docs",
21
+ "check-code-formatting": "prettier --check src/",
22
+ "check-markdown-docs": "autogen-markdown-doc check",
23
+ };
24
+
25
+ if (ui) scripts["test:e2e"] = "doikayt-playwright-install && playwright test";
26
+
27
+ return scripts;
28
+ }
29
+
30
+ export function devDependencyNames({ ui = false } = {}) {
31
+ const names = ["vitest", "@doikayt/autogen-markdown-doc"];
32
+ if (ui) names.push("@playwright/test");
33
+ return names;
34
+ }
package/src/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // Main CLI entry for this package, registered as the `typescript-build-config`
3
+ // bin. Invoked as `npx @doikayt/typescript-build-config <subcommand>` — npx
4
+ // resolves the bin whose name matches the package. argv[2] is the subcommand
5
+ // (argv[0]=node, argv[1]=this file). Today the only subcommand is `init`; this
6
+ // dispatcher is the seam where future ones (e.g. `check-conventions`) plug in.
7
+ import { runInit } from "./init.js";
8
+
9
+ const sub = process.argv[2];
10
+
11
+ if (sub === "init") {
12
+ // runInit is async (it prompts via readline); translate resolve/reject into
13
+ // process exit codes so the shell sees success/failure.
14
+ runInit().then(
15
+ () => process.exit(0),
16
+ (err) => {
17
+ console.error(err?.message ?? err);
18
+ process.exit(1);
19
+ },
20
+ );
21
+ } else {
22
+ console.error(
23
+ `Unknown command: ${sub ?? "(none)"}\nUsage: typescript-build-config init`,
24
+ );
25
+ process.exit(1);
26
+ }
@@ -0,0 +1,29 @@
1
+ import { execFileSync } from "child_process";
2
+
3
+ export function toCaretRange(version) {
4
+ return `^${version}`;
5
+ }
6
+
7
+ // Default view: ask npm for a package's latest published version. Injectable so
8
+ // resolveVersions stays unit-testable without shelling out.
9
+ export function npmView(name) {
10
+ return execFileSync("npm", ["view", name, "version"], {
11
+ encoding: "utf8",
12
+ }).trim();
13
+ }
14
+
15
+ // Resolve each package name to a caret range on its latest version. If the
16
+ // lookup fails (offline) or returns nothing, fall back to "latest" so the
17
+ // declared devDependency is still installable once the user runs `npm install`.
18
+ export function resolveVersions(names, view = npmView) {
19
+ const out = {};
20
+ for (const name of names) {
21
+ try {
22
+ const version = view(name);
23
+ out[name] = version ? toCaretRange(version) : "latest";
24
+ } catch {
25
+ out[name] = "latest";
26
+ }
27
+ }
28
+ return out;
29
+ }
package/src/init.js ADDED
@@ -0,0 +1,74 @@
1
+ import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { createInterface } from "readline";
5
+ import { canonicalScripts, devDependencyNames } from "./canonical-scripts.js";
6
+ import { applyToPackageJson } from "./package-json.js";
7
+ import { resolveVersions } from "./dep-versions.js";
8
+
9
+ const templatesDir = fileURLToPath(new URL("templates/", import.meta.url));
10
+
11
+ // Copy a template into the project only when the destination is absent, so a
12
+ // consumer's existing config is never overwritten. Returns what happened.
13
+ function seedFile(src, destPath) {
14
+ if (existsSync(destPath)) return "skipped";
15
+ copyFileSync(resolve(templatesDir, src), destPath);
16
+ return "seeded";
17
+ }
18
+
19
+ // Default interactive prompt. Injectable so runInit is testable without stdin.
20
+ async function askYesNo(question) {
21
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
22
+ try {
23
+ const answer = await new Promise((res) => rl.question(question, res));
24
+ return /^y(es)?$/i.test(answer.trim());
25
+ } finally {
26
+ rl.close();
27
+ }
28
+ }
29
+
30
+ export async function runInit({
31
+ cwd = process.cwd(),
32
+ prompt = askYesNo,
33
+ resolveDevVersions = resolveVersions,
34
+ log = console.log,
35
+ } = {}) {
36
+ const pkgPath = resolve(cwd, "package.json");
37
+ const raw = readFileSync(pkgPath, "utf8");
38
+
39
+ const ui = await prompt(
40
+ "Is this a UI project that needs Playwright (e2e)? [y/N] ",
41
+ );
42
+
43
+ const scripts = canonicalScripts({ ui });
44
+ const devDependencies = resolveDevVersions(devDependencyNames({ ui }));
45
+
46
+ const { text, added, skipped } = applyToPackageJson(raw, {
47
+ scripts,
48
+ devDependencies,
49
+ });
50
+ writeFileSync(pkgPath, text);
51
+
52
+ const configs = [{ name: "vitest.config.ts" }];
53
+ if (ui) configs.push({ name: "playwright.config.ts" });
54
+ const seededConfigs = [];
55
+ const keptConfigs = [];
56
+ for (const { name } of configs) {
57
+ const result = seedFile(name, resolve(cwd, name));
58
+ (result === "seeded" ? seededConfigs : keptConfigs).push(name);
59
+ }
60
+
61
+ const report = (label, keys) => {
62
+ if (keys.length) log(`${label}: ${keys.join(", ")}`);
63
+ };
64
+ report("Added scripts", added.scripts);
65
+ report("Kept existing scripts", skipped.scripts);
66
+ report("Declared devDependencies", added.devDependencies);
67
+ report("Kept existing devDependencies", skipped.devDependencies);
68
+ report("Seeded config files", seededConfigs);
69
+ report("Kept existing config files", keptConfigs);
70
+ log("");
71
+ log("Next: run `npm install` to fetch the newly declared devDependencies.");
72
+
73
+ return { added, skipped, seededConfigs, keptConfigs };
74
+ }
@@ -0,0 +1,60 @@
1
+ // Idempotent helpers for editing a consumer's package.json: add keys only when
2
+ // absent, never overwrite what the consumer already has, and preserve the
3
+ // file's original indentation and trailing-newline style.
4
+
5
+ export function mergeAbsent(existing = {}, additions = {}) {
6
+ const merged = { ...existing };
7
+ const added = [];
8
+ const skipped = [];
9
+ for (const [key, value] of Object.entries(additions)) {
10
+ if (Object.prototype.hasOwnProperty.call(merged, key)) {
11
+ skipped.push(key);
12
+ } else {
13
+ merged[key] = value;
14
+ added.push(key);
15
+ }
16
+ }
17
+ return { merged, added, skipped };
18
+ }
19
+
20
+ export function detectIndent(text) {
21
+ const m = text.match(/\n([\t ]+)/);
22
+ return m ? m[1] : " ";
23
+ }
24
+
25
+ /**
26
+ * Merge scripts / devDependencies into package.json text, adding only absent
27
+ * keys and preserving the file's indentation and trailing-newline style. Does
28
+ * not touch disk — returns the new text plus a report of what changed.
29
+ *
30
+ * @param {string} rawText - current package.json contents
31
+ * @param {{scripts?: Record<string,string>, devDependencies?: Record<string,string>}} additions
32
+ * @returns {{
33
+ * text: string,
34
+ * added: { scripts: string[], devDependencies: string[] },
35
+ * skipped: { scripts: string[], devDependencies: string[] }
36
+ * }} new file text, plus keys added vs. skipped (already present)
37
+ */
38
+ export function applyToPackageJson(
39
+ rawText,
40
+ { scripts = {}, devDependencies = {} } = {},
41
+ ) {
42
+ const pkg = JSON.parse(rawText);
43
+ const indent = detectIndent(rawText);
44
+
45
+ const s = mergeAbsent(pkg.scripts, scripts);
46
+ const d = mergeAbsent(pkg.devDependencies, devDependencies);
47
+
48
+ const next = { ...pkg };
49
+ if (Object.keys(s.merged).length) next.scripts = s.merged;
50
+ if (Object.keys(d.merged).length) next.devDependencies = d.merged;
51
+
52
+ const trailingNewline = rawText.endsWith("\n") ? "\n" : "";
53
+ const text = JSON.stringify(next, null, indent) + trailingNewline;
54
+
55
+ return {
56
+ text,
57
+ added: { scripts: s.added, devDependencies: d.added },
58
+ skipped: { scripts: s.skipped, devDependencies: d.skipped },
59
+ };
60
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "child_process";
3
+ import { realpathSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+
6
+ // Treat CI as active when the CI env var is present and not an explicit falsy
7
+ // value. Most CI systems (including GitHub Actions) set CI=true.
8
+ export function isCI(env = process.env) {
9
+ const v = env.CI;
10
+ if (v === undefined) return false;
11
+ return v !== "" && v !== "false" && v !== "0";
12
+ }
13
+
14
+ // --with-deps installs OS-level libraries and requires root, so it is only safe
15
+ // (and only needed) on Linux CI runners. Everywhere else — dev laptops, NixOS,
16
+ // macOS — install just the browser binary.
17
+ export function playwrightInstallArgs({ ci, platform }) {
18
+ const args = ["install"];
19
+ if (ci && platform === "linux") args.push("--with-deps");
20
+ args.push("chromium");
21
+ return args;
22
+ }
23
+
24
+ export function run({ env = process.env, platform = process.platform } = {}) {
25
+ const args = [
26
+ "playwright",
27
+ ...playwrightInstallArgs({ ci: isCI(env), platform }),
28
+ ];
29
+ const result = spawnSync("npx", args, { stdio: "inherit" });
30
+ return result.status ?? 1;
31
+ }
32
+
33
+ // This file is both an importable module (unit tests import the pure functions)
34
+ // and an executable bin. Guard the CLI action so `run()` — which shells out to
35
+ // `playwright install` — fires only when the file is run directly, never on
36
+ // import. When launched via the npm `.bin/doikayt-playwright-install` shim,
37
+ // argv[1] is a symlink, so realpath both sides before comparing.
38
+ function invokedDirectly() {
39
+ const entry = process.argv[1];
40
+ if (!entry) return false;
41
+ try {
42
+ return realpathSync(entry) === fileURLToPath(import.meta.url);
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ if (invokedDirectly()) {
49
+ process.exit(run());
50
+ }
@@ -147,7 +147,7 @@ const missingTargets = [];
147
147
  if (!projectPkg.scripts?.["ci"]) {
148
148
  missingTargets.push({
149
149
  name: "ci",
150
- hint: ' "ci": "npm run check-all-format && npm run test"',
150
+ hint: ' "ci": "prettier --check src/ && npm test"',
151
151
  nxNote: false,
152
152
  });
153
153
  }
@@ -170,7 +170,7 @@ if (!hasUpdateAllFormat) {
170
170
  if (!hasUpdateAllFormat) {
171
171
  missingTargets.push({
172
172
  name: "update-all-format",
173
- hint: ' "update-all-format": "npm run update-code-formatting && npm run update-markdown-docs"',
173
+ hint: ' "update-all-format": "prettier --write src/"',
174
174
  nxNote: true,
175
175
  });
176
176
  }
@@ -0,0 +1,17 @@
1
+ import { definePlaywrightConfig } from "@doikayt/typescript-build-config/playwright";
2
+
3
+ // Seeded by `typescript-build-config init`. definePlaywrightConfig merges the
4
+ // NixOS system-Chromium path automatically; fill in the TODOs for your app.
5
+ export default definePlaywrightConfig({
6
+ testDir: "tests/e2e",
7
+ // Playwright starts your app before the run and waits for it to be reachable,
8
+ // so `npm run test:e2e` needs no separate server step in CI.
9
+ webServer: {
10
+ command: "npm run start", // TODO: the command that starts your app
11
+ url: "http://localhost:3000", // TODO: the URL Playwright waits for
12
+ reuseExistingServer: true,
13
+ },
14
+ use: {
15
+ baseURL: "http://localhost:3000", // TODO: base URL for page.goto("/")
16
+ },
17
+ });
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ // Seeded by `typescript-build-config init`. Vitest runs TypeScript with zero
4
+ // config, so everything here is optional — it exists as a signpost for the
5
+ // common knobs. Uncomment and adjust as your project needs.
6
+ export default defineConfig({
7
+ test: {
8
+ // include: ["src/**/*.test.ts"],
9
+ // environment: "node", // or "jsdom" / "happy-dom" for DOM tests
10
+ // coverage: { provider: "v8", reporter: ["text", "html"] },
11
+ },
12
+ });