create-vite-react-boot 1.0.18 → 1.0.19

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