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.
Files changed (2) hide show
  1. package/bin/cli.js +198 -9
  2. 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
- console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
460
+ // ─────────────────────────────────────────────
461
+ console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
462
+
281
463
 
282
464
  // 패키지 매니저별 silent 옵션
283
- const silentFlag =
284
- pm === "npm" ? "--silent" :
285
- pm === "yarn" ? "--silent" :
286
- pm === "pnpm" ? "--silent" :
287
- "";
465
+ const silentFlag =
466
+ pm === "npm" ? "--silent" :
467
+ pm === "yarn" ? "--silent" :
468
+ pm === "pnpm" ? "--silent" : "";
288
469
 
289
- runQuiet(pm, ["install", ...(silentFlag ? [silentFlag] : [])], { cwd: target });
290
- console.log(green(" → 의존성 설치 완료!\n"));
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
  // ─────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vite-react-boot",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Create a Vite + React + TS app with Tailwind, axios, AuthContext, login/register, routing.",
5
5
  "type": "module",
6
6
  "bin": {