create-vite-react-boot 1.0.16 → 1.0.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/bin/cli.js +198 -9
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,187 @@ import prompts from "prompts";
|
|
|
6
6
|
import spawn from "cross-spawn"; // 크로스플랫폼 스폰
|
|
7
7
|
import { green, yellow, red, cyan, bold } from "kolorist";
|
|
8
8
|
import { applyScaffold } from "../lib/apply.js";
|
|
9
|
+
import readline from "node:readline"; // ← 진행바 렌더링용
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function formatHMS(s) {
|
|
13
|
+
const m = Math.floor(s / 60).toString().padStart(2, "0");
|
|
14
|
+
const ss = Math.floor(s % 60).toString().padStart(2, "0");
|
|
15
|
+
return `${m}:${ss}`;
|
|
16
|
+
}
|
|
17
|
+
function barLine({ width, ratio }) {
|
|
18
|
+
const w = Math.max(10, width);
|
|
19
|
+
const filled = Math.round(w * Math.max(0, Math.min(1, ratio)));
|
|
20
|
+
return `[${"#".repeat(filled)}${"-".repeat(w - filled)}]`;
|
|
21
|
+
}
|
|
22
|
+
function writeLine(y, text) {
|
|
23
|
+
readline.cursorTo(process.stdout, 0, y);
|
|
24
|
+
readline.clearLine(process.stdout, 0);
|
|
25
|
+
process.stdout.write(text);
|
|
26
|
+
}
|
|
27
|
+
function termSize() {
|
|
28
|
+
const cols = process.stdout.columns || 80;
|
|
29
|
+
const rows = process.stdout.rows || 24;
|
|
30
|
+
return { cols, rows };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작업") {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
const { cols } = termSize();
|
|
36
|
+
const barWidth = Math.min(40, Math.max(20, Math.floor(cols * 0.35)));
|
|
37
|
+
let linesReserved = 3; // 헤더 + 메인바 + 로그요약 1줄
|
|
38
|
+
|
|
39
|
+
// 상태
|
|
40
|
+
let percent = null; // 0..1 (정확 모드에서만)
|
|
41
|
+
let downloadedBytes = 0; // 추정치(출력 길이 기반, npm fallback)
|
|
42
|
+
let totalHint = null; // yarn/pnpm가 알려주면 총량
|
|
43
|
+
let lastTick = Date.now();
|
|
44
|
+
let emaRate = 0; // 지수이동평균(바이트/초), ETA 추정용
|
|
45
|
+
const alpha = 0.15;
|
|
46
|
+
|
|
47
|
+
// 화면 세팅
|
|
48
|
+
const startTop = process.stdout.rows ? (process.stdout.rows - 1) : 0;
|
|
49
|
+
process.stdout.write("\x1B[?25l"); // 커서 숨김
|
|
50
|
+
const header = () => {
|
|
51
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
52
|
+
const eta = (() => {
|
|
53
|
+
if (percent != null && percent > 0 && totalHint) {
|
|
54
|
+
const remain = (1 - percent) * totalHint;
|
|
55
|
+
const rate = emaRate > 1 ? emaRate : null;
|
|
56
|
+
return rate ? formatHMS(remain / rate) : "--:--";
|
|
57
|
+
}
|
|
58
|
+
if (emaRate > 100) {
|
|
59
|
+
const remainBytes = totalHint ? (totalHint - downloadedBytes) : null;
|
|
60
|
+
if (remainBytes && remainBytes > 0) return formatHMS(remainBytes / emaRate);
|
|
61
|
+
}
|
|
62
|
+
return "--:--";
|
|
63
|
+
})();
|
|
64
|
+
return `${label} [${formatHMS(elapsed)}] ${totalHint ? `${(downloadedBytes / 1048576).toFixed(1)} / ${(totalHint / 1048576).toFixed(1)} MiB` : `${(downloadedBytes / 1048576).toFixed(1)} MiB`} (eta: ${eta})`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// 렌더러
|
|
68
|
+
function render(lastMsg = "") {
|
|
69
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
70
|
+
const ratio = percent != null ? percent : Math.min(0.95, Math.max(0.1, 1 - 1 / Math.max(2, elapsed / 3)));
|
|
71
|
+
const line0 = header();
|
|
72
|
+
const line1 = `${barLine({ width: barWidth, ratio })}`;
|
|
73
|
+
const line2 = lastMsg ? `… ${lastMsg.slice(0, cols - 4)}` : "";
|
|
74
|
+
|
|
75
|
+
// 위에서 3줄 확보해 덮어쓰기
|
|
76
|
+
writeLine(0, line0.padEnd(cols));
|
|
77
|
+
writeLine(1, line1.padEnd(cols));
|
|
78
|
+
writeLine(2, line2.padEnd(cols));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 프로세스 실행
|
|
82
|
+
await new Promise((resolve, reject) => {
|
|
83
|
+
// yarn/pnpm에선 JSON/ndjson 모드로 이벤트 파싱을 노려본다
|
|
84
|
+
let a = args.slice();
|
|
85
|
+
const isYarn = /yarn/i.test(cmd);
|
|
86
|
+
const isPnpm = /pnpm/i.test(cmd);
|
|
87
|
+
|
|
88
|
+
if (isYarn && !a.includes("--json")) a.push("--json");
|
|
89
|
+
if (isPnpm && !a.includes("--reporter")) a.push("--reporter", "ndjson");
|
|
90
|
+
|
|
91
|
+
const child = spawn(cmd, a, {
|
|
92
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
+
cwd,
|
|
94
|
+
env: { ...process.env, ...env },
|
|
95
|
+
shell: false,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let bufOut = "", bufErr = "";
|
|
99
|
+
let lastMsg = "";
|
|
100
|
+
|
|
101
|
+
const handleJSONLine = (line) => {
|
|
102
|
+
try {
|
|
103
|
+
const j = JSON.parse(line);
|
|
104
|
+
// yarn classic: progressStart/progressTick/progressFinish
|
|
105
|
+
if (j.type === "progressStart" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
|
|
106
|
+
const total = j.data.total;
|
|
107
|
+
totalHint = total; // 단위는 'tick'이지만 총량 힌트로 사용
|
|
108
|
+
}
|
|
109
|
+
if (j.type === "progressTick" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
|
|
110
|
+
percent = Math.max(0, Math.min(1, j.data.current / j.data.total));
|
|
111
|
+
// tick 기반으로도 바이트 추정치 보정
|
|
112
|
+
downloadedBytes += 64 * 1024; // 대략치
|
|
113
|
+
}
|
|
114
|
+
if (j.type === "step" && j.data) {
|
|
115
|
+
lastMsg = j.data;
|
|
116
|
+
}
|
|
117
|
+
// pnpm ndjson: msg에 Progress 문구가 섞임
|
|
118
|
+
if (j.msg) lastMsg = j.msg;
|
|
119
|
+
} catch { /* ignore non-json */ }
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const tickRate = (chunkLen) => {
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const dt = (now - lastTick) / 1000;
|
|
125
|
+
lastTick = now;
|
|
126
|
+
if (dt > 0) {
|
|
127
|
+
const inst = chunkLen / dt;
|
|
128
|
+
emaRate = emaRate ? (emaRate * (1 - alpha) + inst * alpha) : inst;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
child.stdout.on("data", (b) => {
|
|
133
|
+
const s = b.toString("utf8");
|
|
134
|
+
bufOut += s;
|
|
135
|
+
downloadedBytes += b.length; // npm fallback 시 “대략 MiB”로 사용
|
|
136
|
+
tickRate(b.length);
|
|
137
|
+
|
|
138
|
+
// yarn/pnpm JSON 라인 파싱
|
|
139
|
+
if (isYarn || isPnpm) {
|
|
140
|
+
const lines = s.split(/\r?\n/);
|
|
141
|
+
for (const ln of lines) {
|
|
142
|
+
if (!ln.trim()) continue;
|
|
143
|
+
handleJSONLine(ln);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
|
|
147
|
+
}
|
|
148
|
+
render(lastMsg);
|
|
149
|
+
});
|
|
150
|
+
child.stderr.on("data", (b) => {
|
|
151
|
+
const s = b.toString("utf8");
|
|
152
|
+
bufErr += s;
|
|
153
|
+
downloadedBytes += b.length;
|
|
154
|
+
tickRate(b.length);
|
|
155
|
+
|
|
156
|
+
if (isYarn || isPnpm) {
|
|
157
|
+
const lines = s.split(/\r?\n/);
|
|
158
|
+
for (const ln of lines) {
|
|
159
|
+
if (!ln.trim()) continue;
|
|
160
|
+
// 일부 PM은 stderr에도 json이 섞여온다
|
|
161
|
+
if ((ln.startsWith("{") && ln.endsWith("}"))) handleJSONLine(ln);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
|
|
165
|
+
}
|
|
166
|
+
render(lastMsg);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// 주기적 리렌더 (출력이 잠시 멎어도 바 갱신)
|
|
170
|
+
const iv = setInterval(() => render(lastMsg), 80);
|
|
171
|
+
|
|
172
|
+
child.on("error", (e) => {
|
|
173
|
+
clearInterval(iv);
|
|
174
|
+
// 마지막 화면 정리
|
|
175
|
+
render("에러 발생");
|
|
176
|
+
process.stdout.write("\x1B[?25h");
|
|
177
|
+
reject(e);
|
|
178
|
+
});
|
|
179
|
+
child.on("close", (code) => {
|
|
180
|
+
clearInterval(iv);
|
|
181
|
+
percent = 1;
|
|
182
|
+
render("완료");
|
|
183
|
+
process.stdout.write("\n\x1B[?25h");
|
|
184
|
+
if (code === 0) resolve();
|
|
185
|
+
else reject(Object.assign(new Error(`${cmd} exited ${code}`), { code, stdout: bufOut, stderr: bufErr }));
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
9
190
|
|
|
10
191
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
192
|
const cwd = process.cwd();
|
|
@@ -276,19 +457,27 @@ export default function App(){
|
|
|
276
457
|
|
|
277
458
|
// ─────────────────────────────────────────────
|
|
278
459
|
// 2) 1회 설치 (조용히 설치: fund/audit 등 장황 로그 숨김)
|
|
279
|
-
|
|
280
|
-
|
|
460
|
+
// ─────────────────────────────────────────────
|
|
461
|
+
console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
|
|
462
|
+
|
|
281
463
|
|
|
282
464
|
// 패키지 매니저별 silent 옵션
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"";
|
|
465
|
+
const silentFlag =
|
|
466
|
+
pm === "npm" ? "--silent" :
|
|
467
|
+
pm === "yarn" ? "--silent" :
|
|
468
|
+
pm === "pnpm" ? "--silent" : "";
|
|
288
469
|
|
|
289
|
-
|
|
290
|
-
|
|
470
|
+
// CI 환경이면 npm은 ci 사용, 아니면 install
|
|
471
|
+
const isCI = process.env.CI === "true";
|
|
472
|
+
const args =
|
|
473
|
+
pm === "npm"
|
|
474
|
+
? [isCI ? "ci" : "install", ...(silentFlag ? [silentFlag] : [])]
|
|
475
|
+
: ["install", ...(silentFlag ? [silentFlag] : [])];
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
await runWithProgress(pm, args, { cwd: target }, "의존성 설치");
|
|
291
479
|
|
|
480
|
+
console.log(green(" → 의존성 설치 완료!\n"));
|
|
292
481
|
// ─────────────────────────────────────────────
|
|
293
482
|
// 3) 인증/라우팅/axios/Tailwind 추가 파일 (파일만 생성, 설치 없음)
|
|
294
483
|
// ─────────────────────────────────────────────
|