create-vite-react-boot 1.0.18 → 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 +275 -283
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -3,84 +3,148 @@ import { fileURLToPath } from "url";
3
3
  import path from "path";
4
4
  import fs from "fs";
5
5
  import prompts from "prompts";
6
- import spawn from "cross-spawn"; // 크로스플랫폼 스폰
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"; // ← 진행바 렌더링용
9
+ import readline from "node:readline";
10
10
 
11
+ /*──────────────────────────────────────────────────────────────────────────────
12
+ 기본 유틸
13
+ ──────────────────────────────────────────────────────────────────────────────*/
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const cwd = process.cwd();
16
+
17
+ function detectPM() {
18
+ const ua = process.env.npm_config_user_agent || "";
19
+ if (ua.includes("pnpm")) return "pnpm";
20
+ if (ua.includes("yarn")) return "yarn";
21
+ return "npm";
22
+ }
23
+ function ensureDir(dir) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ function write(p, content) {
27
+ ensureDir(path.dirname(p));
28
+ fs.writeFileSync(p, content);
29
+ }
30
+ function writeJSON(p, obj) {
31
+ write(p, JSON.stringify(obj, null, 2) + "\n");
32
+ }
33
+ function isEmptyDir(dir) {
34
+ return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0;
35
+ }
36
+ /** 조용 실행(출력 숨김) */
37
+ function runQuiet(cmd, args = [], options = {}) {
38
+ const child = spawn.sync(cmd, args, { stdio: "pipe", shell: false, ...options });
39
+ const code = child.status ?? 0;
40
+ if (code !== 0) process.exit(code);
41
+ }
42
+ /** 로그 노출 실행 */
43
+ function runPrint(cmd, args = [], options = {}) {
44
+ const child = spawn.sync(cmd, args, { stdio: "inherit", shell: false, ...options });
45
+ const code = child.status ?? 0;
46
+ if (code !== 0) process.exit(code);
47
+ }
11
48
 
49
+ /*──────────────────────────────────────────────────────────────────────────────
50
+ 진행바/스피너 헬퍼
51
+ ──────────────────────────────────────────────────────────────────────────────*/
12
52
  function formatHMS(s) {
13
53
  const m = Math.floor(s / 60).toString().padStart(2, "0");
14
54
  const ss = Math.floor(s % 60).toString().padStart(2, "0");
15
55
  return `${m}:${ss}`;
16
56
  }
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
57
  function termSize() {
28
58
  const cols = process.stdout.columns || 80;
29
59
  const rows = process.stdout.rows || 24;
30
60
  return { cols, rows };
31
61
  }
62
+ function barLine({ width, ratio }) {
63
+ const w = Math.max(10, width);
64
+ const r = Math.max(0, Math.min(1, ratio));
65
+ const filled = Math.round(w * r);
66
+ return `[${"#".repeat(filled)}${"-".repeat(w - filled)}]`;
67
+ }
32
68
 
69
+ /** 진행바 기반 실행 (yarn/pnpm은 이벤트 파싱, npm은 추정 진행) */
33
70
  async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작업") {
34
71
  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줄
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";
38
76
 
39
77
  // 상태
40
- let percent = null; // 0..1 (정확 모드에서만)
41
- let downloadedBytes = 0; // 추정치(출력 길이 기반, npm fallback)
42
- let totalHint = null; // yarn/pnpm가 알려주면 총량
78
+ let percent = null; // 0..1 (yarn/pnpm 이벤트 기반)
79
+ let downloadedBytes = 0; // 추정치(출력 길이 기반)
80
+ let totalHint = null; // yarn/pnpm가 주면 총량 추정
43
81
  let lastTick = Date.now();
44
- let emaRate = 0; // 지수이동평균(바이트/초), ETA 추정용
82
+ let emaRate = 0; // byte/s
45
83
  const alpha = 0.15;
46
84
 
