@doikayt/typescript-build-config 0.1.18 → 0.1.20

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
@@ -123,7 +123,9 @@ small single-purpose scripts, and the check side mirrors the write side:
123
123
  publish config accordingly (non-destructively):
124
124
 
125
125
  - **Library** — `main` / `types` / `exports` → `dist/index.js`, `files: ["dist"]`,
126
- and `prepack`, so it publishes a compiled, typed ESM package on push.
126
+ and `prepack`, so it publishes a compiled, typed ESM package on push. `init`
127
+ also prompts for the package name (default `@doikayt/<name>`; a bare name is
128
+ scoped to `@doikayt`, an already-scoped name is kept).
127
129
  - **App / CLI** (the default) — marked `private: true`, so a release versions and
128
130
  tags it but never publishes to npm.
129
131
 
@@ -142,8 +144,9 @@ npm install # fetch the declared devD
142
144
 
143
145
  `init` writes the canonical scripts (`ci`, `build`, `test`, `update-all-format`,
144
146
  `check-all-format`, and their sub-tasks), declares `vitest` +
145
- `@doikayt/autogen-markdown-doc` + `typescript`, and seeds a commented
146
- `vitest.config.ts`. Answering the "publishable library?" prompt sets the publish
147
+ `@doikayt/autogen-markdown-doc` + `typescript` + `@changesets/cli` (the release
148
+ pipeline needs it), and seeds a commented `vitest.config.ts`. Answering the
149
+ "publishable library?" prompt sets the publish
147
150
  config — `private` for an app, or `prepack` + the `dist/`-based fields for a
148
151
  library (see [The canonical script set](#the-canonical-script-set)).
149
152
 
@@ -336,8 +339,8 @@ rely on it.
336
339
  ## Release Pipeline
337
340
 
338
341
  On install, the postinstall script copies the release pipeline files into your
339
- project automatically. The package name in `auto-changeset.sh` is substituted
340
- from your project's `package.json` at install time.
342
+ project automatically. `auto-changeset.sh` reads the package name from your
343
+ `package.json` at runtime, so renaming the package needs no pipeline edits.
341
344
 
342
345
  On `npm update`, the behavior per file is:
343
346
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doikayt/typescript-build-config",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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'.",
@@ -31,7 +31,16 @@ export function canonicalScripts({ ui = false, library = false } = {}) {
31
31
  }
32
32
 
33
33
  export function devDependencyNames({ ui = false } = {}) {
34
- const names = ["vitest", "@doikayt/autogen-markdown-doc", "typescript"];
34
+ // @changesets/cli is required by the release pipeline (release.yml runs
35
+ // `npx changeset version` / `publish`), so every consumer needs it installed —
36
+ // otherwise `npx changeset` can't resolve the `changeset` bin and the release
37
+ // job fails with "could not determine executable to run".
38
+ const names = [
39
+ "vitest",
40
+ "@doikayt/autogen-markdown-doc",
41
+ "typescript",
42
+ "@changesets/cli",
43
+ ];
35
44
  if (ui) names.push("@playwright/test");
36
45
  return names;
37
46
  }
package/src/init.js CHANGED
@@ -62,21 +62,29 @@ function createAskYesNo() {
62
62
  closed = true;
63
63
  while (waiters.length) waiters.shift()("");
64
64
  });
65
- const ask = (question) =>
65
+ const nextLine = (question) =>
66
66
  new Promise((res) => {
67
67
  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);
68
+ if (lines.length) res(lines.shift());
69
+ else if (closed) res("");
70
+ else waiters.push(res);
72
71
  });
72
+ const ask = async (question) =>
73
+ /^y(es)?$/i.test((await nextLine(question)).trim());
74
+ ask.text = async (question) => (await nextLine(question)).trim();
73
75
  ask.close = () => rl.close();
74
76
  return ask;
75
77
  }
76
78
 
