@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 +193 -10
- package/package.json +5 -1
- package/src/canonical-scripts.js +34 -0
- package/src/cli.js +26 -0
- package/src/dep-versions.js +29 -0
- package/src/init.js +74 -0
- package/src/package-json.js +60 -0
- package/src/playwright-install.js +50 -0
- package/src/postinstall.js +2 -2
- package/src/templates/playwright.config.ts +17 -0
- package/src/templates/vitest.config.ts +12 -0
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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)
|
|
242
|
-
`npm install` or `npm update`
|
|
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.
|
|
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
|
+
}
|
package/src/postinstall.js
CHANGED
|
@@ -147,7 +147,7 @@ const missingTargets = [];
|
|
|
147
147
|
if (!projectPkg.scripts?.["ci"]) {
|
|
148
148
|
missingTargets.push({
|
|
149
149
|
name: "ci",
|
|
150
|
-
hint: ' "ci": "
|
|
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": "
|
|
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
|
+
});
|