47
- // 화면 세팅
48
- const startTop = process.stdout.rows ? (process.stdout.rows - 1) : 0;
49
- process.stdout.write("\x1B[?25l"); // 커서 숨김
50
- const header = () => {
85
+ // 렌더 throttle & dedupe
86
+ let lastRender = 0;
87
+ let lastPrinted = "";
88
+
89
+ const mkLine = (brief = "") => {
51
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
+
52
99
  const eta = (() => {
53
100
  if (percent != null && percent > 0 && totalHint) {
54
101
  const remain = (1 - percent) * totalHint;
55
102
  const rate = emaRate > 1 ? emaRate : null;
56
103
  return rate ? formatHMS(remain / rate) : "--:--";
57
104
  }
58
- if (emaRate > 100) {
59
- const remainBytes = totalHint ? (totalHint - downloadedBytes) : null;
60
- if (remainBytes && remainBytes > 0) return formatHMS(remainBytes / emaRate);
61
- }
62
- return "--:--";
105
+ return emaRate > 50 ? formatHMS((totalHint ? Math.max(0, totalHint - downloadedBytes) : 0) / Math.max(1, emaRate)) : "--:--";
63
106
  })();
64
- return `${label} [${formatHMS(elapsed)}] ${totalHint ? `${(downloadedBytes / 1048576).toFixed(1)} / ${(totalHint / 1048576).toFixed(1)} MiB` : `${(downloadedBytes / 1048576).toFixed(1)} MiB`} (eta: ${eta})`;
107
+
108
+ const sizeText = totalHint
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;
65
119
  };
66
120
 
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
- }
121
+ const safeWrite = (line) => {
122
+ // 200ms 이하/동일 라인일 땐 스킵
123
+ const now = Date.now();
124
+ if (now - lastRender < 200) return;
125
+ if (line === lastPrinted) return;
126
+
127
+ lastRender = now;
128
+ lastPrinted = line;
129
+
130
+ // 1줄 덮어쓰기
131
+ readline.clearLine(process.stdout, 0);
132
+ readline.cursorTo(process.stdout, 0);
133
+ process.stdout.write(line);
134
+ };
135
+
136
+ const finishWrite = (ok = true) => {
137
+ readline.clearLine(process.stdout, 0);
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());
80
146
 
81
- // 프로세스 실행
82
147
  await new Promise((resolve, reject) => {
83
- // yarn/pnpm에선 JSON/ndjson 모드로 이벤트 파싱을 노려본다
84
148
  let a = args.slice();
85
149
  const isYarn = /yarn/i.test(cmd);
86
150
  const isPnpm = /pnpm/i.test(cmd);
@@ -95,28 +159,21 @@ async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작
95
159
  shell: false,
96
160
  });
97
161
 
98
- let bufOut = "", bufErr = "";
99
162
  let lastMsg = "";
100
-
101
163
  const handleJSONLine = (line) => {
102
164
  try {
103
165
  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'이지만 총량 힌트로 사용
166
+ if (j.type === "progressStart" && j.data && typeof j.data.total === "number") {
167
+ totalHint = j.data.total;
108
168
  }
109
169
  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;
170
+ const p = j.data.current / j.data.total;
171
+ if (!Number.isNaN(p)) percent = Math.max(0, Math.min(1, p));
172
+ downloadedBytes += 64 * 1024; // 대략 보정
116
173
  }
117
- // pnpm ndjson: msg에 Progress 문구가 섞임
118
- if (j.msg) lastMsg = j.msg;
119
- } catch { /* ignore non-json */ }
174
+ if (j.type === "step" && j.data) lastMsg = String(j.data);
175
+ if (j.msg) lastMsg = String(j.msg);
176
+ } catch { /* ignore */ }
120
177
  };
121
178
 
122
179
  const tickRate = (chunkLen) => {
@@ -130,184 +187,151 @@ async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작
130
187
  };
131
188
 
132
189
  child.stdout.on("data", (b) => {
133
- const s = b.toString("utf8");
134
- bufOut += s;
135
- downloadedBytes += b.length; // npm fallback 시 “대략 MiB”로 사용
190
+ downloadedBytes += b.length;
136
191
  tickRate(b.length);
137
-
138
- // yarn/pnpm JSON 라인 파싱
192
+ const s = b.toString("utf8");
139
193
  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
- }
194
+ for (const ln of s.split(/\r?\n/)) if (ln.trim()) handleJSONLine(ln);
145
195
  } else {
146
196
  lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
147
197
  }
148
- render(lastMsg);
198
+ safeWrite(mkLine(lastMsg));
149
199
  });
150
200
  child.stderr.on("data", (b) => {
151
- const s = b.toString("utf8");
152
- bufErr += s;
153
201
  downloadedBytes += b.length;
154
202
  tickRate(b.length);
155
-
203
+ const s = b.toString("utf8");
156
204
  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
- }
205
+ for (const ln of s.split(/\r?\n/)) if (ln.trim() && ln.startsWith("{") && ln.endsWith("}")) handleJSONLine(ln);
163
206
  } else {
164
207
  lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
165
208
  }
