@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 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 # if starting fresh
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. The package name in `auto-changeset.sh` is substituted
341
- from your project's `package.json` at install time.
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/e2e-verification.md](docs/e2e-verification.md).
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.19",
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). Today the only subcommand is `init`; this
6
- // dispatcher is the seam where future ones (e.g. `check-conventions`) plug in.
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)"}\nUsage: typescript-build-config init`,
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 ask = (question) =>
66
+ const nextLine = (question) =>
66
67
  new Promise((res) => {
67
68
  process.stdout.write(question);
68
- const answer = (line) => res(/^y(es)?$/i.test(line.trim()));
69
- if (lines.length) answer(lines.shift());
70
- else if (closed) answer("");
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({ cwd, ask, resolveDevVersions, log });
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({ cwd, ask, resolveDevVersions, log }) {
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\n" +
106
- " — a non-library (an app/CLI) is marked private so it is never published. [y/N] ",
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: packageFields({ library }),
122
- // `npm init -y` seeds these placeholders; treat them as unset so the
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
- PACKAGES=("__PACKAGE_NAME__")
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)
@@ -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
- let canonical = readFileSync(srcPath, "utf8");
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
+ }