@doikayt/typescript-build-config 0.1.19 → 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
 
@@ -337,8 +339,8 @@ rely on it.
337
339
  ## Release Pipeline
338
340
 
339
341
  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.
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.
342
344
 
343
345
  On `npm update`, the behavior per file is:
344
346
 
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.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'.",
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 });