@doikayt/typescript-build-config 0.1.19 → 0.1.21
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 +53 -6
- package/package.json +1 -1
- package/src/cli.js +14 -3
- package/src/init.js +56 -17
- package/src/new-package.js +38 -0
- package/src/pipeline/auto-changeset.sh +3 -1
- package/src/postinstall.js +1 -10
- package/src/scope-name.js +6 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Shared build configuration presets for TypeScript-based projects.
|
|
|
13
13
|
- [Console / CLI project](#console--cli-project)
|
|
14
14
|
- [UI / web project (with Playwright e2e)](#ui--web-project-with-playwright-e2e)
|
|
15
15
|
- [Existing project](#existing-project)
|
|
16
|
+
- [Trying it out (demo + scratch repo)](#trying-it-out-demo--scratch-repo)
|
|
16
17
|
- [Dependency Strategy](#dependency-strategy)
|
|
17
18
|
- [Current Contents](#current-contents)
|
|
18
19
|
- [Delivery Model](#delivery-model)
|
|
@@ -23,6 +24,7 @@ Shared build configuration presets for TypeScript-based projects.
|
|
|
23
24
|
- [NX projects](#nx-projects)
|
|
24
25
|
- [The `update-all-format` target](#the-update-all-format-target)
|
|
25
26
|
- [Publishing](#publishing)
|
|
27
|
+
- [Team shell aliases](#team-shell-aliases)
|
|
26
28
|
- [For Maintainers](#for-maintainers)
|
|
27
29
|
- [Clone and install](#clone-and-install)
|
|
28
30
|
- [Run the full CI suite](#run-the-full-ci-suite)
|
|
@@ -123,7 +125,9 @@ small single-purpose scripts, and the check side mirrors the write side:
|
|
|
123
125
|
publish config accordingly (non-destructively):
|
|
124
126
|
|
|
125
127
|
- **Library** — `main` / `types` / `exports` → `dist/index.js`, `files: ["dist"]`,
|
|
126
|
-
and `prepack`, so it publishes a compiled, typed ESM package on push.
|
|
128
|
+
and `prepack`, so it publishes a compiled, typed ESM package on push. `init`
|
|
129
|
+
also prompts for the package name (default `@doikayt/<name>`; a bare name is
|
|
130
|
+
scoped to `@doikayt`, an already-scoped name is kept).
|
|
127
131
|
- **App / CLI** (the default) — marked `private: true`, so a release versions and
|
|
128
132
|
tags it but never publishes to npm.
|
|
129
133
|
|
|
@@ -134,7 +138,7 @@ publish config accordingly (non-destructively):
|
|
|
134
138
|
### Console / CLI project
|
|
135
139
|
|
|
136
140
|
```bash
|
|
137
|
-
npm init -y
|
|
141
|
+
npx @doikayt/typescript-build-config new # npm init -y + @doikayt scope
|
|
138
142
|
npm install --save-dev @doikayt/typescript-build-config
|
|
139
143
|
npx @doikayt/typescript-build-config init # answer "n" to Playwright; "n" (app) or "y" (library)
|
|
140
144
|
npm install # fetch the declared devDependencies
|
|
@@ -151,7 +155,7 @@ library (see [The canonical script set](#the-canonical-script-set)).
|
|
|
151
155
|
### UI / web project (with Playwright e2e)
|
|
152
156
|
|
|
153
157
|
```bash
|
|
154
|
-
npm init -y
|
|
158
|
+
npx @doikayt/typescript-build-config new # npm init -y + @doikayt scope
|
|
155
159
|
npm install --save-dev @doikayt/typescript-build-config
|
|
156
160
|
npx @doikayt/typescript-build-config init # answer "y" to the Playwright prompt
|
|
157
161
|
npm install
|
|
@@ -171,6 +175,22 @@ required targets (`ci`, `update-all-format`) until you add them — either by
|
|
|
171
175
|
running `init` to adopt the full canonical set, or by defining the two scripts
|
|
172
176
|
by hand (see [Conventions](#conventions-every-project-must-adhere-to)).
|
|
173
177
|
|
|
178
|
+
### Trying it out (demo + scratch repo)
|
|
179
|
+
|
|
180
|
+
Answer **yes** to the demo prompt and `init` seeds a minimal starter — a `src/`
|
|
181
|
+
module, a `README` with doc-generator markers, and a `project.json` — so a
|
|
182
|
+
brand-new project builds, tests, and (for a library) publishes on push.
|
|
183
|
+
|
|
184
|
+
To exercise a scaffold end-to-end without standing up a repo, push it to the
|
|
185
|
+
shared throwaway repo
|
|
186
|
+
**[`doikayt/scratch-pad`](https://github.com/doikayt/scratch-pad)** (force-overwrite
|
|
187
|
+
each run). The full procedure — for both a library and an app, with what to look
|
|
188
|
+
for at each step — is in [docs/verification-runbook.md](docs/verification-runbook.md):
|
|
189
|
+
|
|
190
|
+
- [Pushing to the scratch repo](docs/verification-runbook.md#pushing-to-a-reusable-scratch-repo)
|
|
191
|
+
- [Testing publish without cluttering npm](docs/verification-runbook.md#testing-publish-without-cluttering-npm)
|
|
192
|
+
— dry-run / verdaccio, and deleting a real test publish afterward.
|
|
193
|
+
|
|
174
194
|
## Dependency Strategy
|
|
175
195
|
|
|
176
196
|
`eslint` and `prettier` are listed as `dependencies` and are pulled in
|
|
@@ -183,6 +203,8 @@ versions of these tools may produce peer dependency conflicts.
|
|
|
183
203
|
|
|
184
204
|
## Current Contents
|
|
185
205
|
|
|
206
|
+
- `new` command (`npx @doikayt/typescript-build-config new`) — runs `npm init -y`
|
|
207
|
+
then scopes the package name to `@doikayt` (an existing scope is kept)
|
|
186
208
|
- `init` scaffolder CLI (`npx @doikayt/typescript-build-config init`) — writes
|
|
187
209
|
the canonical npm-script set, declares dev dependencies, and seeds config
|
|
188
210
|
templates (see [Usage](#usage))
|
|
@@ -337,8 +359,8 @@ rely on it.
|
|
|
337
359
|
## Release Pipeline
|
|
338
360
|
|
|
339
361
|
On install, the postinstall script copies the release pipeline files into your
|
|
340
|
-
project automatically.
|
|
341
|
-
|
|
362
|
+
project automatically. `auto-changeset.sh` reads the package name from your
|
|
363
|
+
`package.json` at runtime, so renaming the package needs no pipeline edits.
|
|
342
364
|
|
|
343
365
|
On `npm update`, the behavior per file is:
|
|
344
366
|
|
|
@@ -471,6 +493,31 @@ release job until one is committed.
|
|
|
471
493
|
**Manual publish (emergency):** trigger the workflow manually via
|
|
472
494
|
**GitHub Actions → CI / Release → Run workflow** on the `main` branch.
|
|
473
495
|
|
|
496
|
+
## Team shell aliases
|
|
497
|
+
|
|
498
|
+
This repo doubles as the team's base setup. After cloning it, install the shared
|
|
499
|
+
shell aliases — a **required** onboarding step, so everyone creates and scaffolds
|
|
500
|
+
repos the same way:
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
git clone git@github.com:doikayt/typescript-build-config.git
|
|
504
|
+
cd typescript-build-config
|
|
505
|
+
./shell/install.sh # adds a `source` line to ~/.zshrc / ~/.bashrc (idempotent)
|
|
506
|
+
exec $SHELL # reload — or: source shell/aliases.sh
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Manual alternative: add `source <clone-path>/shell/aliases.sh` to your shell rc.
|
|
510
|
+
|
|
511
|
+
Aliases provided ([`shell/aliases.sh`](shell/aliases.sh)) — require `gh`
|
|
512
|
+
(authenticated) and `node`/`npm`:
|
|
513
|
+
|
|
514
|
+
| Alias | What it does |
|
|
515
|
+
| --- | --- |
|
|
516
|
+
| `mkrepo <name>` | Create a public repo in the `doikayt` org (guards against duplicates). |
|
|
517
|
+
| `dk-new` | `npm init -y` + `@doikayt` scope. |
|
|
518
|
+
| `dk-init` | Scaffold the build config into the current project. |
|
|
519
|
+
| `dk-scaffold <name> [lib\|app]` | Create the repo, scaffold, and push — one command (defaults to app). |
|
|
520
|
+
|
|
474
521
|
## For Maintainers
|
|
475
522
|
|
|
476
523
|
### Clone and install
|
|
@@ -507,7 +554,7 @@ npm run update-all-format && npm run ci
|
|
|
507
554
|
|
|
508
555
|
To verify end-to-end that `init` scaffolds and ships both a library and an app —
|
|
509
556
|
by hand, with what to look for at each step — follow
|
|
510
|
-
[docs/
|
|
557
|
+
[docs/verification-runbook.md](docs/verification-runbook.md).
|
|
511
558
|
|
|
512
559
|
## License
|
|
513
560
|
|
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.21",
|
|
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'.",
|
package/src/cli.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Main CLI entry for this package, registered as the `typescript-build-config`
|
|
3
3
|
// bin. Invoked as `npx @doikayt/typescript-build-config <subcommand>` — npx
|
|
4
4
|
// resolves the bin whose name matches the package. argv[2] is the subcommand
|
|
5
|
-
// (argv[0]=node, argv[1]=this file).
|
|
6
|
-
//
|
|
5
|
+
// (argv[0]=node, argv[1]=this file). This dispatcher is the seam where future
|
|
6
|
+
// subcommands (e.g. `check-conventions`) plug in.
|
|
7
7
|
import { runInit } from "./init.js";
|
|
8
|
+
import { runNew } from "./new-package.js";
|
|
8
9
|
|
|
9
10
|
const sub = process.argv[2];
|
|
10
11
|
|
|
@@ -18,9 +19,19 @@ if (sub === "init") {
|
|
|
18
19
|
process.exit(1);
|
|
19
20
|
},
|
|
20
21
|
);
|
|
22
|
+
} else if (sub === "new") {
|
|
23
|
+
// `npm init -y` + @doikayt scope. Synchronous (spawnSync).
|
|
24
|
+
try {
|
|
25
|
+
runNew();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(err?.message ?? err);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
21
31
|
} else {
|
|
22
32
|
console.error(
|
|
23
|
-
`Unknown command: ${sub ?? "(none)"}\
|
|
33
|
+
`Unknown command: ${sub ?? "(none)"}\n` +
|
|
34
|
+
`Usage: typescript-build-config <new|init>`,
|
|
24
35
|
);
|
|
25
36
|
process.exit(1);
|
|
26
37
|
}
|
package/src/init.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./canonical-scripts.js";
|
|
17
17
|
import { applyToPackageJson } from "./package-json.js";
|
|
18
18
|
import { resolveVersions } from "./dep-versions.js";
|
|
19
|
+
import { scopeName } from "./scope-name.js";
|
|
19
20
|
|
|
20
21
|
const templatesDir = fileURLToPath(new URL("templates/", import.meta.url));
|
|
21
22
|
|
|
@@ -62,14 +63,16 @@ function createAskYesNo() {
|
|
|
62
63
|
closed = true;
|
|
63
64
|
while (waiters.length) waiters.shift()("");
|
|
64
65
|
});
|
|
65
|
-
const
|
|
66
|
+
const nextLine = (question) =>
|
|
66
67
|
new Promise((res) => {
|
|
67
68
|
process.stdout.write(question);
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
else
|
|
71
|
-
else waiters.push(answer);
|
|
69
|
+
if (lines.length) res(lines.shift());
|
|
70
|
+
else if (closed) res("");
|
|
71
|
+
else waiters.push(res);
|
|
72
72
|
});
|
|
73
|
+
const ask = async (question) =>
|
|
74
|
+
/^y(es)?$/i.test((await nextLine(question)).trim());
|
|
75
|
+
ask.text = async (question) => (await nextLine(question)).trim();
|
|
73
76
|
ask.close = () => rl.close();
|
|
74
77
|
return ask;
|
|
75
78
|
}
|
|
@@ -77,6 +80,7 @@ function createAskYesNo() {
|
|
|
77
80
|
export async function runInit({
|
|
78
81
|
cwd = process.cwd(),
|
|
79
82
|
prompt,
|
|
83
|
+
promptText,
|
|
80
84
|
resolveDevVersions = resolveVersions,
|
|
81
85
|
log = console.log,
|
|
82
86
|
} = {}) {
|
|
@@ -84,16 +88,32 @@ export async function runInit({
|
|
|
84
88
|
// on error so a leftover interface never keeps the process alive.
|
|
85
89
|
const ownAsk = prompt ? null : createAskYesNo();
|
|
86
90
|
const ask = prompt ?? ownAsk;
|
|
91
|
+
// Free-text prompt (package name). Falls back to "" (accept the default) when a
|
|
92
|
+
// yes/no prompt is injected without a text one, so tests need not supply it.
|
|
93
|
+
const askText = promptText ?? ownAsk?.text ?? (async () => "");
|
|
87
94
|
try {
|
|
88
|
-
return await collectAndScaffold({
|
|
95
|
+
return await collectAndScaffold({
|
|
96
|
+
cwd,
|
|
97
|
+
ask,
|
|
98
|
+
askText,
|
|
99
|
+
resolveDevVersions,
|
|
100
|
+
log,
|
|
101
|
+
});
|
|
89
102
|
} finally {
|
|
90
103
|
ownAsk?.close();
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
async function collectAndScaffold({
|
|
107
|
+
async function collectAndScaffold({
|
|
108
|
+
cwd,
|
|
109
|
+
ask,
|
|
110
|
+
askText,
|
|
111
|
+
resolveDevVersions,
|
|
112
|
+
log,
|
|
113
|
+
}) {
|
|
95
114
|
const pkgPath = resolve(cwd, "package.json");
|
|
96
115
|
const raw = readFileSync(pkgPath, "utf8");
|
|
116
|
+
const currentName = JSON.parse(raw).name;
|
|
97
117
|
|
|
98
118
|
// Collect every answer up front, before doing any work or printing next
|
|
99
119
|
// steps, so the flow reads: ask everything → act → report.
|
|
@@ -102,10 +122,21 @@ async function collectAndScaffold({ cwd, ask, resolveDevVersions, log }) {
|
|
|
102
122
|
);
|
|
103
123
|
|
|
104
124
|
const library = await ask(
|
|
105
|
-
"Is this a publishable library (published to npm)? Default is No
|
|
106
|
-
"
|
|
125
|
+
"Is this a publishable library (published to npm)? Default is No.\n" +
|
|
126
|
+
" No = an app/CLI (marked private, never published). [y/N] ",
|
|
107
127
|
);
|
|
108
128
|
|
|
129
|
+
// The package name is what gets published, so ask only for a library. A bare
|
|
130
|
+
// name gets the @doikayt scope; an already-scoped name is kept.
|
|
131
|
+
let libraryName;
|
|
132
|
+
if (library) {
|
|
133
|
+
const suggested = scopeName(currentName || "package");
|
|
134
|
+
const entered = await askText(
|
|
135
|
+
`Package name for publishing? [${suggested}] `,
|
|
136
|
+
);
|
|
137
|
+
libraryName = entered ? scopeName(entered) : suggested;
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
const wantDemo = await ask(
|
|
110
141
|
"Scaffold a starter demo (minimal src module + README + project.json) so\n" +
|
|
111
142
|
"you can build, test, and release immediately? Default is No; existing\n" +
|
|
@@ -115,17 +146,25 @@ async function collectAndScaffold({ cwd, ask, resolveDevVersions, log }) {
|
|
|
115
146
|
const scripts = canonicalScripts({ ui, library });
|
|
116
147
|
const devDependencies = resolveDevVersions(devDependencyNames({ ui }));
|
|
117
148
|
|
|
149
|
+
const fields = packageFields({ library });
|
|
150
|
+
// `npm init -y` seeds these placeholders; treat them as unset so the canonical
|
|
151
|
+
// `test` script and (for a library) `main` replace them instead of shadowing.
|
|
152
|
+
const replaceDefaults = {
|
|
153
|
+
scripts: { test: 'echo "Error: no test specified" && exit 1' },
|
|
154
|
+
fields: { main: "index.js" },
|
|
155
|
+
};
|
|
156
|
+
if (library) {
|
|
157
|
+
// The chosen name is a deliberate publish decision, so it replaces whatever
|
|
158
|
+
// name npm init left in place.
|
|
159
|
+
fields.name = libraryName;
|
|
160
|
+
if (currentName != null) replaceDefaults.fields.name = currentName;
|
|
161
|
+
}
|
|
162
|
+
|
|
118
163
|
const { text, added, skipped } = applyToPackageJson(raw, {
|
|
119
164
|
scripts,
|
|
120
165
|
devDependencies,
|
|
121
|
-
fields
|
|
122
|
-
|
|
123
|
-
// canonical `test` script and (for a library) `main` replace them instead of
|
|
124
|
-
// shadowing them.
|
|
125
|
-
replaceDefaults: {
|
|
126
|
-
scripts: { test: 'echo "Error: no test specified" && exit 1' },
|
|
127
|
-
fields: { main: "index.js" },
|
|
128
|
-
},
|
|
166
|
+
fields,
|
|
167
|
+
replaceDefaults,
|
|
129
168
|
});
|
|
130
169
|
writeFileSync(pkgPath, text);
|
|
131
170
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
import { scopeName } from "./scope-name.js";
|
|
5
|
+
|
|
6
|
+
// Default `npm init -y` runner. Injectable so runNew is testable without npm.
|
|
7
|
+
function npmInitY(cwd) {
|
|
8
|
+
const res = spawnSync("npm", ["init", "-y"], { cwd, stdio: "inherit" });
|
|
9
|
+
if (res.error) throw res.error;
|
|
10
|
+
if (res.status !== 0)
|
|
11
|
+
throw new Error(`npm init -y exited with ${res.status}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Run `npm init -y`, then scope the generated package name to @doikayt unless it
|
|
15
|
+
// is already scoped (any @scope/ is left alone). Writes package.json back and
|
|
16
|
+
// returns the final name plus whether it changed.
|
|
17
|
+
export function runNew({
|
|
18
|
+
cwd = process.cwd(),
|
|
19
|
+
npmInit = npmInitY,
|
|
20
|
+
log = console.log,
|
|
21
|
+
} = {}) {
|
|
22
|
+
npmInit(cwd);
|
|
23
|
+
|
|
24
|
+
const pkgPath = resolve(cwd, "package.json");
|
|
25
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
26
|
+
const before = pkg.name || "package";
|
|
27
|
+
const scoped = scopeName(before);
|
|
28
|
+
|
|
29
|
+
if (scoped !== pkg.name) {
|
|
30
|
+
pkg.name = scoped;
|
|
31
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
32
|
+
log(`Scoped package name: ${before} → ${scoped}`);
|
|
33
|
+
} else {
|
|
34
|
+
log(`Package name already scoped: ${scoped}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { name: scoped, changed: scoped !== before };
|
|
38
|
+
}
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
CHANGESET_DIR=".changeset"
|
|
18
|
-
|
|
18
|
+
# Read the package name from package.json at runtime, so it stays correct even if
|
|
19
|
+
# the project is renamed after install — package.json is the single source of truth.
|
|
20
|
+
PACKAGES=("$(node -p "require('./package.json').name")")
|
|
19
21
|
|
|
20
22
|
# Skip if a manually-authored changeset already exists
|
|
21
23
|
EXISTING=$(find "$CHANGESET_DIR" -name "*.md" ! -name "README.md" | wc -l)
|
package/src/postinstall.js
CHANGED
|
@@ -61,8 +61,6 @@ if (alreadyExist.length > 0) {
|
|
|
61
61
|
|
|
62
62
|
// --- Pipeline files: per-file copy/warn/diff ---
|
|
63
63
|
|
|
64
|
-
const projectName = projectPkg.name;
|
|
65
|
-
|
|
66
64
|
const pipelineFiles = [
|
|
67
65
|
{ src: "release.yml", dest: ".github/workflows/release.yml" },
|
|
68
66
|
{ src: "changeset-config.json", dest: ".changeset/config.json" },
|
|
@@ -77,14 +75,7 @@ for (const { src, dest } of pipelineFiles) {
|
|
|
77
75
|
const srcPath = resolve(pipelineDir, src);
|
|
78
76
|
const destPath = resolve(projectRoot, dest);
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (src === "auto-changeset.sh") {
|
|
83
|
-
canonical = canonical.replace(
|
|
84
|
-
'PACKAGES=("__PACKAGE_NAME__")',
|
|
85
|
-
`PACKAGES=("${projectName}")`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
78
|
+
const canonical = readFileSync(srcPath, "utf8");
|
|
88
79
|
|
|
89
80
|
if (!existsSync(destPath)) {
|
|
90
81
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Prepend the @doikayt scope unless the name is already scoped — any existing
|
|
2
|
+
// @scope/ is left as-is. Shared by `init` (library name prompt) and the `new`
|
|
3
|
+
// command (scoped `npm init`).
|
|
4
|
+
export function scopeName(name) {
|
|
5
|
+
return name.startsWith("@") ? name : `@doikayt/${name}`;
|
|
6
|
+
}
|