@doikayt/typescript-build-config 0.1.16 → 0.1.18
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/package.json +1 -1
- package/src/init.js +89 -39
- package/src/package-json.js +20 -1
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.18",
|
|
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
|
@@ -42,33 +42,74 @@ function dirIsEmpty(dir) {
|
|
|
42
42
|
return !existsSync(dir) || readdirSync(dir).length === 0;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Default interactive prompt.
|
|
46
|
-
|
|
45
|
+
// Default interactive prompt. Buffers input lines as they arrive and lets each
|
|
46
|
+
// question pull the next one, so it works with both a live terminal and piped /
|
|
47
|
+
// non-interactive stdin (scripts, CI). `rl.question` alone drops lines that
|
|
48
|
+
// arrive before the next call — with a pipe every line lands at once, which made
|
|
49
|
+
// init silently no-op. On EOF, unanswered prompts default to No. Injectable, so
|
|
50
|
+
// runInit stays testable without stdin.
|
|
51
|
+
function createAskYesNo() {
|
|
47
52
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const lines = []; // input lines not yet consumed by a prompt
|
|
54
|
+
const waiters = []; // prompt resolvers waiting for the next line
|
|
55
|
+
let closed = false;
|
|
56
|
+
rl.on("line", (line) => {
|
|
57
|
+
const waiter = waiters.shift();
|
|
58
|
+
if (waiter) waiter(line);
|
|
59
|
+
else lines.push(line);
|
|
60
|
+
});
|
|
61
|
+
rl.on("close", () => {
|
|
62
|
+
closed = true;
|
|
63
|
+
while (waiters.length) waiters.shift()("");
|
|
64
|
+
});
|
|
65
|
+
const ask = (question) =>
|
|
66
|
+
new Promise((res) => {
|
|
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);
|
|
72
|
+
});
|
|
73
|
+
ask.close = () => rl.close();
|
|
74
|
+
return ask;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
export async function runInit({
|
|
57
78
|
cwd = process.cwd(),
|
|
58
|
-
prompt
|
|
79
|
+
prompt,
|
|
59
80
|
resolveDevVersions = resolveVersions,
|
|
60
81
|
log = console.log,
|
|
61
82
|
} = {}) {
|
|
83
|
+
// One readline interface for all prompts (see createAskYesNo); close it even
|
|
84
|
+
// on error so a leftover interface never keeps the process alive.
|
|
85
|
+
const ownAsk = prompt ? null : createAskYesNo();
|
|
86
|
+
const ask = prompt ?? ownAsk;
|
|
87
|
+
try {
|
|
88
|
+
return await collectAndScaffold({ cwd, ask, resolveDevVersions, log });
|
|
89
|
+
} finally {
|
|
90
|
+
ownAsk?.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function collectAndScaffold({ cwd, ask, resolveDevVersions, log }) {
|
|
62
95
|
const pkgPath = resolve(cwd, "package.json");
|
|
63
96
|
const raw = readFileSync(pkgPath, "utf8");
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
// Collect every answer up front, before doing any work or printing next
|
|
99
|
+
// steps, so the flow reads: ask everything → act → report.
|
|
100
|
+
const ui = await ask(
|
|
101
|
+
"Is this a UI project that needs Playwright (e2e)? Default is No. [y/N] ",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
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] ",
|
|
67
107
|
);
|
|
68
108
|
|
|
69
|
-
const
|
|
70
|
-
"
|
|
71
|
-
"
|
|
109
|
+
const wantDemo = await ask(
|
|
110
|
+
"Scaffold a starter demo (minimal src module + README + project.json) so\n" +
|
|
111
|
+
"you can build, test, and release immediately? Default is No; existing\n" +
|
|
112
|
+
"files are never overwritten. [y/N] ",
|
|
72
113
|
);
|
|
73
114
|
|
|
74
115
|
const scripts = canonicalScripts({ ui, library });
|
|
@@ -78,6 +119,13 @@ export async function runInit({
|
|
|
78
119
|
scripts,
|
|
79
120
|
devDependencies,
|
|
80
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
|
+
},
|
|
81
129
|
});
|
|
82
130
|
writeFileSync(pkgPath, text);
|
|
83
131
|
|
|
@@ -90,6 +138,16 @@ export async function runInit({
|
|
|
90
138
|
(result === "seeded" ? seededConfigs : keptConfigs).push(name);
|
|
91
139
|
}
|
|
92
140
|
|
|
141
|
+
const demo = wantDemo
|
|
142
|
+
? scaffoldDemo({ cwd })
|
|
143
|
+
: {
|
|
144
|
+
scaffolded: false,
|
|
145
|
+
reason: "declined",
|
|
146
|
+
wrote: [],
|
|
147
|
+
skipped: [],
|
|
148
|
+
paths: [],
|
|
149
|
+
};
|
|
150
|
+
|
|
93
151
|
const report = (label, keys) => {
|
|
94
152
|
if (keys.length) log(`${label}: ${keys.join(", ")}`);
|
|
95
153
|
};
|
|
@@ -101,28 +159,28 @@ export async function runInit({
|
|
|
101
159
|
report("Kept existing package fields", skipped.fields);
|
|
102
160
|
report("Seeded config files", seededConfigs);
|
|
103
161
|
report("Kept existing config files", keptConfigs);
|
|
104
|
-
|
|
105
|
-
|
|
162
|
+
report("Demo seeded", demo.wrote);
|
|
163
|
+
report("Demo kept existing", demo.skipped);
|
|
106
164
|
|
|
107
|
-
|
|
165
|
+
log("");
|
|
166
|
+
if (demo.scaffolded) {
|
|
167
|
+
log(
|
|
168
|
+
"Next: `npm install`, then `npm test` and `npm run update-all-format`, " +
|
|
169
|
+
"then commit and push to cut a release.",
|
|
170
|
+
);
|
|
171
|
+
log(`To remove the starter files later: rm -r ${demo.paths.join(" ")}`);
|
|
172
|
+
} else {
|
|
173
|
+
log("Next: run `npm install` to fetch the newly declared devDependencies.");
|
|
174
|
+
}
|
|
108
175
|
|
|
109
176
|
return { added, skipped, seededConfigs, keptConfigs, demo };
|
|
110
177
|
}
|
|
111
178
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
const wantDemo = await prompt(
|
|
118
|
-
"Scaffold a starter demo (minimal src module + README + project.json) so you\n" +
|
|
119
|
-
"can build, test, and release immediately? Existing files are never\n" +
|
|
120
|
-
"overwritten. [y/N] ",
|
|
121
|
-
);
|
|
122
|
-
if (!wantDemo) {
|
|
123
|
-
return { scaffolded: false, reason: "declined", wrote: [], skipped: [] };
|
|
124
|
-
}
|
|
125
|
-
|
|
179
|
+
// Seed a minimal starter into a new project: a src/ module, a README with
|
|
180
|
+
// doc-generator markers, and a project.json — each independently and only when
|
|
181
|
+
// its slot is empty, so nothing a consumer already has is overwritten. Writes
|
|
182
|
+
// files only and returns what it did; the caller reports and prints next steps.
|
|
183
|
+
function scaffoldDemo({ cwd }) {
|
|
126
184
|
const demoTemplate = resolve(templatesDir, "demo");
|
|
127
185
|
const wrote = [];
|
|
128
186
|
const skipped = [];
|
|
@@ -152,13 +210,5 @@ async function maybeScaffoldDemo({ cwd, prompt, log }) {
|
|
|
152
210
|
}
|
|
153
211
|
}
|
|
154
212
|
|
|
155
|
-
log("");
|
|
156
|
-
if (wrote.length) log(`Demo: seeded ${wrote.join(", ")}.`);
|
|
157
|
-
if (skipped.length) log(`Demo: kept existing ${skipped.join(", ")}.`);
|
|
158
|
-
if (paths.length) {
|
|
159
|
-
log(`To remove the starter files later: rm -r ${paths.join(" ")}`);
|
|
160
|
-
log("Then: npm install → npm test → npm run update-all-format → push.");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
213
|
return { scaffolded: paths.length > 0, reason: null, wrote, skipped, paths };
|
|
164
214
|
}
|
package/src/package-json.js
CHANGED
|
@@ -38,14 +38,33 @@ export function detectIndent(text) {
|
|
|
38
38
|
* added: { scripts: string[], devDependencies: string[], fields: string[] },
|
|
39
39
|
* skipped: { scripts: string[], devDependencies: string[], fields: string[] }
|
|
40
40
|
* }} new file text, plus keys added vs. skipped (already present)
|
|
41
|
+
*
|
|
42
|
+
* `replaceDefaults` names placeholder values that `npm init -y` inserts (e.g.
|
|
43
|
+
* `scripts.test: 'echo "Error: no test specified" && exit 1'`, `main: "index.js"`).
|
|
44
|
+
* A key whose current value exactly equals its placeholder is treated as unset,
|
|
45
|
+
* so a canonical value can replace it instead of being skipped as "present".
|
|
41
46
|
*/
|
|
42
47
|
export function applyToPackageJson(
|
|
43
48
|
rawText,
|
|
44
|
-
{
|
|
49
|
+
{
|
|
50
|
+
scripts = {},
|
|
51
|
+
devDependencies = {},
|
|
52
|
+
fields = {},
|
|
53
|
+
replaceDefaults = {},
|
|
54
|
+
} = {},
|
|
45
55
|
) {
|
|
46
56
|
const pkg = JSON.parse(rawText);
|
|
47
57
|
const indent = detectIndent(rawText);
|
|
48
58
|
|
|
59
|
+
const stripDefaults = (obj, defaults = {}) => {
|
|
60
|
+
if (!obj) return;
|
|
61
|
+
for (const [key, placeholder] of Object.entries(defaults)) {
|
|
62
|
+
if (obj[key] === placeholder) delete obj[key];
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
stripDefaults(pkg, replaceDefaults.fields);
|
|
66
|
+
stripDefaults(pkg.scripts, replaceDefaults.scripts);
|
|
67
|
+
|
|
49
68
|
const s = mergeAbsent(pkg.scripts, scripts);
|
|
50
69
|
const d = mergeAbsent(pkg.devDependencies, devDependencies);
|
|
51
70
|
const f = mergeAbsent(pkg, fields);
|