@dunx/create-app 3.0.3 → 3.0.4
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 +71 -17
- package/dist/banner.d.ts +14 -0
- package/dist/{chunk-x7s3axkt.js → chunk-5tn3ybzv.js} +62 -73
- package/dist/cli.js +745 -72
- package/dist/features.d.ts +2 -2
- package/dist/generate.d.ts +0 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/dist/keys.d.ts +48 -0
- package/dist/prompt.d.ts +43 -0
- package/dist/prompts.d.ts +53 -0
- package/dist/scaffold.d.ts +17 -0
- package/dist/style.d.ts +26 -0
- package/dist/tty.d.ts +58 -0
- package/dist/wizard.d.ts +35 -0
- package/package.json +1 -1
- package/templates/features/assets/assets.module.ts +1 -1
- package/templates/features/auth/auth.module.ts +1 -1
- package/templates/features/http/http.module.ts +1 -1
- package/templates/minimal/package.json +3 -2
- package/dist/stdin.d.ts +0 -14
package/dist/cli.js
CHANGED
|
@@ -3,49 +3,726 @@
|
|
|
3
3
|
import {
|
|
4
4
|
FEATURES,
|
|
5
5
|
ScaffoldError,
|
|
6
|
-
|
|
7
|
-
featureNames,
|
|
6
|
+
blockingEntries,
|
|
8
7
|
impliedBy,
|
|
8
|
+
isValidPackageName,
|
|
9
|
+
packageVersion,
|
|
10
|
+
resolveFeatures,
|
|
9
11
|
scaffold
|
|
10
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-5tn3ybzv.js";
|
|
11
13
|
|
|
12
14
|
// src/cli.ts
|
|
13
15
|
import { parseArgs } from "util";
|
|
14
16
|
import { relative } from "path";
|
|
15
17
|
|
|
16
|
-
// src/
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// src/banner.ts
|
|
19
|
+
var LOGO = [
|
|
20
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
21
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D",
|
|
22
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2554\u255D ",
|
|
23
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2557 ",
|
|
24
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u255D \u2588\u2588\u2557",
|
|
25
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
26
|
+
];
|
|
27
|
+
var RAMP = [
|
|
28
|
+
"#00e6b8",
|
|
29
|
+
"#00d4aa",
|
|
30
|
+
"#12bfa4",
|
|
31
|
+
"#1fa89c",
|
|
32
|
+
"#2b8f92",
|
|
33
|
+
"#357788"
|
|
34
|
+
];
|
|
35
|
+
var INDENT = " ";
|
|
36
|
+
var NEEDS = 2 + 35 + 2;
|
|
37
|
+
|
|
38
|
+
class Banner {
|
|
39
|
+
#style;
|
|
40
|
+
constructor(style) {
|
|
41
|
+
this.#style = style;
|
|
42
|
+
}
|
|
43
|
+
lines(width, version) {
|
|
44
|
+
const label = `create-app ${version}`;
|
|
45
|
+
if (width < NEEDS) {
|
|
46
|
+
return [
|
|
47
|
+
`${INDENT}${this.#style.bold(this.#style.accent("dunx"))} ${this.#style.muted(label)}`
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
...LOGO.map((row, at) => `${INDENT}${this.#style.hex(RAMP[at] ?? "#00d4aa", row)}`),
|
|
52
|
+
`${INDENT}${this.#style.muted(label)}`
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
Object.defineProperty(Banner, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "style: Style", typeOnly: "Style" }] });
|
|
57
|
+
|
|
58
|
+
// src/keys.ts
|
|
59
|
+
var KeyName = Object.freeze({
|
|
60
|
+
Up: "up",
|
|
61
|
+
Down: "down",
|
|
62
|
+
Left: "left",
|
|
63
|
+
Right: "right",
|
|
64
|
+
Home: "home",
|
|
65
|
+
End: "end",
|
|
66
|
+
Enter: "enter",
|
|
67
|
+
Space: "space",
|
|
68
|
+
Tab: "tab",
|
|
69
|
+
ShiftTab: "shiftTab",
|
|
70
|
+
Backspace: "backspace",
|
|
71
|
+
ClearLine: "clearLine",
|
|
72
|
+
Escape: "escape",
|
|
73
|
+
Interrupt: "interrupt",
|
|
74
|
+
Char: "char",
|
|
75
|
+
Unknown: "unknown"
|
|
76
|
+
});
|
|
77
|
+
var key = (name, char = "") => ({ name, char });
|
|
78
|
+
var CONTROLS = Object.freeze({
|
|
79
|
+
3: KeyName.Interrupt,
|
|
80
|
+
4: KeyName.Interrupt,
|
|
81
|
+
8: KeyName.Backspace,
|
|
82
|
+
9: KeyName.Tab,
|
|
83
|
+
10: KeyName.Enter,
|
|
84
|
+
13: KeyName.Enter,
|
|
85
|
+
21: KeyName.ClearLine,
|
|
86
|
+
32: KeyName.Space,
|
|
87
|
+
127: KeyName.Backspace
|
|
88
|
+
});
|
|
89
|
+
var FINALS = Object.freeze({
|
|
90
|
+
A: KeyName.Up,
|
|
91
|
+
B: KeyName.Down,
|
|
92
|
+
C: KeyName.Right,
|
|
93
|
+
D: KeyName.Left,
|
|
94
|
+
F: KeyName.End,
|
|
95
|
+
H: KeyName.Home,
|
|
96
|
+
Z: KeyName.ShiftTab
|
|
97
|
+
});
|
|
98
|
+
var TILDES = Object.freeze({
|
|
99
|
+
"1": KeyName.Home,
|
|
100
|
+
"4": KeyName.End,
|
|
101
|
+
"7": KeyName.Home,
|
|
102
|
+
"8": KeyName.End
|
|
103
|
+
});
|
|
104
|
+
var ESCAPE = 27;
|
|
105
|
+
var isParameter = (code) => code >= 32 && code <= 63;
|
|
106
|
+
|
|
107
|
+
class KeyDecoder {
|
|
108
|
+
#pending = "";
|
|
109
|
+
#decoder = new TextDecoder("utf-8");
|
|
110
|
+
push(bytes) {
|
|
111
|
+
this.#pending += this.#decoder.decode(bytes, { stream: true });
|
|
112
|
+
const keys = [];
|
|
113
|
+
while (this.#pending.length > 0) {
|
|
114
|
+
const next = this.#take();
|
|
115
|
+
if (next === undefined)
|
|
116
|
+
break;
|
|
117
|
+
keys.push(next);
|
|
118
|
+
}
|
|
119
|
+
return keys;
|
|
120
|
+
}
|
|
121
|
+
#take() {
|
|
122
|
+
const buffer = this.#pending;
|
|
123
|
+
const code = buffer.charCodeAt(0);
|
|
124
|
+
if (code === ESCAPE)
|
|
125
|
+
return this.#takeEscape(buffer);
|
|
126
|
+
const named = CONTROLS[code];
|
|
127
|
+
if (named !== undefined) {
|
|
128
|
+
this.#pending = buffer.slice(1);
|
|
129
|
+
return key(named);
|
|
130
|
+
}
|
|
131
|
+
if (code < 32) {
|
|
132
|
+
this.#pending = buffer.slice(1);
|
|
133
|
+
return key(KeyName.Unknown);
|
|
134
|
+
}
|
|
135
|
+
const size = code >= 55296 && code <= 56319 ? 2 : 1;
|
|
136
|
+
this.#pending = buffer.slice(size);
|
|
137
|
+
return key(KeyName.Char, buffer.slice(0, size));
|
|
138
|
+
}
|
|
139
|
+
#takeEscape(buffer) {
|
|
140
|
+
if (buffer.length === 1) {
|
|
141
|
+
this.#pending = "";
|
|
142
|
+
return key(KeyName.Escape);
|
|
143
|
+
}
|
|
144
|
+
const introducer = buffer[1];
|
|
145
|
+
if (introducer !== "[" && introducer !== "O") {
|
|
146
|
+
this.#pending = buffer.slice(2);
|
|
147
|
+
return key(KeyName.Unknown);
|
|
148
|
+
}
|
|
149
|
+
let at = 2;
|
|
150
|
+
while (at < buffer.length && isParameter(buffer.charCodeAt(at)))
|
|
151
|
+
at += 1;
|
|
152
|
+
if (at === buffer.length)
|
|
153
|
+
return;
|
|
154
|
+
const parameters = buffer.slice(2, at);
|
|
155
|
+
const final = buffer[at] ?? "";
|
|
156
|
+
this.#pending = buffer.slice(at + 1);
|
|
157
|
+
if (final === "~")
|
|
158
|
+
return key(TILDES[parameters] ?? KeyName.Unknown);
|
|
159
|
+
return key(FINALS[final] ?? KeyName.Unknown);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/prompt.ts
|
|
164
|
+
class CancelledError extends Error {
|
|
165
|
+
name = "CancelledError";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class Prompt {
|
|
169
|
+
#done = false;
|
|
170
|
+
#cancelled = false;
|
|
171
|
+
#answer;
|
|
172
|
+
press(key2) {
|
|
173
|
+
if (this.#done)
|
|
174
|
+
return;
|
|
175
|
+
if (key2.name === KeyName.Interrupt || key2.name === KeyName.Escape) {
|
|
176
|
+
this.#cancelled = true;
|
|
177
|
+
this.#done = true;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.handle(key2);
|
|
181
|
+
}
|
|
182
|
+
finish(answer) {
|
|
183
|
+
this.#answer = answer;
|
|
184
|
+
this.#done = true;
|
|
185
|
+
}
|
|
186
|
+
get done() {
|
|
187
|
+
return this.#done;
|
|
188
|
+
}
|
|
189
|
+
get cancelled() {
|
|
190
|
+
return this.#cancelled;
|
|
191
|
+
}
|
|
192
|
+
get answer() {
|
|
193
|
+
if (!this.#done || this.#cancelled) {
|
|
194
|
+
throw new Error("The prompt was not answered.");
|
|
195
|
+
}
|
|
196
|
+
return this.#answer;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
var CSI = "\x1B[";
|
|
200
|
+
var HIDE_CURSOR = `${CSI}?25l`;
|
|
201
|
+
var SHOW_CURSOR = `${CSI}?25h`;
|
|
202
|
+
var ERASE_DOWN = `${CSI}0J`;
|
|
203
|
+
var up = (lines) => lines > 0 ? `${CSI}${lines}A` : "";
|
|
204
|
+
|
|
205
|
+
class PromptRunner {
|
|
206
|
+
#tty;
|
|
207
|
+
constructor(tty) {
|
|
208
|
+
this.#tty = tty;
|
|
209
|
+
}
|
|
210
|
+
async ask(prompt) {
|
|
211
|
+
const width = Math.max(20, this.#tty.columns - 1);
|
|
212
|
+
let painted = 0;
|
|
213
|
+
const paint = () => {
|
|
214
|
+
const height = Math.max(3, this.#tty.rows - 1);
|
|
215
|
+
const lines = prompt.frame(width, height).slice(0, height);
|
|
216
|
+
this.#tty.write(`${up(painted)}${ERASE_DOWN}${lines.join(`
|
|
217
|
+
`)}
|
|
218
|
+
`);
|
|
219
|
+
painted = lines.length;
|
|
220
|
+
};
|
|
221
|
+
this.#tty.write(HIDE_CURSOR);
|
|
222
|
+
try {
|
|
223
|
+
await new Promise((resolve) => {
|
|
224
|
+
paint();
|
|
225
|
+
this.#tty.open((key2) => {
|
|
226
|
+
prompt.press(key2);
|
|
227
|
+
if (prompt.done)
|
|
228
|
+
resolve();
|
|
229
|
+
else
|
|
230
|
+
paint();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
} finally {
|
|
234
|
+
this.#tty.close();
|
|
235
|
+
const closing = prompt.cancelled ? "" : `${prompt.summary(width)}
|
|
236
|
+
`;
|
|
237
|
+
this.#tty.write(`${up(painted)}${ERASE_DOWN}${closing}${SHOW_CURSOR}`);
|
|
238
|
+
}
|
|
239
|
+
if (prompt.cancelled)
|
|
240
|
+
throw new CancelledError("Cancelled.");
|
|
241
|
+
return prompt.answer;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
Object.defineProperty(PromptRunner, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "tty: Tty", typeOnly: "Tty" }] });
|
|
245
|
+
|
|
246
|
+
// src/style.ts
|
|
247
|
+
var CSI2 = "\x1B[";
|
|
248
|
+
var RESET = `${CSI2}0m`;
|
|
249
|
+
var tone = (hex) => Bun.color(hex, "ansi-256") ?? "";
|
|
250
|
+
var ACCENT = tone("#00d4aa");
|
|
251
|
+
var MUTED = tone("#8a8f98");
|
|
252
|
+
var DANGER = tone("#e5534b");
|
|
253
|
+
var WARN = tone("#d29922");
|
|
254
|
+
|
|
255
|
+
class Style {
|
|
256
|
+
#enabled;
|
|
257
|
+
constructor(enabled = Bun.enableANSIColors) {
|
|
258
|
+
this.#enabled = enabled;
|
|
259
|
+
}
|
|
260
|
+
#wrap(code, text) {
|
|
261
|
+
return this.#enabled && code !== "" ? `${code}${text}${RESET}` : text;
|
|
262
|
+
}
|
|
263
|
+
accent(text) {
|
|
264
|
+
return this.#wrap(ACCENT, text);
|
|
265
|
+
}
|
|
266
|
+
muted(text) {
|
|
267
|
+
return this.#wrap(MUTED, text);
|
|
268
|
+
}
|
|
269
|
+
danger(text) {
|
|
270
|
+
return this.#wrap(DANGER, text);
|
|
271
|
+
}
|
|
272
|
+
warn(text) {
|
|
273
|
+
return this.#wrap(WARN, text);
|
|
274
|
+
}
|
|
275
|
+
bold(text) {
|
|
276
|
+
return this.#wrap(`${CSI2}1m`, text);
|
|
277
|
+
}
|
|
278
|
+
invert(text) {
|
|
279
|
+
return this.#wrap(`${CSI2}7m`, text);
|
|
280
|
+
}
|
|
281
|
+
hex(colour, text) {
|
|
282
|
+
return this.#wrap(tone(colour), text);
|
|
283
|
+
}
|
|
284
|
+
clip(text, width) {
|
|
285
|
+
if (width <= 0)
|
|
286
|
+
return "";
|
|
287
|
+
if (Bun.stringWidth(text) <= width)
|
|
288
|
+
return text;
|
|
289
|
+
return `${Bun.sliceAnsi(text, 0, Math.max(0, width - 1))}${this.muted("\u2026")}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
Object.defineProperty(Style, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "enabled: boolean = Bun.enableANSIColors", optional: true }] });
|
|
293
|
+
|
|
294
|
+
// src/tty.ts
|
|
295
|
+
class Tty {
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class ProcessTty extends Tty {
|
|
299
|
+
#decoder = new KeyDecoder;
|
|
300
|
+
#input;
|
|
301
|
+
#output;
|
|
302
|
+
#listener;
|
|
303
|
+
constructor(input = process.stdin, output = process.stdout) {
|
|
304
|
+
super();
|
|
305
|
+
this.#input = input;
|
|
306
|
+
this.#output = output;
|
|
307
|
+
}
|
|
308
|
+
static available() {
|
|
309
|
+
return process.stdin.isTTY === true && typeof process.stdin.setRawMode === "function" && process.stdout.isTTY === true;
|
|
310
|
+
}
|
|
311
|
+
get columns() {
|
|
312
|
+
return this.#output.columns ?? 80;
|
|
313
|
+
}
|
|
314
|
+
get rows() {
|
|
315
|
+
return this.#output.rows ?? 24;
|
|
316
|
+
}
|
|
317
|
+
write(text) {
|
|
318
|
+
this.#output.write(text);
|
|
319
|
+
}
|
|
320
|
+
open(onKey) {
|
|
321
|
+
const listener = (chunk) => {
|
|
322
|
+
for (const key2 of this.#decoder.push(chunk))
|
|
323
|
+
onKey(key2);
|
|
324
|
+
};
|
|
325
|
+
this.#listener = listener;
|
|
326
|
+
this.#input.setRawMode?.(true);
|
|
327
|
+
this.#input.resume();
|
|
328
|
+
this.#input.on("data", listener);
|
|
329
|
+
}
|
|
330
|
+
close() {
|
|
331
|
+
if (this.#listener === undefined)
|
|
332
|
+
return;
|
|
333
|
+
this.#input.off("data", this.#listener);
|
|
334
|
+
this.#listener = undefined;
|
|
335
|
+
this.#input.setRawMode?.(false);
|
|
336
|
+
this.#input.pause();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
Object.defineProperty(ProcessTty, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "input: KeySource = process.stdin", optional: true }, { unresolved: "output: FrameSink = process.stdout", optional: true }] });
|
|
340
|
+
|
|
341
|
+
// src/wizard.ts
|
|
342
|
+
import { basename, resolve } from "path";
|
|
343
|
+
|
|
344
|
+
// src/prompts.ts
|
|
345
|
+
var ASKING = "?";
|
|
346
|
+
var ANSWERED = "\u2713";
|
|
347
|
+
var POINTER = "\u276F";
|
|
348
|
+
var CHOSEN = "\u25C9";
|
|
349
|
+
var REQUIRED = "\u25C8";
|
|
350
|
+
var UNCHOSEN = "\u25CB";
|
|
351
|
+
|
|
352
|
+
class TextPrompt extends Prompt {
|
|
353
|
+
style;
|
|
354
|
+
title;
|
|
355
|
+
#value;
|
|
356
|
+
#at;
|
|
357
|
+
#error = "";
|
|
358
|
+
constructor(style, title, initial) {
|
|
359
|
+
super();
|
|
360
|
+
this.style = style;
|
|
361
|
+
this.title = title;
|
|
362
|
+
this.#value = initial;
|
|
363
|
+
this.#at = initial.length;
|
|
364
|
+
}
|
|
365
|
+
validate(_value) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
hint() {
|
|
369
|
+
return "Enter to accept. Ctrl+C to cancel.";
|
|
370
|
+
}
|
|
371
|
+
frame(width) {
|
|
372
|
+
const text = this.#value;
|
|
373
|
+
const at = this.#at;
|
|
374
|
+
const under = at < text.length ? text.slice(at, at + 1) : " ";
|
|
375
|
+
const drawn = text.slice(0, at) + this.style.invert(under) + text.slice(at + 1);
|
|
376
|
+
const footer = this.#error === "" ? this.style.muted(this.hint()) : this.style.danger(this.#error);
|
|
377
|
+
return [
|
|
378
|
+
`${this.style.accent(ASKING)} ${this.style.bold(this.title)}`,
|
|
379
|
+
this.style.clip(` ${drawn}`, width),
|
|
380
|
+
this.style.clip(` ${footer}`, width)
|
|
381
|
+
];
|
|
382
|
+
}
|
|
383
|
+
summary(width) {
|
|
384
|
+
return this.style.clip(`${this.style.accent(ANSWERED)} ${this.title} ${this.style.accent(this.#value)}`, width);
|
|
385
|
+
}
|
|
386
|
+
handle(key2) {
|
|
387
|
+
this.#error = "";
|
|
388
|
+
switch (key2.name) {
|
|
389
|
+
case KeyName.Char:
|
|
390
|
+
this.#value = this.#value.slice(0, this.#at) + key2.char + this.#value.slice(this.#at);
|
|
391
|
+
this.#at += key2.char.length;
|
|
392
|
+
return;
|
|
393
|
+
case KeyName.Space:
|
|
394
|
+
this.#error = "A space is not usable here.";
|
|
395
|
+
return;
|
|
396
|
+
case KeyName.Backspace:
|
|
397
|
+
if (this.#at === 0)
|
|
398
|
+
return;
|
|
399
|
+
this.#value = this.#value.slice(0, this.#at - 1) + this.#value.slice(this.#at);
|
|
400
|
+
this.#at -= 1;
|
|
401
|
+
return;
|
|
402
|
+
case KeyName.ClearLine:
|
|
403
|
+
this.#value = "";
|
|
404
|
+
this.#at = 0;
|
|
405
|
+
return;
|
|
406
|
+
case KeyName.Left:
|
|
407
|
+
this.#at = Math.max(0, this.#at - 1);
|
|
408
|
+
return;
|
|
409
|
+
case KeyName.Right:
|
|
410
|
+
this.#at = Math.min(this.#value.length, this.#at + 1);
|
|
411
|
+
return;
|
|
412
|
+
case KeyName.Home:
|
|
413
|
+
this.#at = 0;
|
|
414
|
+
return;
|
|
415
|
+
case KeyName.End:
|
|
416
|
+
this.#at = this.#value.length;
|
|
417
|
+
return;
|
|
418
|
+
case KeyName.Enter: {
|
|
419
|
+
const problem = this.validate(this.#value);
|
|
420
|
+
if (problem === undefined)
|
|
421
|
+
this.finish(this.#value);
|
|
422
|
+
else
|
|
423
|
+
this.#error = problem;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
default:
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
Object.defineProperty(TextPrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "protected readonly style: Style", typeOnly: "Style" }, { unresolved: "readonly title: string" }, { unresolved: "initial: string" }] });
|
|
432
|
+
|
|
433
|
+
class ConfirmPrompt extends Prompt {
|
|
434
|
+
style;
|
|
435
|
+
title;
|
|
436
|
+
#value;
|
|
437
|
+
constructor(style, title, initial) {
|
|
438
|
+
super();
|
|
439
|
+
this.style = style;
|
|
440
|
+
this.title = title;
|
|
441
|
+
this.#value = initial;
|
|
442
|
+
}
|
|
443
|
+
#option(label, active) {
|
|
444
|
+
return active ? this.style.invert(` ${label} `) : this.style.muted(` ${label} `);
|
|
445
|
+
}
|
|
446
|
+
frame(width) {
|
|
447
|
+
return [
|
|
448
|
+
`${this.style.accent(ASKING)} ${this.style.bold(this.title)}`,
|
|
449
|
+
this.style.clip(` ${this.#option("yes", this.#value)} ${this.#option("no", !this.#value)}`, width),
|
|
450
|
+
this.style.clip(` ${this.style.muted("y or n. Enter takes the highlighted one.")}`, width)
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
summary(width) {
|
|
454
|
+
const answer = this.#value ? "yes" : "no";
|
|
455
|
+
return this.style.clip(`${this.style.accent(ANSWERED)} ${this.title} ${this.style.accent(answer)}`, width);
|
|
456
|
+
}
|
|
457
|
+
handle(key2) {
|
|
458
|
+
const typed = key2.char.toLowerCase();
|
|
459
|
+
if (typed === "y") {
|
|
460
|
+
this.finish(true);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (typed === "n") {
|
|
464
|
+
this.finish(false);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (key2.name === KeyName.Left || key2.name === KeyName.Right || key2.name === KeyName.Tab || key2.name === KeyName.Space) {
|
|
468
|
+
this.#value = !this.#value;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key2.name === KeyName.Enter)
|
|
472
|
+
this.finish(this.#value);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
Object.defineProperty(ConfirmPrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly style: Style", typeOnly: "Style" }, { unresolved: "private readonly title: string" }, { unresolved: "initial: boolean" }] });
|
|
476
|
+
var CHROME = 4;
|
|
477
|
+
|
|
478
|
+
class SelectPrompt extends Prompt {
|
|
479
|
+
style;
|
|
480
|
+
title;
|
|
481
|
+
items;
|
|
482
|
+
#cursor = 0;
|
|
483
|
+
#offset = 0;
|
|
484
|
+
#chosen = new Set;
|
|
485
|
+
constructor(style, title, items, initial = []) {
|
|
486
|
+
super();
|
|
487
|
+
this.style = style;
|
|
488
|
+
this.title = title;
|
|
489
|
+
this.items = items;
|
|
490
|
+
for (const value of initial)
|
|
491
|
+
this.#chosen.add(value);
|
|
492
|
+
}
|
|
493
|
+
implied(_chosen) {
|
|
494
|
+
return new Set;
|
|
495
|
+
}
|
|
496
|
+
notes(_chosen) {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
#marker(value, implied) {
|
|
500
|
+
if (this.#chosen.has(value))
|
|
501
|
+
return this.style.accent(CHOSEN);
|
|
502
|
+
if (implied.has(value))
|
|
503
|
+
return this.style.warn(REQUIRED);
|
|
504
|
+
return this.style.muted(UNCHOSEN);
|
|
505
|
+
}
|
|
506
|
+
#window(budget) {
|
|
507
|
+
const visible = Math.max(1, Math.min(this.items.length, budget));
|
|
508
|
+
if (this.#cursor < this.#offset)
|
|
509
|
+
this.#offset = this.#cursor;
|
|
510
|
+
if (this.#cursor >= this.#offset + visible) {
|
|
511
|
+
this.#offset = this.#cursor - visible + 1;
|
|
512
|
+
}
|
|
513
|
+
this.#offset = Math.min(this.#offset, Math.max(0, this.items.length - visible));
|
|
514
|
+
return this.items.slice(this.#offset, this.#offset + visible);
|
|
515
|
+
}
|
|
516
|
+
frame(width, height) {
|
|
517
|
+
const implied = this.implied(this.#chosen);
|
|
518
|
+
const notes = this.notes(this.#chosen);
|
|
519
|
+
const pad = Math.max(...this.items.map((item) => item.label.length));
|
|
520
|
+
const window = this.#window(height - CHROME - notes.length);
|
|
521
|
+
const lines = [
|
|
522
|
+
`${this.style.accent(ASKING)} ${this.style.bold(this.title)} ` + this.style.muted(this.#count(implied))
|
|
523
|
+
];
|
|
524
|
+
if (this.#offset > 0) {
|
|
525
|
+
lines.push(this.style.muted(` \u2191 ${this.#offset} more`));
|
|
526
|
+
}
|
|
527
|
+
for (const [index, item] of window.entries()) {
|
|
528
|
+
const at = this.#offset + index;
|
|
529
|
+
const pointer = at === this.#cursor ? this.style.accent(POINTER) : " ";
|
|
530
|
+
const label = at === this.#cursor ? this.style.bold(item.label.padEnd(pad)) : item.label.padEnd(pad);
|
|
531
|
+
lines.push(this.style.clip(`${pointer} ${this.#marker(item.value, implied)} ${label} ${this.style.muted(item.hint)}`, width));
|
|
532
|
+
}
|
|
533
|
+
const below = this.items.length - this.#offset - window.length;
|
|
534
|
+
if (below > 0)
|
|
535
|
+
lines.push(this.style.muted(` \u2193 ${below} more`));
|
|
536
|
+
for (const note of notes) {
|
|
537
|
+
lines.push(this.style.clip(` ${this.style.warn(note)}`, width));
|
|
538
|
+
}
|
|
539
|
+
lines.push(this.style.clip(` ${this.style.muted("Space toggles. \u2191\u2193 moves. a all, n none. Enter continues.")}`, width));
|
|
540
|
+
return lines;
|
|
541
|
+
}
|
|
542
|
+
#count(implied) {
|
|
543
|
+
if (this.#chosen.size === 0)
|
|
544
|
+
return "nothing chosen yet";
|
|
545
|
+
const extra = implied.size === 0 ? "" : `, ${implied.size} pulled in`;
|
|
546
|
+
return `${this.#chosen.size} chosen${extra}`;
|
|
547
|
+
}
|
|
548
|
+
summary(width) {
|
|
549
|
+
const chosen = this.#ordered();
|
|
550
|
+
const answer = chosen.length === 0 ? "none, the minimal template" : chosen.join(", ");
|
|
551
|
+
return this.style.clip(`${this.style.accent(ANSWERED)} ${this.title} ${this.style.accent(answer)}`, width);
|
|
552
|
+
}
|
|
553
|
+
#ordered() {
|
|
554
|
+
return this.items.map((item) => item.value).filter((value) => this.#chosen.has(value));
|
|
555
|
+
}
|
|
556
|
+
#move(by) {
|
|
557
|
+
const count = this.items.length;
|
|
558
|
+
this.#cursor = (this.#cursor + by + count) % count;
|
|
559
|
+
}
|
|
560
|
+
handle(key2) {
|
|
561
|
+
switch (key2.name) {
|
|
562
|
+
case KeyName.Up:
|
|
563
|
+
case KeyName.ShiftTab:
|
|
564
|
+
this.#move(-1);
|
|
565
|
+
return;
|
|
566
|
+
case KeyName.Down:
|
|
567
|
+
case KeyName.Tab:
|
|
568
|
+
this.#move(1);
|
|
569
|
+
return;
|
|
570
|
+
case KeyName.Home:
|
|
571
|
+
this.#cursor = 0;
|
|
572
|
+
return;
|
|
573
|
+
case KeyName.End:
|
|
574
|
+
this.#cursor = this.items.length - 1;
|
|
575
|
+
return;
|
|
576
|
+
case KeyName.Space: {
|
|
577
|
+
const value = this.items[this.#cursor]?.value;
|
|
578
|
+
if (value === undefined)
|
|
579
|
+
return;
|
|
580
|
+
if (!this.#chosen.delete(value))
|
|
581
|
+
this.#chosen.add(value);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
case KeyName.Enter:
|
|
585
|
+
this.finish(this.#ordered());
|
|
586
|
+
return;
|
|
587
|
+
case KeyName.Char:
|
|
588
|
+
this.#type(key2.char);
|
|
589
|
+
return;
|
|
590
|
+
default:
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
#type(char) {
|
|
595
|
+
if (char === "k")
|
|
596
|
+
this.#move(-1);
|
|
597
|
+
else if (char === "j")
|
|
598
|
+
this.#move(1);
|
|
599
|
+
else if (char === "a")
|
|
600
|
+
for (const item of this.items)
|
|
601
|
+
this.#chosen.add(item.value);
|
|
602
|
+
else if (char === "n")
|
|
603
|
+
this.#chosen.clear();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
Object.defineProperty(SelectPrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "protected readonly style: Style", typeOnly: "Style" }, { unresolved: "readonly title: string" }, { unresolved: "protected readonly items: readonly SelectItem[]" }, { unresolved: "initial: readonly string[] = []", optional: true }] });
|
|
607
|
+
|
|
608
|
+
// src/wizard.ts
|
|
609
|
+
var DEFAULT_TARGET = "my-api";
|
|
610
|
+
|
|
611
|
+
class DirectoryPrompt extends TextPrompt {
|
|
612
|
+
validate(value) {
|
|
613
|
+
return value.trim() === "" ? "Name a directory to write into." : undefined;
|
|
614
|
+
}
|
|
615
|
+
hint() {
|
|
616
|
+
return 'Enter to accept. A single "." writes into the directory you are in.';
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
class PackageNamePrompt extends TextPrompt {
|
|
621
|
+
validate(value) {
|
|
622
|
+
return isValidPackageName(value) ? undefined : "npm forbids uppercase, and a leading dot or underscore.";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
class FeaturePrompt extends SelectPrompt {
|
|
627
|
+
constructor(style, initial) {
|
|
628
|
+
super(style, "Features", FEATURES.map((feature) => ({
|
|
629
|
+
value: feature.name,
|
|
630
|
+
label: feature.name,
|
|
631
|
+
hint: feature.summary
|
|
632
|
+
})), initial);
|
|
633
|
+
}
|
|
634
|
+
#resolved(chosen) {
|
|
635
|
+
return resolveFeatures([...chosen]);
|
|
636
|
+
}
|
|
637
|
+
implied(chosen) {
|
|
638
|
+
return new Set(impliedBy([...chosen], this.#resolved(chosen)));
|
|
639
|
+
}
|
|
640
|
+
notes(chosen) {
|
|
641
|
+
const resolved = this.#resolved(chosen);
|
|
642
|
+
const lines = [];
|
|
643
|
+
const implied = impliedBy([...chosen], resolved);
|
|
644
|
+
if (implied.length > 0) {
|
|
645
|
+
const verb = implied.length === 1 ? "comes" : "come";
|
|
646
|
+
lines.push(`${implied.join(", ")} ${verb} along as a requirement.`);
|
|
647
|
+
}
|
|
648
|
+
const services = new Map;
|
|
649
|
+
for (const feature of resolved) {
|
|
650
|
+
if (feature.service === undefined)
|
|
651
|
+
continue;
|
|
652
|
+
const named = services.get(feature.service) ?? [];
|
|
653
|
+
named.push(feature.name);
|
|
654
|
+
services.set(feature.service, named);
|
|
655
|
+
}
|
|
656
|
+
for (const [service, named] of services) {
|
|
657
|
+
const verb = named.length === 1 ? "needs" : "need";
|
|
658
|
+
lines.push(`${named.join(", ")} ${verb} ${service}.`);
|
|
659
|
+
}
|
|
660
|
+
return lines;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
Object.defineProperty(FeaturePrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "style: Style", typeOnly: "Style" }, { unresolved: "initial: readonly string[]" }] });
|
|
664
|
+
|
|
665
|
+
class Wizard {
|
|
666
|
+
#runner;
|
|
667
|
+
#style;
|
|
668
|
+
constructor(runner, style) {
|
|
669
|
+
this.#runner = runner;
|
|
670
|
+
this.#style = style;
|
|
20
671
|
}
|
|
21
|
-
|
|
672
|
+
async run(defaults) {
|
|
673
|
+
const target = defaults.target ?? await this.#runner.ask(new DirectoryPrompt(this.#style, "Directory", DEFAULT_TARGET));
|
|
674
|
+
const name = await this.#name(defaults, target);
|
|
675
|
+
const features = await this.#runner.ask(new FeaturePrompt(this.#style, defaults.features));
|
|
676
|
+
const force = await this.#force(defaults, target);
|
|
677
|
+
return { target, name, features, force };
|
|
678
|
+
}
|
|
679
|
+
async#name(defaults, target) {
|
|
680
|
+
if (defaults.name !== undefined)
|
|
681
|
+
return defaults.name;
|
|
682
|
+
const derived = basename(resolve(defaults.cwd, target));
|
|
683
|
+
if (isValidPackageName(derived))
|
|
684
|
+
return derived;
|
|
685
|
+
return this.#runner.ask(new PackageNamePrompt(this.#style, "Package name", slug(derived)));
|
|
686
|
+
}
|
|
687
|
+
async#force(defaults, target) {
|
|
688
|
+
if (defaults.force)
|
|
689
|
+
return true;
|
|
690
|
+
const blocking = blockingEntries(resolve(defaults.cwd, target));
|
|
691
|
+
if (blocking.length === 0)
|
|
692
|
+
return false;
|
|
693
|
+
const shown = blocking.slice(0, 3).join(", ");
|
|
694
|
+
const rest = blocking.length > 3 ? `, +${blocking.length - 3} more` : "";
|
|
695
|
+
const proceed = await this.#runner.ask(new ConfirmPrompt(this.#style, `${target} already has files in it (${shown}${rest}). Write into it anyway?`, false));
|
|
696
|
+
if (!proceed)
|
|
697
|
+
throw new CancelledError("Nothing written.");
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
Object.defineProperty(Wizard, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "runner: PromptRunner", typeOnly: "PromptRunner" }, { unresolved: "style: Style", typeOnly: "Style" }] });
|
|
702
|
+
var slug = (value) => {
|
|
703
|
+
const lowered = value.toLowerCase().replace(/[^a-z0-9\-._~]+/g, "-").replace(/^[-._]+/, "").replace(/-+$/, "");
|
|
704
|
+
return lowered === "" ? DEFAULT_TARGET : lowered;
|
|
22
705
|
};
|
|
23
706
|
|
|
24
707
|
// src/cli.ts
|
|
25
708
|
var USAGE = `Scaffold a new dunx application.
|
|
26
709
|
|
|
27
|
-
bunx @dunx/create-app
|
|
710
|
+
bunx @dunx/create-app [directory]
|
|
711
|
+
|
|
712
|
+
It asks. Up and down move, space toggles a feature, Enter takes the selection,
|
|
713
|
+
and the list says which features your choices pull in and which need a service
|
|
714
|
+
running. Nothing about the app is chosen by a flag.
|
|
28
715
|
|
|
29
716
|
Options:
|
|
30
|
-
--name <name>
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
With no --with and no prompt, you get the minimal template: five files, one route.
|
|
40
|
-
With features, the wiring is generated around them and each feature's directory is
|
|
41
|
-
copied from dunx's own examples/full, which CI runs and tours on every push.
|
|
717
|
+
--name <name> package name for the app (default: the directory name)
|
|
718
|
+
--force write into a directory that already has files in it
|
|
719
|
+
--yes, -y skip the questions and take the minimal template
|
|
720
|
+
--help print this
|
|
721
|
+
|
|
722
|
+
With no terminal to ask in - piped, or CI - it takes the minimal template rather
|
|
723
|
+
than blocking on a question nothing can answer. To choose features from a script,
|
|
724
|
+
call \`scaffold({ features })\` from \`@dunx/create-app\` instead.
|
|
42
725
|
`;
|
|
43
|
-
var featureList = () => FEATURES.map((feature) => {
|
|
44
|
-
const needs = feature.requires.length === 0 ? "" : ` (pulls in ${feature.requires.join(", ")})`;
|
|
45
|
-
const service = feature.service === undefined ? "" : ` [needs ${feature.service}]`;
|
|
46
|
-
return ` ${feature.name.padEnd(12)}${feature.summary}${needs}${service}`;
|
|
47
|
-
}).join(`
|
|
48
|
-
`);
|
|
49
726
|
function fail(message) {
|
|
50
727
|
console.error(message);
|
|
51
728
|
process.exit(1);
|
|
@@ -55,10 +732,6 @@ var { values, positionals } = parseArgs({
|
|
|
55
732
|
allowPositionals: true,
|
|
56
733
|
options: {
|
|
57
734
|
name: { type: "string" },
|
|
58
|
-
template: { type: "string" },
|
|
59
|
-
with: { type: "string" },
|
|
60
|
-
all: { type: "boolean", default: false },
|
|
61
|
-
list: { type: "boolean", default: false },
|
|
62
735
|
force: { type: "boolean", default: false },
|
|
63
736
|
yes: { type: "boolean", default: false, short: "y" },
|
|
64
737
|
help: { type: "boolean", default: false, short: "h" }
|
|
@@ -68,59 +741,55 @@ if (values.help === true) {
|
|
|
68
741
|
console.log(USAGE);
|
|
69
742
|
process.exit(0);
|
|
70
743
|
}
|
|
71
|
-
|
|
72
|
-
console.log(`Features:
|
|
73
|
-
${featureList()}`);
|
|
74
|
-
process.exit(0);
|
|
75
|
-
}
|
|
744
|
+
var style = new Style;
|
|
76
745
|
var target = positionals[0];
|
|
77
|
-
|
|
78
|
-
|
|
746
|
+
var withoutAsking = () => {
|
|
747
|
+
if (target === undefined) {
|
|
748
|
+
fail(`Missing the target directory.
|
|
79
749
|
|
|
80
750
|
${USAGE}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
var ask = async () => {
|
|
84
|
-
if (values.all === true)
|
|
85
|
-
return featureNames;
|
|
86
|
-
if (values.with !== undefined)
|
|
87
|
-
return split(values.with);
|
|
88
|
-
if (values.yes === true || !process.stdin.isTTY)
|
|
89
|
-
return [];
|
|
90
|
-
console.log(`Features (empty for the minimal template):
|
|
91
|
-
${featureList()}
|
|
92
|
-
`);
|
|
93
|
-
console.log("Names or numbers, comma separated. `all` for everything.");
|
|
94
|
-
process.stdout.write("> ");
|
|
95
|
-
const answer = await readLine();
|
|
96
|
-
if (answer === "")
|
|
97
|
-
return [];
|
|
98
|
-
if (answer === "all")
|
|
99
|
-
return featureNames;
|
|
100
|
-
return split(answer).map((part) => {
|
|
101
|
-
const index = Number(part);
|
|
102
|
-
return Number.isInteger(index) && index >= 1 && index <= FEATURES.length ? FEATURES[index - 1]?.name ?? part : part;
|
|
103
|
-
});
|
|
104
|
-
};
|
|
105
|
-
try {
|
|
106
|
-
const requested = await ask();
|
|
107
|
-
const result = await scaffold({
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
108
753
|
target,
|
|
109
754
|
...values.name === undefined ? {} : { name: values.name },
|
|
110
|
-
|
|
111
|
-
features: requested,
|
|
755
|
+
features: [],
|
|
112
756
|
force: values.force === true
|
|
757
|
+
};
|
|
758
|
+
};
|
|
759
|
+
var byAsking = async () => {
|
|
760
|
+
const tty = new ProcessTty;
|
|
761
|
+
const banner = new Banner(style).lines(tty.columns, await packageVersion());
|
|
762
|
+
console.log(["", ...banner, ""].join(`
|
|
763
|
+
`));
|
|
764
|
+
const wizard = new Wizard(new PromptRunner(tty), style);
|
|
765
|
+
const answers = await wizard.run({
|
|
766
|
+
target,
|
|
767
|
+
name: values.name,
|
|
768
|
+
features: [],
|
|
769
|
+
force: values.force === true,
|
|
770
|
+
cwd: process.cwd()
|
|
113
771
|
});
|
|
772
|
+
return answers;
|
|
773
|
+
};
|
|
774
|
+
var interactive = values.yes !== true && ProcessTty.available();
|
|
775
|
+
try {
|
|
776
|
+
const options = interactive ? await byAsking() : withoutAsking();
|
|
777
|
+
const result = await scaffold(options);
|
|
114
778
|
const where = relative(process.cwd(), result.directory);
|
|
115
|
-
console.log(where === "" ? `
|
|
779
|
+
console.log(where === "" ? `
|
|
780
|
+
Created ${result.name} here` : `
|
|
781
|
+
Created ${result.name} in ${where}/`);
|
|
116
782
|
if (result.features.length === 0) {
|
|
117
783
|
console.log(` ${result.files.length} files from the ${result.template} template
|
|
118
784
|
`);
|
|
119
785
|
} else {
|
|
120
|
-
const
|
|
121
|
-
|
|
786
|
+
const requested = options.features ?? [];
|
|
787
|
+
const implied = result.features.filter((name) => !requested.includes(name));
|
|
788
|
+
const noun = result.features.length === 1 ? "feature" : "features";
|
|
789
|
+
console.log(` ${result.files.length} files, ${result.features.length} ${noun}: ` + `${result.features.join(", ")}`);
|
|
122
790
|
if (implied.length > 0) {
|
|
123
|
-
|
|
791
|
+
const as = implied.length === 1 ? "a requirement" : "requirements";
|
|
792
|
+
console.log(` ${implied.join(", ")} came along as ${as}`);
|
|
124
793
|
}
|
|
125
794
|
const services = FEATURES.filter((feature) => result.features.includes(feature.name) && feature.service !== undefined);
|
|
126
795
|
for (const feature of services) {
|
|
@@ -132,8 +801,12 @@ try {
|
|
|
132
801
|
if (where !== "")
|
|
133
802
|
console.log(` cd ${where}`);
|
|
134
803
|
console.log(" bun install");
|
|
135
|
-
console.log(
|
|
804
|
+
console.log(` bun run dev ${style.muted("# or `start`, which does not watch")}`);
|
|
136
805
|
} catch (error) {
|
|
806
|
+
if (error instanceof CancelledError) {
|
|
807
|
+
console.log(style.muted(error.message));
|
|
808
|
+
process.exit(130);
|
|
809
|
+
}
|
|
137
810
|
if (error instanceof ScaffoldError)
|
|
138
811
|
fail(error.message);
|
|
139
812
|
throw error;
|