@kubb/cli 5.11.4 → 5.11.6
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/dist/{Telemetry-Be1nltUh.cjs → Telemetry-Be3hIy6_.cjs} +2 -42
- package/dist/Telemetry-Be3hIy6_.cjs.map +1 -0
- package/dist/{Telemetry-CwaVQsvD.js → Telemetry-CfJAUDnh.js} +3 -31
- package/dist/Telemetry-CfJAUDnh.js.map +1 -0
- package/dist/{background-CQPyCSn0.js → background-BBpH2jmk.js} +7 -5
- package/dist/background-BBpH2jmk.js.map +1 -0
- package/dist/{background-mKdZ5HRx.cjs → background-BW-e9FCX.cjs} +5 -4
- package/dist/background-BW-e9FCX.cjs.map +1 -0
- package/dist/{background-aTbwrgui.js → background-DS2zOde-.js} +1 -1
- package/dist/config-Bh00Hwpw.js +132 -0
- package/dist/config-Bh00Hwpw.js.map +1 -0
- package/dist/config-D63Gum4F.cjs +169 -0
- package/dist/config-D63Gum4F.cjs.map +1 -0
- package/dist/config-iVdOcpro.js +1 -0
- package/dist/credentials-CZK6zPLv.js.map +1 -1
- package/dist/credentials-DDaKqtJn.cjs.map +1 -1
- package/dist/errors-Cl69p_vI.cjs +43 -0
- package/dist/errors-Cl69p_vI.cjs.map +1 -0
- package/dist/errors-DmC83ALX.js +32 -0
- package/dist/errors-DmC83ALX.js.map +1 -0
- package/dist/index.cjs +872 -307
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +863 -298
- package/dist/index.js.map +1 -1
- package/dist/registry-BlKLO4ft.cjs +272 -0
- package/dist/registry-BlKLO4ft.cjs.map +1 -0
- package/dist/registry-CgQYxUju.js +200 -0
- package/dist/registry-CgQYxUju.js.map +1 -0
- package/dist/reporters-Dl8lrJZG.js +817 -0
- package/dist/reporters-Dl8lrJZG.js.map +1 -0
- package/dist/reporters-TkCgd4dS.cjs +829 -0
- package/dist/reporters-TkCgd4dS.cjs.map +1 -0
- package/dist/{run-YHth5Afz.js → run-BJn0RSW2.js} +170 -76
- package/dist/run-BJn0RSW2.js.map +1 -0
- package/dist/{run-BWRmQxtE.cjs → run-BszyZwY3.cjs} +176 -82
- package/dist/run-BszyZwY3.cjs.map +1 -0
- package/dist/{run-Bczs5vbI.cjs → run-BtVKUjdK.cjs} +4 -3
- package/dist/{run-Bczs5vbI.cjs.map → run-BtVKUjdK.cjs.map} +1 -1
- package/dist/run-CFf0yxyY.cjs +94 -0
- package/dist/run-CFf0yxyY.cjs.map +1 -0
- package/dist/run-D_dD7xHp.js +93 -0
- package/dist/run-D_dD7xHp.js.map +1 -0
- package/dist/run-DsJJCzQD.js +1 -0
- package/dist/{run-B6VxJ_VM.js → run-KuzAqsjg.js} +3 -2
- package/dist/{run-B6VxJ_VM.js.map → run-KuzAqsjg.js.map} +1 -1
- package/dist/run-TrjCNL8e.cjs +227 -0
- package/dist/run-TrjCNL8e.cjs.map +1 -0
- package/dist/run-wkN1irUo.js +224 -0
- package/dist/run-wkN1irUo.js.map +1 -0
- package/dist/{snapshot-Cilp10vE.js → snapshot-CdGwHH15.js} +49 -62
- package/dist/snapshot-CdGwHH15.js.map +1 -0
- package/dist/{snapshot-D_rXnnMH.cjs → snapshot-u4L8mh4Y.cjs} +51 -64
- package/dist/snapshot-u4L8mh4Y.cjs.map +1 -0
- package/dist/status-DHPeR3bq.js +77 -0
- package/dist/status-DHPeR3bq.js.map +1 -0
- package/dist/status-dpSvxxtN.cjs +78 -0
- package/dist/status-dpSvxxtN.cjs.map +1 -0
- package/dist/studioWorker.js +2 -2
- package/dist/summary-6-rotl2L.cjs +46 -0
- package/dist/summary-6-rotl2L.cjs.map +1 -0
- package/dist/summary-DkSLti6f.js +41 -0
- package/dist/summary-DkSLti6f.js.map +1 -0
- package/dist/text-CMP4M19Z.js +239 -0
- package/dist/text-CMP4M19Z.js.map +1 -0
- package/dist/text-CknhKMvM.cjs +341 -0
- package/dist/text-CknhKMvM.cjs.map +1 -0
- package/dist/utils-CnjEJIdZ.js +612 -0
- package/dist/utils-CnjEJIdZ.js.map +1 -0
- package/dist/utils-DpKrXR2t.cjs +727 -0
- package/dist/utils-DpKrXR2t.cjs.map +1 -0
- package/package.json +7 -7
- package/dist/Telemetry-Be1nltUh.cjs.map +0 -1
- package/dist/Telemetry-CwaVQsvD.js.map +0 -1
- package/dist/background-CQPyCSn0.js.map +0 -1
- package/dist/background-mKdZ5HRx.cjs.map +0 -1
- package/dist/config-B56ZuVhi.js +0 -95
- package/dist/config-B56ZuVhi.js.map +0 -1
- package/dist/config-Cs4N79Fm.cjs +0 -113
- package/dist/config-Cs4N79Fm.cjs.map +0 -1
- package/dist/output-HgzJqkSF.cjs +0 -1076
- package/dist/output-HgzJqkSF.cjs.map +0 -1
- package/dist/output-PhkPtNP7.js +0 -937
- package/dist/output-PhkPtNP7.js.map +0 -1
- package/dist/registry-DYUhC7rH.js +0 -115
- package/dist/registry-DYUhC7rH.js.map +0 -1
- package/dist/registry-tk-v78fd.cjs +0 -157
- package/dist/registry-tk-v78fd.cjs.map +0 -1
- package/dist/run-AfjIBtiL.js +0 -39
- package/dist/run-AfjIBtiL.js.map +0 -1
- package/dist/run-BWRmQxtE.cjs.map +0 -1
- package/dist/run-B_Gr9TJ4.cjs +0 -345
- package/dist/run-B_Gr9TJ4.cjs.map +0 -1
- package/dist/run-CwrI0wRy.js +0 -1
- package/dist/run-DDoJGjWT.cjs +0 -40
- package/dist/run-DDoJGjWT.cjs.map +0 -1
- package/dist/run-SYKj8-Q5.js +0 -343
- package/dist/run-SYKj8-Q5.js.map +0 -1
- package/dist/run-YHth5Afz.js.map +0 -1
- package/dist/snapshot-Cilp10vE.js.map +0 -1
- package/dist/snapshot-D_rXnnMH.cjs.map +0 -1
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-CPE0t6oh.cjs");
|
|
2
|
+
const require_Telemetry = require("./Telemetry-Be3hIy6_.cjs");
|
|
3
|
+
const require_text = require("./text-CknhKMvM.cjs");
|
|
4
|
+
let node_util = require("node:util");
|
|
5
|
+
let node_process = require("node:process");
|
|
6
|
+
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
7
|
+
let node_fs_promises = require("node:fs/promises");
|
|
8
|
+
let node_path = require("node:path");
|
|
9
|
+
require("node:fs");
|
|
10
|
+
//#region ../../internals/utils/src/fs.ts
|
|
11
|
+
/**
|
|
12
|
+
* Resolves to `true` when the file or directory at `path` exists.
|
|
13
|
+
* Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* if (await exists('./kubb.config.ts')) {
|
|
18
|
+
* const content = await read('./kubb.config.ts')
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
async function exists(path) {
|
|
23
|
+
if (require_Telemetry.runtime.isBun) return Bun.file(path).exists();
|
|
24
|
+
return (0, node_fs_promises.access)(path).then(() => true, () => false);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reads the file at `path` as a UTF-8 string.
|
|
28
|
+
* Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const source = await read('./src/Pet.ts')
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
async function read(path) {
|
|
36
|
+
if (require_Telemetry.runtime.isBun) return Bun.file(path).text();
|
|
37
|
+
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether `stored` already holds `source`, comparing on the trimmed text rather than the exact
|
|
41
|
+
* bytes. Surrounding whitespace is what a formatter adds and what editors strip, and neither is a
|
|
42
|
+
* reason to rewrite the file.
|
|
43
|
+
*
|
|
44
|
+
* Both sides are trimmed, so a storage that keeps bytes verbatim settles on the same answer as one
|
|
45
|
+
* that normalizes what it stores. Trimming only `stored` would leave a source with leading
|
|
46
|
+
* whitespace rewritten on every build, since the stored copy keeps the whitespace the comparison
|
|
47
|
+
* has already dropped.
|
|
48
|
+
*/
|
|
49
|
+
function matchesStored({ stored, source }) {
|
|
50
|
+
return stored.trim() === source.trim();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Writes `data` to `path`, trimming surrounding whitespace and ending the file with a single newline
|
|
54
|
+
* the way prettier, biome, and oxfmt all do.
|
|
55
|
+
* Skips the write when the trimmed content is empty, or when the file already holds that content.
|
|
56
|
+
* Creates any missing parent directories automatically.
|
|
57
|
+
* When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* await write('./src/Pet.ts', source) // writes and returns the trimmed content plus a newline
|
|
62
|
+
* await write('./src/Pet.ts', source) // null — file unchanged
|
|
63
|
+
* await write('./src/Pet.ts', ' ') // null — empty content skipped
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
async function write(path, data, options = {}) {
|
|
67
|
+
const trimmed = data.trim();
|
|
68
|
+
if (trimmed === "") return null;
|
|
69
|
+
const content = `${trimmed}\n`;
|
|
70
|
+
const resolved = (0, node_path.resolve)(path);
|
|
71
|
+
let stored = options.stored;
|
|
72
|
+
if (stored === void 0) {
|
|
73
|
+
if (require_Telemetry.runtime.isBun) {
|
|
74
|
+
const file = Bun.file(resolved);
|
|
75
|
+
stored = await file.exists() ? await file.text() : null;
|
|
76
|
+
} else try {
|
|
77
|
+
stored = await (0, node_fs_promises.readFile)(resolved, { encoding: "utf-8" });
|
|
78
|
+
} catch {
|
|
79
|
+
stored = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (matchesStored({
|
|
83
|
+
stored: stored ?? "",
|
|
84
|
+
source: trimmed
|
|
85
|
+
})) return null;
|
|
86
|
+
await writeRaw(resolved, content);
|
|
87
|
+
if (options.sanity) {
|
|
88
|
+
const savedData = await read(resolved);
|
|
89
|
+
if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
|
|
90
|
+
return savedData;
|
|
91
|
+
}
|
|
92
|
+
return content;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
|
|
96
|
+
* Uses Bun.write under Bun and creates missing parent directories under either runtime.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
|
|
100
|
+
*/
|
|
101
|
+
async function writeRaw(path, content) {
|
|
102
|
+
const resolved = (0, node_path.resolve)(path);
|
|
103
|
+
if (require_Telemetry.runtime.isBun) {
|
|
104
|
+
await Bun.write(resolved, content);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== "ENOENT") throw error;
|
|
111
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(resolved), { recursive: true });
|
|
112
|
+
await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
|
|
117
|
+
* to absolute paths first, so relative and `..`-containing inputs compare correctly.
|
|
118
|
+
*
|
|
119
|
+
* Guards a destructive or an out-of-tree operation: before wiping an output directory, check that
|
|
120
|
+
* it does not contain the project root, and before loading a path a caller supplied, check that it
|
|
121
|
+
* did not escape the directory it is allowed to read from.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* isPathInside('./src/gen', '.') // true — nested inside the root
|
|
125
|
+
* isPathInside('.', '.') // true — the same directory counts as inside
|
|
126
|
+
* isPathInside('.', './src/gen') // false — the root is not inside its own output
|
|
127
|
+
* isPathInside('../other', '.') // false — escapes the root
|
|
128
|
+
*/
|
|
129
|
+
function isPathInside(path, parent) {
|
|
130
|
+
const resolvedPath = (0, node_path.resolve)(path);
|
|
131
|
+
const resolvedParent = (0, node_path.resolve)(parent);
|
|
132
|
+
if (resolvedPath === resolvedParent) return true;
|
|
133
|
+
const rel = (0, node_path.relative)(resolvedParent, resolvedPath);
|
|
134
|
+
return rel !== "" && !rel.startsWith("..") && !(0, node_path.isAbsolute)(rel);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region ../../internals/utils/src/promise.ts
|
|
138
|
+
/**
|
|
139
|
+
* Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
|
|
140
|
+
* takes the next item the moment it frees up instead of waiting for a batch to drain.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* await inParallel({ items: files, limit: 50, run: (file) => storage.writeItem(file.path, file.source) })
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
async function inParallel({ items, limit, run }) {
|
|
148
|
+
const queue = items.entries();
|
|
149
|
+
const worker = async () => {
|
|
150
|
+
for (const [index, item] of queue) await run(item, index);
|
|
151
|
+
};
|
|
152
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/loggers/banner.ts
|
|
156
|
+
/**
|
|
157
|
+
* xterm-256 color for each pixel letter of the mascot. Fixed codes rather than named colors, so a
|
|
158
|
+
* terminal theme cannot repaint it.
|
|
159
|
+
*/
|
|
160
|
+
const colors = {
|
|
161
|
+
/** Hoodie. */
|
|
162
|
+
O: 208,
|
|
163
|
+
/** Shadow where the hood meets the face. */
|
|
164
|
+
R: 166,
|
|
165
|
+
/** Face. */
|
|
166
|
+
F: 223,
|
|
167
|
+
/** Eyes. */
|
|
168
|
+
K: 16,
|
|
169
|
+
/** Blush. */
|
|
170
|
+
B: 217,
|
|
171
|
+
/** Mouth. */
|
|
172
|
+
M: 131
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* The two pixel rows of the face around the eyes, for each expression.
|
|
176
|
+
*/
|
|
177
|
+
const eyes = {
|
|
178
|
+
open: ["FFFFFFFFFF", "FFKFFFFKFF"],
|
|
179
|
+
left: ["FFFFFFFFFF", "FKFFFFKFFF"],
|
|
180
|
+
right: ["FFFFFFFFFF", "FFFKFFFFKF"],
|
|
181
|
+
blink: ["FFFFFFFFFF", "FKKFFFFKKF"],
|
|
182
|
+
happy: ["FFKFFFFKFF", "FKFKFFKFKF"]
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* The frames the mascot plays when the CLI starts, each with how long it stays on screen. It ends on
|
|
186
|
+
* open eyes, which stay.
|
|
187
|
+
*/
|
|
188
|
+
const wakeUp = [
|
|
189
|
+
["blink", 100],
|
|
190
|
+
["open", 180],
|
|
191
|
+
["left", 140],
|
|
192
|
+
["right", 140],
|
|
193
|
+
["open", 80],
|
|
194
|
+
["happy", 220],
|
|
195
|
+
["open", 0]
|
|
196
|
+
];
|
|
197
|
+
/**
|
|
198
|
+
* The kid in the orange hoodie from the Kubb logo, one letter per pixel and `.` for none. Each
|
|
199
|
+
* 2 × 2 block becomes one terminal cell, so a block holds at most two colors.
|
|
200
|
+
*/
|
|
201
|
+
function pixels(expression) {
|
|
202
|
+
const [top, bottom] = eyes[expression];
|
|
203
|
+
return [
|
|
204
|
+
"....OOOOOOOOOO....",
|
|
205
|
+
"..OORRRRRRRRRROO..",
|
|
206
|
+
`..OO${top}OO..`,
|
|
207
|
+
`OOFF${bottom}FFOO`,
|
|
208
|
+
"OOFFBBFFMMFFBBFFOO",
|
|
209
|
+
"..OOOOFFFFFFOOOO.."
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Quadrant glyphs indexed by which quarters are filled: top left 1, top right 2, bottom left 4,
|
|
214
|
+
* bottom right 8.
|
|
215
|
+
*/
|
|
216
|
+
const quadrants = " ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█";
|
|
217
|
+
/**
|
|
218
|
+
* Draws each 2 × 2 block of pixels as one quadrant glyph. The glyph's filled quarters take the first
|
|
219
|
+
* color in the block and the rest take the other.
|
|
220
|
+
*/
|
|
221
|
+
function drawHead(expression) {
|
|
222
|
+
const rows = pixels(expression);
|
|
223
|
+
const lines = [];
|
|
224
|
+
for (let y = 0; y < rows.length; y += 2) {
|
|
225
|
+
let line = "";
|
|
226
|
+
for (let x = 0; x < rows[y].length; x += 2) {
|
|
227
|
+
const block = [
|
|
228
|
+
rows[y][x],
|
|
229
|
+
rows[y][x + 1],
|
|
230
|
+
rows[y + 1][x],
|
|
231
|
+
rows[y + 1][x + 1]
|
|
232
|
+
];
|
|
233
|
+
const fg = block.find((pixel) => pixel in colors);
|
|
234
|
+
if (!fg) {
|
|
235
|
+
line += " ";
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const bg = colors[block.find((pixel) => pixel !== fg) ?? "."];
|
|
239
|
+
const mask = block.reduce((bits, pixel, index) => pixel === fg ? bits | 1 << index : bits, 0);
|
|
240
|
+
line += `\x1b[38;5;${colors[fg]}${bg === void 0 ? "" : `;48;5;${bg}`}m${quadrants[mask]}\x1b[0m`;
|
|
241
|
+
}
|
|
242
|
+
lines.push(line);
|
|
243
|
+
}
|
|
244
|
+
return lines;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Generates the Kubb mascot welcome banner as an ANSI-colored string: the head with the version,
|
|
248
|
+
* title, and description beside it.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* console.log(getIntro({ title: 'kubb.config.ts', description: 'generating…', version: '5.0.0', expression: 'open' }))
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
function getIntro({ title, description, version, expression }) {
|
|
256
|
+
const brand = (value, bold = false) => `\x1b[${bold ? "1;" : ""}38;5;${colors.O}m${value}\x1b[0m`;
|
|
257
|
+
const text = [
|
|
258
|
+
brand(`KUBB v${version}`, true),
|
|
259
|
+
(0, node_util.styleText)("gray", title),
|
|
260
|
+
`${brand("➜")} ${(0, node_util.styleText)("white", description)}`
|
|
261
|
+
];
|
|
262
|
+
return drawHead(expression).map((line, index) => ` ${line} ${text[index]}`).join("\n");
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Formats a millisecond duration with a threshold-based ANSI color.
|
|
266
|
+
* `≤ 500 ms` → green · `≤ 1 000 ms` → yellow · `> 1 000 ms` → red.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* formatMsWithColor(200) // '\x1b[32m200ms\x1b[39m'
|
|
271
|
+
* formatMsWithColor(800) // '\x1b[33m800ms\x1b[39m'
|
|
272
|
+
* formatMsWithColor(1500) // '\x1b[31m1.50s\x1b[39m'
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
function formatMsWithColor(ms) {
|
|
276
|
+
const formatted = require_Telemetry.formatMs(ms);
|
|
277
|
+
if (ms <= 500) return (0, node_util.styleText)("green", formatted);
|
|
278
|
+
if (ms <= 1e3) return (0, node_util.styleText)("yellow", formatted);
|
|
279
|
+
return (0, node_util.styleText)("red", formatted);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* A `size`-cell progress bar: `done` of `total` as filled cells, the rest light. Any progress shows
|
|
283
|
+
* at least one cell, and only a finished bar fills the last one.
|
|
284
|
+
*/
|
|
285
|
+
function formatBar(done, total, size = 20) {
|
|
286
|
+
const filled = !done ? 0 : done >= total ? size : Math.min(size - 1, Math.max(1, Math.round(done / total * size)));
|
|
287
|
+
return (0, node_util.styleText)("magenta", "█".repeat(filled)) + (0, node_util.styleText)("dim", "░".repeat(size - filled));
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/ui/spinner.ts
|
|
291
|
+
/**
|
|
292
|
+
* A progress spinner, or a writer that prints each message it is given when the terminal cannot
|
|
293
|
+
* animate one. Callers drive both the same way, including the failure state.
|
|
294
|
+
*/
|
|
295
|
+
function createSpinner() {
|
|
296
|
+
return require_text.writer().spinner();
|
|
297
|
+
}
|
|
298
|
+
/** How long a wait may run before a spinner shows, so a fast answer draws nothing. */
|
|
299
|
+
const SPINNER_DELAY_MS = 150;
|
|
300
|
+
/**
|
|
301
|
+
* Awaits `promise`, showing a spinner with `message` only when it takes longer than 150 ms.
|
|
302
|
+
*/
|
|
303
|
+
async function withSpinner({ promise, message }) {
|
|
304
|
+
let spinner;
|
|
305
|
+
const timer = setTimeout(() => {
|
|
306
|
+
spinner = createSpinner();
|
|
307
|
+
spinner.start(message);
|
|
308
|
+
}, SPINNER_DELAY_MS);
|
|
309
|
+
try {
|
|
310
|
+
return await promise;
|
|
311
|
+
} finally {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
spinner?.clear();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/runners/generate/utils.ts
|
|
318
|
+
/** Glob pattern for paths the file watcher ignores. */
|
|
319
|
+
const WATCHER_IGNORED_PATHS = "**/{.git,node_modules}/**";
|
|
320
|
+
/**
|
|
321
|
+
* Quiet window in milliseconds that collapses a burst of watcher events (an editor save emits several) into one rebuild.
|
|
322
|
+
* It outlasts chokidar's 100 ms atomic-save window, which holds back the event of a write-then-rename save.
|
|
323
|
+
*/
|
|
324
|
+
const WATCHER_DEBOUNCE_MS = 250;
|
|
325
|
+
/** Interval in milliseconds between polls of a remote `input` URL in watch mode, which emits no filesystem events. */
|
|
326
|
+
const URL_WATCHER_INTERVAL_MS = 2e3;
|
|
327
|
+
/** Upper bound in milliseconds for one URL watcher request, headers and body read, so a hung server never stalls polling. */
|
|
328
|
+
const URL_WATCHER_TIMEOUT_MS = 1e4;
|
|
329
|
+
/** `14:02:11 petStore.yaml changed`, the line a watcher prints when its input changes. */
|
|
330
|
+
function formatChange({ target, removed = false, now = /* @__PURE__ */ new Date() }) {
|
|
331
|
+
return `${(0, node_util.styleText)("dim", now.toTimeString().slice(0, 8))} ${(0, node_util.styleText)("bold", target)} ${removed ? "removed" : "changed"}`;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Starts a file watcher on the given paths and calls `cb` on any change.
|
|
335
|
+
* Ignores `.git` and `node_modules` directories. Event bursts (an editor save emits several)
|
|
336
|
+
* are debounced into one call. `cb` must never reject: its result is not awaited, so the caller
|
|
337
|
+
* serializes and reports its own builds. Resolves to a function that stops watching.
|
|
338
|
+
*/
|
|
339
|
+
async function startWatcher(path, cb, log = {
|
|
340
|
+
info: console.log,
|
|
341
|
+
error: console.log
|
|
342
|
+
}) {
|
|
343
|
+
const { watch } = await import("chokidar");
|
|
344
|
+
const watcher = watch(path, {
|
|
345
|
+
ignorePermissionErrors: true,
|
|
346
|
+
ignored: WATCHER_IGNORED_PATHS,
|
|
347
|
+
ignoreInitial: true
|
|
348
|
+
});
|
|
349
|
+
for (const signal of ["SIGINT", "SIGTERM"]) node_process.default.once(signal, () => void watcher.close());
|
|
350
|
+
let debounceTimer = null;
|
|
351
|
+
watcher.on("all", (type, file) => {
|
|
352
|
+
log.info(formatChange({
|
|
353
|
+
target: (0, node_path.relative)(node_process.default.cwd(), file),
|
|
354
|
+
removed: type === "unlink"
|
|
355
|
+
}));
|
|
356
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
357
|
+
debounceTimer = setTimeout(() => {
|
|
358
|
+
debounceTimer = null;
|
|
359
|
+
cb(path);
|
|
360
|
+
}, WATCHER_DEBOUNCE_MS);
|
|
361
|
+
});
|
|
362
|
+
return async () => {
|
|
363
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
364
|
+
await watcher.close();
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Fetches the body of a remote spec URL, `undefined` when the server does not answer with a
|
|
369
|
+
* readable 2xx body before `signal` aborts. The caller seeds `startUrlWatcher` with the result, so
|
|
370
|
+
* the watcher compares polls against the content the initial build ran on.
|
|
371
|
+
*/
|
|
372
|
+
async function fetchUrlBody(url, signal = AbortSignal.timeout(URL_WATCHER_TIMEOUT_MS)) {
|
|
373
|
+
try {
|
|
374
|
+
const response = await fetch(url, { signal });
|
|
375
|
+
if (!response.ok) return void 0;
|
|
376
|
+
return await response.text();
|
|
377
|
+
} catch {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Polls a remote spec URL and calls `cb` whenever the response body changes. A remote document
|
|
383
|
+
* emits no filesystem events, so this is the URL counterpart of `startWatcher`.
|
|
384
|
+
*
|
|
385
|
+
* `initialBody` seeds the change detection, so an edit landing between the initial build and the
|
|
386
|
+
* first poll still rebuilds. An unreachable server (the API restarting between saves) is reported
|
|
387
|
+
* once per outage and polling continues; after it recovers, a rebuild happens only when the body
|
|
388
|
+
* actually differs from the last one seen, so a plain restart with an unchanged spec stays quiet.
|
|
389
|
+
* Each request is aborted after `timeoutMs`, so a hung response delays at most one poll.
|
|
390
|
+
* `cb` must never reject: its result is not awaited.
|
|
391
|
+
*
|
|
392
|
+
* Returns a function that stops polling and aborts the in-flight request, so callers (and tests)
|
|
393
|
+
* can shut the watcher down without signaling the process.
|
|
394
|
+
*/
|
|
395
|
+
function startUrlWatcher(url, cb, options = {}) {
|
|
396
|
+
const { log = {
|
|
397
|
+
info: console.log,
|
|
398
|
+
error: console.log
|
|
399
|
+
}, intervalMs = URL_WATCHER_INTERVAL_MS, timeoutMs = URL_WATCHER_TIMEOUT_MS, initialBody } = options;
|
|
400
|
+
let lastBody = initialBody;
|
|
401
|
+
let offline = false;
|
|
402
|
+
let timer = null;
|
|
403
|
+
let controller = null;
|
|
404
|
+
let stopped = false;
|
|
405
|
+
const stop = () => {
|
|
406
|
+
stopped = true;
|
|
407
|
+
if (timer) clearTimeout(timer);
|
|
408
|
+
controller?.abort();
|
|
409
|
+
};
|
|
410
|
+
node_process.default.once("SIGINT", stop);
|
|
411
|
+
node_process.default.once("SIGTERM", stop);
|
|
412
|
+
const schedule = () => {
|
|
413
|
+
if (!stopped) timer = setTimeout(() => void poll(), intervalMs);
|
|
414
|
+
};
|
|
415
|
+
const poll = async () => {
|
|
416
|
+
controller = new AbortController();
|
|
417
|
+
const body = await fetchUrlBody(url, AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs)]));
|
|
418
|
+
if (body === void 0) {
|
|
419
|
+
if (!offline && !stopped) {
|
|
420
|
+
offline = true;
|
|
421
|
+
log.error((0, node_util.styleText)("red", `Cannot reach ${url}, polling until it responds again`));
|
|
422
|
+
}
|
|
423
|
+
schedule();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (offline) {
|
|
427
|
+
offline = false;
|
|
428
|
+
log.info((0, node_util.styleText)("yellow", `${url} is reachable again`));
|
|
429
|
+
}
|
|
430
|
+
const changed = lastBody !== void 0 && body !== lastBody;
|
|
431
|
+
if (changed) log.info(formatChange({ target: url }));
|
|
432
|
+
if (changed || lastBody === void 0) cb([url]);
|
|
433
|
+
lastBody = body;
|
|
434
|
+
schedule();
|
|
435
|
+
};
|
|
436
|
+
poll();
|
|
437
|
+
return stop;
|
|
438
|
+
}
|
|
439
|
+
function createSnapshot() {
|
|
440
|
+
return {
|
|
441
|
+
schemas: /* @__PURE__ */ new Set(),
|
|
442
|
+
operations: /* @__PURE__ */ new Set()
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Collects a {@link BuildSnapshot} from the hooks while a build runs. `take` hands back the finished
|
|
447
|
+
* build's snapshot and starts the next one.
|
|
448
|
+
*/
|
|
449
|
+
function trackBuild(hooks) {
|
|
450
|
+
let current = createSnapshot();
|
|
451
|
+
hooks.hook("kubb:generate:schema", (node) => {
|
|
452
|
+
if (node.name) current.schemas.add(node.name);
|
|
453
|
+
});
|
|
454
|
+
hooks.hook("kubb:generate:operation", (node) => {
|
|
455
|
+
current.operations.add(node.operationId);
|
|
456
|
+
});
|
|
457
|
+
return { take() {
|
|
458
|
+
const snapshot = current;
|
|
459
|
+
current = createSnapshot();
|
|
460
|
+
return snapshot;
|
|
461
|
+
} };
|
|
462
|
+
}
|
|
463
|
+
function diffNames(previous, next) {
|
|
464
|
+
return {
|
|
465
|
+
added: [...next].filter((name) => !previous.has(name)).length,
|
|
466
|
+
removed: [...previous].filter((name) => !next.has(name)).length
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Compares two {@link BuildSnapshot}s by name.
|
|
471
|
+
*/
|
|
472
|
+
function diffBuilds(previous, next) {
|
|
473
|
+
return {
|
|
474
|
+
schemas: diffNames(previous.schemas, next.schemas),
|
|
475
|
+
operations: diffNames(previous.operations, next.operations)
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Compares file contents or fingerprints by path.
|
|
480
|
+
*/
|
|
481
|
+
function diffFiles(previous, next) {
|
|
482
|
+
const files = [];
|
|
483
|
+
let unchanged = 0;
|
|
484
|
+
for (const [path, fingerprint] of next) {
|
|
485
|
+
const before = previous.get(path);
|
|
486
|
+
if (before === fingerprint) unchanged++;
|
|
487
|
+
else files.push({
|
|
488
|
+
status: before === void 0 ? "A" : "M",
|
|
489
|
+
path
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
for (const path of previous.keys()) if (!next.has(path)) files.push({
|
|
493
|
+
status: "D",
|
|
494
|
+
path
|
|
495
|
+
});
|
|
496
|
+
return {
|
|
497
|
+
files,
|
|
498
|
+
unchanged
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function countLineChanges(before, after) {
|
|
502
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
503
|
+
for (const line of before ? before.split("\n") : []) remaining.set(line, (remaining.get(line) ?? 0) + 1);
|
|
504
|
+
let added = 0;
|
|
505
|
+
for (const line of after ? after.split("\n") : []) {
|
|
506
|
+
const count = remaining.get(line) ?? 0;
|
|
507
|
+
if (count > 0) remaining.set(line, count - 1);
|
|
508
|
+
else added++;
|
|
509
|
+
}
|
|
510
|
+
const removed = [...remaining.values()].reduce((sum, count) => sum + count, 0);
|
|
511
|
+
return {
|
|
512
|
+
added,
|
|
513
|
+
removed
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Reads the output before generation, so cleaning the directory cannot erase the diff baseline.
|
|
518
|
+
* A missing directory or an unreadable file is skipped.
|
|
519
|
+
*/
|
|
520
|
+
async function readOutputFiles(outputPath) {
|
|
521
|
+
const entries = await (0, node_fs_promises.readdir)(outputPath, {
|
|
522
|
+
recursive: true,
|
|
523
|
+
withFileTypes: true
|
|
524
|
+
}).catch(() => []);
|
|
525
|
+
const files = /* @__PURE__ */ new Map();
|
|
526
|
+
await inParallel({
|
|
527
|
+
items: entries.filter((entry) => entry.isFile()).map((entry) => (0, node_path.join)(entry.parentPath, entry.name)),
|
|
528
|
+
limit: 50,
|
|
529
|
+
run: async (filePath) => {
|
|
530
|
+
const source = await read(filePath).catch(() => null);
|
|
531
|
+
if (source !== null) files.set(filePath, source);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
return files;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Compares generated output with the files captured before generation. The manifest recognizes
|
|
538
|
+
* output rewritten by a formatter, and `clean` includes files the run would delete.
|
|
539
|
+
*/
|
|
540
|
+
async function diffOutput({ storage, paths, clean, manifest, previousFiles }) {
|
|
541
|
+
const generated = new Set(paths);
|
|
542
|
+
const changes = [];
|
|
543
|
+
let unchanged = 0;
|
|
544
|
+
await inParallel({
|
|
545
|
+
items: paths,
|
|
546
|
+
limit: 50,
|
|
547
|
+
run: async (filePath, index) => {
|
|
548
|
+
const next = await storage.readItem(filePath);
|
|
549
|
+
if (next === null) return;
|
|
550
|
+
const previous = previousFiles.get(filePath) ?? null;
|
|
551
|
+
if (previous !== null && (matchesStored({
|
|
552
|
+
stored: previous,
|
|
553
|
+
source: next
|
|
554
|
+
}) || manifest?.isUpToDate({
|
|
555
|
+
key: filePath,
|
|
556
|
+
source: next,
|
|
557
|
+
disk: previous
|
|
558
|
+
}))) {
|
|
559
|
+
unchanged++;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
changes[index] = {
|
|
563
|
+
status: previous === null ? "A" : "M",
|
|
564
|
+
path: filePath,
|
|
565
|
+
...countLineChanges(previous?.trim() ?? "", previous === null ? next.trimEnd() : next.trim())
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
const files = changes.filter((file) => file !== void 0);
|
|
570
|
+
if (clean) {
|
|
571
|
+
for (const [filePath, source] of previousFiles) if (!generated.has(filePath)) files.push({
|
|
572
|
+
status: "D",
|
|
573
|
+
path: filePath,
|
|
574
|
+
...countLineChanges(source.trimEnd(), "")
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
files,
|
|
579
|
+
unchanged
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Reads `r` (rebuild) and `q` (quit) from an interactive stdin in raw mode, and keeps ctrl+c
|
|
584
|
+
* working. Returns `undefined` when stdin is not a TTY. Otherwise returns `arm`, which turns raw
|
|
585
|
+
* mode back on: a spinner restores the terminal when it stops, so call it after each build. Raw mode
|
|
586
|
+
* is restored on exit.
|
|
587
|
+
*/
|
|
588
|
+
function listenForKeys({ rebuild, quit, input = node_process.default.stdin }) {
|
|
589
|
+
if (!input.isTTY) return void 0;
|
|
590
|
+
const arm = () => {
|
|
591
|
+
input.setRawMode(true);
|
|
592
|
+
input.resume();
|
|
593
|
+
};
|
|
594
|
+
input.setEncoding("utf8");
|
|
595
|
+
input.on("data", (data) => {
|
|
596
|
+
const key = data.trim();
|
|
597
|
+
if (key === "r") {
|
|
598
|
+
rebuild();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (key === "q" || data === "") {
|
|
602
|
+
input.setRawMode(false);
|
|
603
|
+
input.pause();
|
|
604
|
+
quit(key === "q" ? 0 : 130);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
node_process.default.once("exit", () => input.setRawMode(false));
|
|
608
|
+
arm();
|
|
609
|
+
return arm;
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
Object.defineProperty(exports, "createSpinner", {
|
|
613
|
+
enumerable: true,
|
|
614
|
+
get: function() {
|
|
615
|
+
return createSpinner;
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
Object.defineProperty(exports, "diffBuilds", {
|
|
619
|
+
enumerable: true,
|
|
620
|
+
get: function() {
|
|
621
|
+
return diffBuilds;
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
Object.defineProperty(exports, "diffFiles", {
|
|
625
|
+
enumerable: true,
|
|
626
|
+
get: function() {
|
|
627
|
+
return diffFiles;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
Object.defineProperty(exports, "diffOutput", {
|
|
631
|
+
enumerable: true,
|
|
632
|
+
get: function() {
|
|
633
|
+
return diffOutput;
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
Object.defineProperty(exports, "exists", {
|
|
637
|
+
enumerable: true,
|
|
638
|
+
get: function() {
|
|
639
|
+
return exists;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
Object.defineProperty(exports, "fetchUrlBody", {
|
|
643
|
+
enumerable: true,
|
|
644
|
+
get: function() {
|
|
645
|
+
return fetchUrlBody;
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
Object.defineProperty(exports, "formatBar", {
|
|
649
|
+
enumerable: true,
|
|
650
|
+
get: function() {
|
|
651
|
+
return formatBar;
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
Object.defineProperty(exports, "formatMsWithColor", {
|
|
655
|
+
enumerable: true,
|
|
656
|
+
get: function() {
|
|
657
|
+
return formatMsWithColor;
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
Object.defineProperty(exports, "getIntro", {
|
|
661
|
+
enumerable: true,
|
|
662
|
+
get: function() {
|
|
663
|
+
return getIntro;
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
Object.defineProperty(exports, "isPathInside", {
|
|
667
|
+
enumerable: true,
|
|
668
|
+
get: function() {
|
|
669
|
+
return isPathInside;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
Object.defineProperty(exports, "listenForKeys", {
|
|
673
|
+
enumerable: true,
|
|
674
|
+
get: function() {
|
|
675
|
+
return listenForKeys;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
Object.defineProperty(exports, "read", {
|
|
679
|
+
enumerable: true,
|
|
680
|
+
get: function() {
|
|
681
|
+
return read;
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
Object.defineProperty(exports, "readOutputFiles", {
|
|
685
|
+
enumerable: true,
|
|
686
|
+
get: function() {
|
|
687
|
+
return readOutputFiles;
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
Object.defineProperty(exports, "startUrlWatcher", {
|
|
691
|
+
enumerable: true,
|
|
692
|
+
get: function() {
|
|
693
|
+
return startUrlWatcher;
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
Object.defineProperty(exports, "startWatcher", {
|
|
697
|
+
enumerable: true,
|
|
698
|
+
get: function() {
|
|
699
|
+
return startWatcher;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
Object.defineProperty(exports, "trackBuild", {
|
|
703
|
+
enumerable: true,
|
|
704
|
+
get: function() {
|
|
705
|
+
return trackBuild;
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
Object.defineProperty(exports, "wakeUp", {
|
|
709
|
+
enumerable: true,
|
|
710
|
+
get: function() {
|
|
711
|
+
return wakeUp;
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
Object.defineProperty(exports, "withSpinner", {
|
|
715
|
+
enumerable: true,
|
|
716
|
+
get: function() {
|
|
717
|
+
return withSpinner;
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
Object.defineProperty(exports, "write", {
|
|
721
|
+
enumerable: true,
|
|
722
|
+
get: function() {
|
|
723
|
+
return write;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
//# sourceMappingURL=utils-DpKrXR2t.cjs.map
|