166
- render(lastMsg);
209
+ safeWrite(mkLine(lastMsg));
167
210
  });
168
211
 
169
- // 주기적 리렌더 (출력이 잠시 멎어도 갱신)
170
- const iv = setInterval(() => render(lastMsg), 80);
212
+ // 주기적 업데이트 (출력 없을 때도 500ms마다만 갱신)
213
+ const iv = setInterval(() => safeWrite(mkLine(lastMsg)), 500);
171
214
 
172
215
  child.on("error", (e) => {
173
216
  clearInterval(iv);
174
- // 마지막 화면 정리
175
- render("에러 발생");
176
- process.stdout.write("\x1B[?25h");
217
+ finishWrite(false);
177
218
  reject(e);
178
219
  });
179
220
  child.on("close", (code) => {
180
221
  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 }));
222
+ finishWrite(code === 0);
223
+ if (code === 0) resolve(); else reject(Object.assign(new Error(`${cmd} exited ${code}`), { code }));
186
224
  });
187
225
  });
188
- }
189
-
190
226
 
191
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
192
- const cwd = process.cwd();
193
-
194
- /** 조용히 실행(로그 숨김) */
195
- function runQuiet(cmd, args = [], options = {}) {
196
- const child = spawn.sync(cmd, args, {
197
- stdio: "pipe", // ← 출력 숨김
198
- shell: false,
199
- ...options,
200
- });
201
- const code = child.status ?? 0;
202
- if (code !== 0) process.exit(code);
227
+ // 커서 복구는 전역 핸들러가 처리
203
228
  }
204
229
 
205
- /** 로그를 보여주며 실행(서브프로세스 출력 노출) */
206
- function runPrint(cmd, args = [], options = {}) {
207
- const child = spawn.sync(cmd, args, {
208
- stdio: "inherit", // ← 출력 표시
209
- shell: false,
210
- ...options,
211
- });
212
- const code = child.status ?? 0;
213
- if (code !== 0) process.exit(code);
214
- }
215
-
216
- function detectPM() {
217
- const ua = process.env.npm_config_user_agent || "";
218
- if (ua.includes("pnpm")) return "pnpm";
219
- if (ua.includes("yarn")) return "yarn";
220
- return "npm";
221
- }
222
-
223
- function ensureDir(dir) {
224
- fs.mkdirSync(dir, { recursive: true });
225
- }
226
-
227
- function write(p, content) {
228
- ensureDir(path.dirname(p));
229
- fs.writeFileSync(p, content);
230
- }
231
230
 