79
+ // Prepend the @doikayt scope unless the name is already scoped.
80
+ function scopeName(name) {
81
+ return name.startsWith("@") ? name : `@doikayt/${name}`;
82
+ }
83
+
77
84
  export async function runInit({
78
85
  cwd = process.cwd(),
79
86
  prompt,
87
+ promptText,
80
88
  resolveDevVersions = resolveVersions,
81
89
  log = console.log,
82
90
  } = {}) {
@@ -84,16 +92,32 @@ export async function runInit({
84
92
  // on error so a leftover interface never keeps the process alive.
85
93
  const ownAsk = prompt ? null : createAskYesNo();
86
94
  const ask = prompt ?? ownAsk;
95
+ // Free-text prompt (package name). Falls back to "" (accept the default) when a
96
+ // yes/no prompt is injected without a text one, so tests need not supply it.
97
+ const askText = promptText ?? ownAsk?.text ?? (async () => "");
87
98
  try {
88
- return await collectAndScaffold({ cwd, ask, resolveDevVersions, log });
99
+ return await collectAndScaffold({
100
+ cwd,
101
+ ask,
102
+ askText,
103
+ resolveDevVersions,
104
+ log,
105
+ });
89
106
  } finally {
90
107
  ownAsk?.close();
91
108
  }
92
109
  }
93
110
 
94
- async function collectAndScaffold({ cwd, ask, resolveDevVersions, log }) {
111
+ async function collectAndScaffold({
112
+ cwd,
113
+ ask,
114
+ askText,
115
+ resolveDevVersions,
116
+ log,
117
+ }) {
95
118
  const pkgPath = resolve(cwd, "package.json");
96
119
  const raw = readFileSync(pkgPath, "utf8");
120
+ const currentName = JSON.parse(raw).name;
97
121
 
98
122
  // Collect every answer up front, before doing any work or printing next
99
123
  // steps, so the flow reads: ask everything → act → report.
@@ -102,10 +126,21 @@ async function collectAndScaffold({ cwd, ask, resolveDevVersions, log }) {
102
126
  );
103
127
 
104
128
  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] ",
129
+ "Is this a publishable library (published to npm)? Default is No.\n" +
130
+ " No = an app/CLI (marked private, never published). [y/N] ",
107
131
  );
108
132
 
133
+ // The package name is what gets published, so ask only for a library. A bare
134
+ // name gets the @doikayt scope; an already-scoped name is kept.
135
+ let libraryName;
136
+ if (library) {
137
+ const suggested = scopeName(currentName || "package");
138
+ const entered = await askText(
139
+ `Package name for publishing? [${suggested}] `,
140
+ );
141
+ libraryName = entered ? scopeName(entered) : suggested;
142
+ }
143
+
109
144
  const wantDemo = await ask(
110
145
  "Scaffold a starter demo (minimal src module + README + project.json) so\n" +
111
146
  "you can build, test, and release immediately? Default is No; existing\n" +
@@ -115,17 +150,25 @@ async function collectAndScaffold({ cwd, ask, resolveDevVersions, log }) {
115
150
  const scripts = canonicalScripts({ ui, library });
116
151
  const devDependencies = resolveDevVersions(devDependencyNames({ ui }));
117
152
 
153
+ const fields = packageFields({ library });
154
+ // `npm init -y` seeds these placeholders; treat them as unset so the canonical
155
+ // `test` script and (for a library) `main` replace them instead of shadowing.
156
+ const replaceDefaults = {
157
+ scripts: { test: 'echo "Error: no test specified" && exit 1' },
158
+ fields: { main: "index.js" },
159
+ };
160
+ if (library) {
161
+ // The chosen name is a deliberate publish decision, so it replaces whatever
162
+ // name npm init left in place.
163
+ fields.name = libraryName;
164
+ if (currentName != null) replaceDefaults.fields.name = currentName;
165
+ }
166
+
118
167
  const { text, added, skipped } = applyToPackageJson(raw, {
119
168
  scripts,
120
169
  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
- },
170
+ fields,
171
+ replaceDefaults,
129
172
  });
130
173
  writeFileSync(pkgPath, text);
131
174
 
@@ -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 });