create-vite-react-boot 1.0.19 → 1.0.20

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 +71 -60
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -69,60 +69,80 @@ function barLine({ width, ratio }) {
69
69
  /** 진행바 기반 실행 (yarn/pnpm은 이벤트 파싱, npm은 추정 진행) */
70
70
  async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작업") {
71
71
  const start = Date.now();
72
- const { cols } = termSize();
73
- const barWidth = Math.min(40, Math.max(20, Math.floor(cols * 0.35)));
72
+ const cols = process.stdout.columns || 80;
73
+
74
+ // ── Windows/Git Bash 등에서 안전하게: 1줄 Compact 모드
75
+ const compact = process.platform === "win32" || process.env.PROGRESS_COMPACT === "1";
74
76
 
75
77
  // 상태
76
- let percent = null; // 0..1 (정확 모드에서만)
78
+ let percent = null; // 0..1 (yarn/pnpm 이벤트 기반)
77
79
  let downloadedBytes = 0; // 추정치(출력 길이 기반)
78
- let totalHint = null; // yarn/pnpm가 알려주면 총량
80
+ let totalHint = null; // yarn/pnpm가 주면 총량 추정
79
81
  let lastTick = Date.now();
80
- let emaRate = 0; // 지수이동평균(바이트/초)
82
+ let emaRate = 0; // byte/s
81
83
  const alpha = 0.15;
82
84
 
83
- // 화면 세팅 (3줄 확보 후 위로 올라가며 덮어쓰기)
84
- process.stdout.write("\x1B[?25l"); // 커서 숨김
85
- process.stdout.write("\n\n\n"); // 3줄 확보
85
+ // 렌더 throttle & dedupe
86
+ let lastRender = 0;
87
+ let lastPrinted = "";
86
88
 
87
- const header = () => {
89
+ const mkLine = (brief = "") => {
88
90
  const elapsed = (Date.now() - start) / 1000;
91
+ const ratio = percent != null
92
+ ? percent
93
+ : Math.min(0.95, Math.max(0.1, 1 - 1 / Math.max(2, elapsed / 3)));
94
+
95
+ const barWidth = Math.min(24, Math.max(10, Math.floor(cols * 0.25)));
96
+ const filled = Math.round(barWidth * Math.max(0, Math.min(1, ratio)));
97
+ const bar = `[${"#".repeat(filled)}${"-".repeat(barWidth - filled)}]`;
98
+
89
99
  const eta = (() => {
90
100
  if (percent != null && percent > 0 && totalHint) {
91
101
  const remain = (1 - percent) * totalHint;
92
102
  const rate = emaRate > 1 ? emaRate : null;
93
103
  return rate ? formatHMS(remain / rate) : "--:--";
94
104
  }
95
- if (emaRate > 100) {
96
- const remainBytes = totalHint ? (totalHint - downloadedBytes) : null;
97
- if (remainBytes && remainBytes > 0) return formatHMS(remainBytes / emaRate);
98
- }
99
- return "--:--";
105
+ return emaRate > 50 ? formatHMS((totalHint ? Math.max(0, totalHint - downloadedBytes) : 0) / Math.max(1, emaRate)) : "--:--";
100
106
  })();
107
+
101
108
  const sizeText = totalHint
102
- ? `${(downloadedBytes / 1048576).toFixed(1)} / ${(totalHint / 1048576).toFixed(1)} MiB`
103
- : `${(downloadedBytes / 1048576).toFixed(1)} MiB`;
104
- return `${label} [${formatHMS(elapsed)}] ${sizeText} (eta: ${eta})`;
109
+ ? `${(downloadedBytes / 1048576).toFixed(1)}/${(totalHint / 1048576).toFixed(1)}MiB`
110
+ : `${(downloadedBytes / 1048576).toFixed(1)}MiB`;
111
+
112
+ const head = `${label} [${formatHMS(elapsed)}] ${sizeText} (eta:${eta})`;
113
+ const msg = brief ? ` ${brief}` : "";
114
+
115
+ // Compact 1줄로 잘라쓰기
116
+ let line = `${head} ${bar}${msg}`;
117
+ if (line.length > cols) line = line.slice(0, cols - 1);
118
+ return line;
105
119
  };
106
120
 
107
- function render(lastMsg = "") {
108
- const elapsed = (Date.now() - start) / 1000;
109
- const ratio = percent != null
110
- ? percent
111
- : Math.min(0.95, Math.max(0.1, 1 - 1 / Math.max(2, elapsed / 3)));
121
+ const safeWrite = (line) => {
122
+ // 200ms 이하/동일 라인일 스킵
123
+ const now = Date.now();
124
+ if (now - lastRender < 200) return;
125
+ if (line === lastPrinted) return;
112
126
 
113
- // 커서를 위로 3줄 올려 블록 덮어쓰기
114
- process.stdout.write("\x1B[3F"); // cursor previous line x3
115
- // line 0
116
- readline.clearLine(process.stdout, 0);
117
- process.stdout.write((header()).padEnd(cols) + "\n");
118
- // line 1
127
+ lastRender = now;
128
+ lastPrinted = line;
129
+
130
+ // 1줄 덮어쓰기
119
131
  readline.clearLine(process.stdout, 0);
120
- process.stdout.write(barLine({ width: barWidth, ratio }).padEnd(cols) + "\n");
121
- // line 2
132
+ readline.cursorTo(process.stdout, 0);
133
+ process.stdout.write(line);
134
+ };
135
+
136
+ const finishWrite = (ok = true) => {
122
137
  readline.clearLine(process.stdout, 0);
123
- const brief = lastMsg ? `… ${lastMsg.slice(0, cols - 4)}` : "";
124
- process.stdout.write(brief.padEnd(cols) + "\n");
125
- }
138
+ readline.cursorTo(process.stdout, 0);
139
+ process.stdout.write((ok ? "… 완료" : "… 실패") + "\n");
140
+ };
141
+
142
+ // 커서 숨김 (종료 시 복구는 전역 핸들러에서)
143
+ process.stdout.write("\x1B[?25l");
144
+ // Compact 모드는 별도 빈줄 확보 없이 1줄만 사용
145
+ safeWrite(mkLine());
126
146
 
127
147
  await new Promise((resolve, reject) => {
128
148
  let a = args.slice();
@@ -140,23 +160,20 @@ async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작
140
160
  });
141
161
 
142
162
  let lastMsg = "";
143
-
144
163
  const handleJSONLine = (line) => {
145
164
  try {
146
165
  const j = JSON.parse(line);
147
- // yarn classic: progressStart/progressTick/progressFinish
148
- if (j.type === "progressStart" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
149
- totalHint = j.data.total; // tick 총량 힌트
166
+ if (j.type === "progressStart" && j.data && typeof j.data.total === "number") {
167
+ totalHint = j.data.total;
150
168
  }
151
169
  if (j.type === "progressTick" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
152
- percent = Math.max(0, Math.min(1, j.data.current / j.data.total));
170
+ const p = j.data.current / j.data.total;
171
+ if (!Number.isNaN(p)) percent = Math.max(0, Math.min(1, p));
153
172
  downloadedBytes += 64 * 1024; // 대략 보정
154
173
  }
155
- if (j.type === "step" && j.data) {
156
- lastMsg = String(j.data);
157
- }
174
+ if (j.type === "step" && j.data) lastMsg = String(j.data);
158
175
  if (j.msg) lastMsg = String(j.msg);
159
- } catch { /* ignore non-json */ }
176
+ } catch { /* ignore */ }
160
177
  };
161
178
 
162
179
  const tickRate = (chunkLen) => {
@@ -174,49 +191,43 @@ async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작
174
191
  tickRate(b.length);
175
192
  const s = b.toString("utf8");
176
193
  if (isYarn || isPnpm) {
177
- for (const ln of s.split(/\r?\n/)) {
178
- if (!ln.trim()) continue;
179
- handleJSONLine(ln);
180
- }
194
+ for (const ln of s.split(/\r?\n/)) if (ln.trim()) handleJSONLine(ln);
181
195
  } else {
182
196
  lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
183
197
  }
184
- render(lastMsg);
198
+ safeWrite(mkLine(lastMsg));
185
199
  });
186
200
  child.stderr.on("data", (b) => {
187
201
  downloadedBytes += b.length;
188
202
  tickRate(b.length);
189
203
  const s = b.toString("utf8");
190
204
  if (isYarn || isPnpm) {
191
- for (const ln of s.split(/\r?\n/)) {
192
- if (!ln.trim()) continue;
193
- if (ln.startsWith("{") && ln.endsWith("}")) handleJSONLine(ln);
194
- }
205
+ for (const ln of s.split(/\r?\n/)) if (ln.trim() && ln.startsWith("{") && ln.endsWith("}")) handleJSONLine(ln);
195
206
  } else {
196
207
  lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
197
208
  }
198
- render(lastMsg);
209
+ safeWrite(mkLine(lastMsg));
199
210
  });
200
211
 
201
- const iv = setInterval(() => render(lastMsg), 80);
212
+ // 주기적 업데이트 (출력 없을 때도 500ms마다만 갱신)
213
+ const iv = setInterval(() => safeWrite(mkLine(lastMsg)), 500);
202
214
 
203
215
  child.on("error", (e) => {
204
216
  clearInterval(iv);
205
- render("에러 발생");
206
- process.stdout.write("\x1B[?25h"); // 커서 보이기
217
+ finishWrite(false);
207
218
  reject(e);
208
219
  });
209
220
  child.on("close", (code) => {
210
221
  clearInterval(iv);
211
- percent = 1;
212
- render("완료");
213
- process.stdout.write("\n\x1B[?25h"); // 커서 보이기
214
- if (code === 0) resolve();
215
- else reject(Object.assign(new Error(`${cmd} exited ${code}`), { code }));
222
+ finishWrite(code === 0);
223
+ if (code === 0) resolve(); else reject(Object.assign(new Error(`${cmd} exited ${code}`), { code }));
216
224
  });
217
225
  });
226
+
227
+ // 커서 복구는 전역 핸들러가 처리
218
228
  }
219
229
 
230
+
220
231
  /** 경량 스피너 (applyScaffold 등 파일 생성용) */
221
232
  function makeSpinner(text = "") {
222
233
  const frames = process.platform === "win32" ? ["-", "\\", "|", "/"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vite-react-boot",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "Create a Vite + React + TS app with Tailwind, axios, AuthContext, login/register, routing.",
5
5
  "type": "module",
6
6
  "bin": {