232
- function writeJSON(p, obj) {
233
- write(p, JSON.stringify(obj, null, 2) + "\n");
231
+ /** 경량 스피너 (applyScaffold 등 파일 생성용) */
232
+ function makeSpinner(text = "") {
233
+ const frames = process.platform === "win32" ? ["-", "\\", "|", "/"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
234
+ let i = 0, timer = null;
235
+ const startTime = Date.now();
236
+ return {
237
+ start(msg = text) {
238
+ process.stdout.write("\x1B[?25l");
239
+ timer = setInterval(() => {
240
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
241
+ readline.clearLine(process.stdout, 0);
242
+ readline.cursorTo(process.stdout, 0);
243
+ process.stdout.write(`${frames[i = (i + 1) % frames.length]} ${msg} ${bold(cyan(`${elapsed}s`))}`);
244
+ }, 80);
245
+ },
246
+ succeed(msg = text) {
247
+ if (timer) clearInterval(timer);
248
+ readline.clearLine(process.stdout, 0);
249
+ readline.cursorTo(process.stdout, 0);
250
+ process.stdout.write(`${green("✔")} ${msg}\n`);
251
+ process.stdout.write("\x1B[?25h");
252
+ },
253
+ fail(msg = text) {
254
+ if (timer) clearInterval(timer);
255
+ readline.clearLine(process.stdout, 0);
256
+ readline.cursorTo(process.stdout, 0);
257
+ process.stdout.write(`${red("✖")} ${msg}\n`);
258
+ process.stdout.write("\x1B[?25h");
259
+ }
260
+ };
234
261
  }
235
262
 
236
- function isEmptyDir(dir) {
237
- return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0;
238
- }
263
+ // 전역 커서 복구 (비정상 종료 포함)
264
+ const showCursor = () => { try { process.stdout.write("\x1B[?25h"); } catch { } };
265
+ process.on("exit", showCursor);
266
+ process.on("SIGINT", () => { showCursor(); process.exit(1); });
267
+ process.on("uncaughtException", () => { showCursor(); process.exit(1); });
268
+ process.on("unhandledRejection", () => { showCursor(); process.exit(1); });
239
269
 
270
+ /*──────────────────────────────────────────────────────────────────────────────
271
+ 메인
272
+ ──────────────────────────────────────────────────────────────────────────────*/
240
273
  (async function main() {
241
- const argName = process.argv[2];
242
-
243
- const { name } = await prompts(
244
- [
245
- {
246
- type: argName ? null : "text",
247
- name: "name",
248
- message: "프로젝트 이름?",
249
- initial: "myapp",
250
- validate: (v) =>
251
- !v || /[\\/:*?"<>|]/.test(v) ? "유효한 폴더명을 입력하세요." : true,
252
- },
253
- ],
254
- { onCancel: () => process.exit(1) }
255
- );
256
-
257
- const appName = argName || name;
258
- if (!appName) {
259
- console.log(red("✖ 프로젝트 이름이 비어있습니다."));
260
- process.exit(1);
261
- }
262
-
263
- const target = path.resolve(cwd, appName);
274
+ const argName = process.argv[2];
275
+
276
+ const { name } = await prompts(
277
+ [
278
+ {
279
+ type: argName ? null : "text",
280
+ name: "name",
281
+ message: "프로젝트 이름?",
282
+ initial: "myapp",
283
+ validate: (v) => !v || /[\\/:*?"<>|]/.test(v) ? "유효한 폴더명을 입력하세요." : true,
284
+ },
285
+ ],
286
+ { onCancel: () => process.exit(1) }
287
+ );
288
+
289
+ const appName = argName || name;
290
+ if (!appName) {
291
+ console.log(red("✖ 프로젝트 이름이 비어있습니다."));
292
+ process.exit(1);
293
+ }
264
294
 
265
- // 같은 폴더에서 다시 실행 방지: 이미 뭔가 있으면 중단
266
- if (fs.existsSync(target) && !isEmptyDir(target)) {
267
- console.log(red(`✖ 대상 폴더가 비어있지 않습니다: ${target}`));
268
- process.exit(1);
269
- }
295
+ const target = path.resolve(cwd, appName);
296
+ if (fs.existsSync(target) && !isEmptyDir(target)) {
297
+ console.log(red(`✖ 대상 폴더가 비어있지 않습니다: ${target}`));
298
+ process.exit(1);
299
+ }
270
300
 
271
- const pm = detectPM();
272
-
273
- // ─────────────────────────────────────────────
274
- // 1) Vite + React + TS 기본 구조를 '직접' 생성 (무프롬프트)
275
- // ─────────────────────────────────────────────
276
- console.log(cyan(`\n▶ Vite + React + TypeScript 기본 파일 생성…`));
277
- ensureDir(target);
278
-
279
- // package.json
280
- const pkgJson = {
281
- name: appName,
282
- private: true,
283
- version: "0.0.0",
284
- type: "module",
285
- scripts: {
286
- dev: "vite",
287
- build: "tsc -b && vite build",
288
- preview: "vite preview",
289
- },
290
- dependencies: {
291
- react: "^18.3.1",
292
- "react-dom": "^18.3.1",
293
- "react-router-dom": "^6.26.1",
294
- axios: "^1.7.7",
295
- },
296
- devDependencies: {
297
- typescript: "^5.5.4",
298
- vite: "^5.4.2",
299
- "@vitejs/plugin-react": "^4.3.1",
300
- tailwindcss: "^3.4.10",
301
- postcss: "^8.4.47",
302
- autoprefixer: "^10.4.20",
303
- },
304
- };
305
- writeJSON(path.join(target, "package.json"), pkgJson);
301
+ const pm = detectPM();
302
+
303
+ // 1) 기본 파일 생성
304
+ console.log(cyan(`\n▶ Vite + React + TypeScript 기본 파일 생성…`));
305
+ ensureDir(target);
306
+
307
+ const pkgJson = {
308
+ name: appName,
309
+ private: true,
310
+ version: "0.0.0",
311
+ type: "module",
312
+ scripts: {
313
+ dev: "vite",
314
+ build: "tsc -b && vite build",
315
+ preview: "vite preview",
316
+ },
317
+ dependencies: {
318
+ react: "^18.3.1",
319
+ "react-dom": "^18.3.1",
320
+ "react-router-dom": "^6.26.1",
321
+ axios: "^1.7.7",
322
+ },
323
+ devDependencies: {
324
+ typescript: "^5.5.4",
325
+ vite: "^5.4.2",
326
+ "@vitejs/plugin-react": "^4.3.1",
327
+ tailwindcss: "^3.4.10",
328
+ postcss: "^8.4.47",
329
+ autoprefixer: "^10.4.20",
330
+ },
331
+ };
332
+ writeJSON(path.join(target, "package.json"), pkgJson);
306
333
 
307
- // tsconfig
308
- write(
309
- path.join(target, "tsconfig.json"),
310
- `{
334
+ write(path.join(target, "tsconfig.json"), `{
311
335
  "compilerOptions": {
312
336
  "target": "ES2020",
313
337
  "useDefineForClassFields": true,
@@ -322,11 +346,8 @@ function isEmptyDir(dir) {
322
346
  },
323
347
  "include": ["src"]
324
348
  }
325
- `
326
- );
327
- write(
328
- path.join(target, "tsconfig.node.json"),
329
- `{
349
+ `);
350
+ write(path.join(target, "tsconfig.node.json"), `{
330
351
  "compilerOptions": {
331
352
  "composite": true,
332
353
  "module": "ESNext",
@@ -335,33 +356,18 @@ function isEmptyDir(dir) {
335
356
  },
336
357
  "include": ["vite.config.ts"]
337
358
  }
338
- `
339
- );
340
-
341
- // vite.config.ts
342
- write(
343
- path.join(target, "vite.config.ts"),
344
- `import { defineConfig } from 'vite'
359
+ `);
360
+ write(path.join(target, "vite.config.ts"), `import { defineConfig } from 'vite'
345
361
  import react from '@vitejs/plugin-react'
346
362
  export default defineConfig({ plugins: [react()] })
347
- `
348
- );
349
-
350
- // .gitignore (Windows/Unix 공통)
351
- write(
352
- path.join(target, ".gitignore"),
353
- `node_modules
363
+ `);
364
+ write(path.join(target, ".gitignore"), `node_modules
354
365
  dist
355
366
  .cache
356
367
  .vscode
357
368
  .DS_Store
358
- `
359
- );
360
-
361
- // index.html
362
- write(
363
- path.join(target, "index.html"),
364
- `<!doctype html>
369
+ `);
370
+ write(path.join(target, "index.html"), `<!doctype html>
365
371
  <html lang="ko">
366
372
  <head>
367
373
  <meta charset="UTF-8" />
@@ -373,18 +379,10 @@ dist
373
379
  <script type="module" src="/src/main.tsx"></script>
374
380
  </body>
375
381
  </html>
376
- `
377
- );
378
-
379
- // Tailwind
380
- write(
381
- path.join(target, "postcss.config.js"),
382
- `export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
383
- `
384
- );
385
- write(
386
- path.join(target, "tailwind.config.ts"),
387
- `import type { Config } from 'tailwindcss'
382
+ `);
383
+ write(path.join(target, "postcss.config.js"), `export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
384
+ `);
385
+ write(path.join(target, "tailwind.config.ts"), `import type { Config } from 'tailwindcss'
388
386
  export default {
389
387
  content: ['./index.html', './src/**/*.{ts,tsx}'],
390
388
  theme: {
@@ -394,13 +392,8 @@ export default {
394
392
  },
395
393
  plugins: []
396
394
  } satisfies Config
397
- `
398
- );
399
-
400
- // src 엔트리
401
- write(
402
- path.join(target, "src", "index.css"),
403
- `@tailwind base;
395
+ `);
396
+ write(path.join(target, "src", "index.css"), `@tailwind base;
404
397
  @tailwind components;
405
398
  @tailwind utilities;
406
399
 
@@ -418,11 +411,8 @@ export default {
418
411
  .header{@apply sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-gray-200}
419
412
  .container{@apply max-w-5xl mx-auto px-4}
420
413
  }
421
- `
422
- );
423
- write(
424
- path.join(target, "src", "main.tsx"),
425
- `import React from 'react'
414
+ `);
415
+ write(path.join(target, "src", "main.tsx"), `import React from 'react'
426
416
  import ReactDOM from 'react-dom/client'
427
417
  import { BrowserRouter } from 'react-router-dom'
428
418
  import App from './App'
@@ -438,11 +428,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
438
428
  </AuthProvider>
439
429
  </React.StrictMode>
440
430
  )
441
- `
442
- );
443
- write(
444
- path.join(target, "src", "App.tsx"),
445
- `import Header from './components/Header'
431
+ `);
432
+ write(path.join(target, "src", "App.tsx"), `import Header from './components/Header'
446
433
  import RoutesView from './routes/index'
447
434
  export default function App(){
448
435
  return <>
@@ -452,48 +439,53 @@ export default function App(){
452
439
  </main>
453
440
  </>
454
441
  }
455
- `
456
- );
442
+ `);
457
443
 
458
- // ─────────────────────────────────────────────
459
- // 2) 1회 설치 (조용히 설치: fund/audit 등 장황 로그 숨김)
460
- // ─────────────────────────────────────────────
444
+ // 2) 설치 (진행바 + TTY/ENV 폴백)
461
445
  console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
462
446
 
463
-
464
- // 패키지 매니저별 silent 옵션
465
447
  const silentFlag =
466
448
  pm === "npm" ? "--silent" :
467
449
  pm === "yarn" ? "--silent" :
468
450
  pm === "pnpm" ? "--silent" : "";
469
451
 
470
- // CI 환경이면 npm은 ci 사용, 아니면 install
471
452
  const isCI = process.env.CI === "true";
472
453
  const args =
473
454
  pm === "npm"
474
455
  ? [isCI ? "ci" : "install", ...(silentFlag ? [silentFlag] : [])]
475
456
  : ["install", ...(silentFlag ? [silentFlag] : [])];
476
-
477
-
478
- await runWithProgress(pm, args, { cwd: target }, "의존성 설치");
457
+
458
+ const envExtra =
459
+ pm === "npm"
460
+ ? { npm_config_loglevel: "error", npm_config_fund: "false", npm_config_audit: "false" }
461
+ : {};
462
+
463
+ const progressMode = process.env.PROGRESS; // 'plain' | 'force' | undefined
464
+ const wantPlain = progressMode === "plain" || (!process.stdout.isTTY && progressMode !== "force");
465
+
466
+ if (wantPlain) {
467
+ runQuiet(pm, args, { cwd: target, env: { ...process.env, ...envExtra } });
468
+ } else {
469
+ await runWithProgress(pm, args, { cwd: target, env: envExtra }, "의존성 설치");
470
+ }
479
471
 
480
472
  console.log(green(" → 의존성 설치 완료!\n"));
481
- // ─────────────────────────────────────────────
482
- // 3) 인증/라우팅/axios/Tailwind 추가 파일 (파일만 생성, 설치 없음)
483
- // ─────────────────────────────────────────────
484
- console.log(yellow("▶ 인증/라우팅/Tailwind/axios 스캐폴딩 적용…"));
485
- await applyScaffold({ root: target });
486
473
 
487
- // ─────────────────────────────────────────────
488
- // 4) 자동 cd + dev 서버 실행
489
- // ─────────────────────────────────────────────
490
- console.log(green("\n✅ 프로젝트 준비 완료!\n"));
491
- console.log(cyan("▶ 개발 서버를 시작합니다…\n"));
474
+ // 3) 추가 스캐폴딩 (경량 스피너)
475
+ const sp = makeSpinner("스캐폴딩 적용");
476
+ sp.start();
477
+ try {
478
+ await applyScaffold({ root: target });
479
+ sp.succeed("스캐폴딩 적용");
480
+ } catch (e) {
481
+ sp.fail("스캐폴딩 실패");
482
+ throw e;
483
+ }
492
484
 
493
- // 작업 디렉토리 전환 후 dev 실행
494
- process.chdir(target);
485
+ // 4) dev 서버 실행
486
+ console.log(green("\n✅ 프로젝트 준비 완료!\n"));
487
+ console.log(cyan("▶ 개발 서버를 시작합니다…\n"));
495
488
 
496
- // npm / yarn / pnpm 공통: pm run dev
497
- runPrint(pm, ["run", "dev"], { cwd: target });
489
+ process.chdir(target);
490
+ runPrint(pm, ["run", "dev"], { cwd: target });
498
491
  })();
499
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vite-react-boot",
3
- "version": "1.0.18",
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": {