create-vite-react-boot 1.0.20 → 1.0.21

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 +188 -369
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -3,335 +3,130 @@ 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";
10
9
 
11
- /*──────────────────────────────────────────────────────────────────────────────
12
- 기본 유틸
13
- ──────────────────────────────────────────────────────────────────────────────*/
14
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
11
  const cwd = process.cwd();
16
12
 
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
- /** 조용 실행(출력 숨김) */
13
+ /** 조용히 실행(로그 숨김) */
37
14
  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);
15
+ const child = spawn.sync(cmd, args, {
16
+ stdio: "pipe", // 출력 숨김
17
+ shell: false,
18
+ ...options,
19
+ });
20
+ const code = child.status ?? 0;
21
+ if (code !== 0) process.exit(code);
41
22
  }
42
- /** 로그 노출 실행 */
23
+
24
+ /** 로그를 보여주며 실행(서브프로세스 출력 노출) */
43
25
  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);
26
+ const child = spawn.sync(cmd, args, {
27
+ stdio: "inherit", // 출력 표시
28
+ shell: false,
29
+ ...options,
30
+ });
31
+ const code = child.status ?? 0;
32
+ if (code !== 0) process.exit(code);
47
33
  }
48
34
 
49
- /*──────────────────────────────────────────────────────────────────────────────
50
- 진행바/스피너 헬퍼
51
- ──────────────────────────────────────────────────────────────────────────────*/
52
- function formatHMS(s) {
53
- const m = Math.floor(s / 60).toString().padStart(2, "0");
54
- const ss = Math.floor(s % 60).toString().padStart(2, "0");
55
- return `${m}:${ss}`;
56
- }
57
- function termSize() {
58
- const cols = process.stdout.columns || 80;
59
- const rows = process.stdout.rows || 24;
60
- return { cols, rows };
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)}]`;
35
+ function detectPM() {
36
+ const ua = process.env.npm_config_user_agent || "";
37
+ if (ua.includes("pnpm")) return "pnpm";
38
+ if (ua.includes("yarn")) return "yarn";
39
+ return "npm";
67
40
  }
68
41
 
69
- /** 진행바 기반 실행 (yarn/pnpm은 이벤트 파싱, npm은 추정 진행) */
70
- async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작업") {
71
- const start = Date.now();
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";
76
-
77
- // 상태
78
- let percent = null; // 0..1 (yarn/pnpm 이벤트 기반)
79
- let downloadedBytes = 0; // 추정치(출력 길이 기반)
80
- let totalHint = null; // yarn/pnpm가 주면 총량 추정
81
- let lastTick = Date.now();
82
- let emaRate = 0; // byte/s
83
- const alpha = 0.15;
84
-
85
- // 렌더 throttle & dedupe
86
- let lastRender = 0;
87
- let lastPrinted = "";
88
-
89
- const mkLine = (brief = "") => {
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
-
99
- const eta = (() => {
100
- if (percent != null && percent > 0 && totalHint) {
101
- const remain = (1 - percent) * totalHint;
102
- const rate = emaRate > 1 ? emaRate : null;
103
- return rate ? formatHMS(remain / rate) : "--:--";
104
- }
105
- return emaRate > 50 ? formatHMS((totalHint ? Math.max(0, totalHint - downloadedBytes) : 0) / Math.max(1, emaRate)) : "--:--";
106
- })();
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;
119
- };
120
-
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());
146
-
147
- await new Promise((resolve, reject) => {
148
- let a = args.slice();
149
- const isYarn = /yarn/i.test(cmd);
150
- const isPnpm = /pnpm/i.test(cmd);
151
-
152
- if (isYarn && !a.includes("--json")) a.push("--json");
153
- if (isPnpm && !a.includes("--reporter")) a.push("--reporter", "ndjson");
154
-
155
- const child = spawn(cmd, a, {
156
- stdio: ["ignore", "pipe", "pipe"],
157
- cwd,
158
- env: { ...process.env, ...env },
159
- shell: false,
160
- });
161
-
162
- let lastMsg = "";
163
- const handleJSONLine = (line) => {
164
- try {
165
- const j = JSON.parse(line);
166
- if (j.type === "progressStart" && j.data && typeof j.data.total === "number") {
167
- totalHint = j.data.total;
168
- }
169
- if (j.type === "progressTick" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
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; // 대략 보정
173
- }
174
- if (j.type === "step" && j.data) lastMsg = String(j.data);
175
- if (j.msg) lastMsg = String(j.msg);
176
- } catch { /* ignore */ }
177
- };
178
-
179
- const tickRate = (chunkLen) => {
180
- const now = Date.now();
181
- const dt = (now - lastTick) / 1000;
182
- lastTick = now;
183
- if (dt > 0) {
184
- const inst = chunkLen / dt;
185
- emaRate = emaRate ? (emaRate * (1 - alpha) + inst * alpha) : inst;
186
- }
187
- };
188
-
189
- child.stdout.on("data", (b) => {
190
- downloadedBytes += b.length;
191
- tickRate(b.length);
192
- const s = b.toString("utf8");
193
- if (isYarn || isPnpm) {
194
- for (const ln of s.split(/\r?\n/)) if (ln.trim()) handleJSONLine(ln);
195
- } else {
196
- lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
197
- }
198
- safeWrite(mkLine(lastMsg));
199
- });
200
- child.stderr.on("data", (b) => {
201
- downloadedBytes += b.length;
202
- tickRate(b.length);
203
- const s = b.toString("utf8");
204
- if (isYarn || isPnpm) {
205
- for (const ln of s.split(/\r?\n/)) if (ln.trim() && ln.startsWith("{") && ln.endsWith("}")) handleJSONLine(ln);
206
- } else {
207
- lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
208
- }
209
- safeWrite(mkLine(lastMsg));
210
- });
211
-
212
- // 주기적 업데이트 (출력 없을 때도 500ms마다만 갱신)
213
- const iv = setInterval(() => safeWrite(mkLine(lastMsg)), 500);
214
-
215
- child.on("error", (e) => {
216
- clearInterval(iv);
217
- finishWrite(false);
218
- reject(e);
219
- });
220
- child.on("close", (code) => {
221
- clearInterval(iv);
222
- finishWrite(code === 0);
223
- if (code === 0) resolve(); else reject(Object.assign(new Error(`${cmd} exited ${code}`), { code }));
224
- });
225
- });
226
-
227
- // 커서 복구는 전역 핸들러가 처리
42
+ function ensureDir(dir) {
43
+ fs.mkdirSync(dir, { recursive: true });
228
44
  }
229
45
 
46
+ function write(p, content) {
47
+ ensureDir(path.dirname(p));
48
+ fs.writeFileSync(p, content);
49
+ }
230
50
 
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
- };
51
+ function writeJSON(p, obj) {
52
+ write(p, JSON.stringify(obj, null, 2) + "\n");
261
53
  }
262
54
 
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); });
55
+ function isEmptyDir(dir) {
56
+ return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0;
57
+ }
269
58
 
270
- /*──────────────────────────────────────────────────────────────────────────────
271
- 메인
272
- ──────────────────────────────────────────────────────────────────────────────*/
273
59
  (async function main() {
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
- }
294
-
295
- const target = path.resolve(cwd, appName);
296
- if (fs.existsSync(target) && !isEmptyDir(target)) {
297
- console.log(red(`✖ 대상 폴더가 비어있지 않습니다: ${target}`));
298
- process.exit(1);
299
- }
60
+ const argName = process.argv[2];
61
+
62
+ const { name } = await prompts(
63
+ [
64
+ {
65
+ type: argName ? null : "text",
66
+ name: "name",
67
+ message: "프로젝트 이름?",
68
+ initial: "myapp",
69
+ validate: (v) =>
70
+ !v || /[\\/:*?"<>|]/.test(v) ? "유효한 폴더명을 입력하세요." : true,
71
+ },
72
+ ],
73
+ { onCancel: () => process.exit(1) }
74
+ );
75
+
76
+ const appName = argName || name;
77
+ if (!appName) {
78
+ console.log(red("✖ 프로젝트 이름이 비어있습니다."));
79
+ process.exit(1);
80
+ }
300
81
 
301
- const pm = detectPM();
82
+ const target = path.resolve(cwd, appName);
302
83
 
303
- // 1) 기본 파일 생성
304
- console.log(cyan(`\n▶ Vite + React + TypeScript 기본 파일 생성…`));
305
- ensureDir(target);
84
+ // 같은 폴더에서 다시 실행 방지: 이미 뭔가 있으면 중단
85
+ if (fs.existsSync(target) && !isEmptyDir(target)) {
86
+ console.log(red(`✖ 대상 폴더가 비어있지 않습니다: ${target}`));
87
+ process.exit(1);
88
+ }
306
89
 
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);
90
+ const pm = detectPM();
91
+
92
+ // ─────────────────────────────────────────────
93
+ // 1) Vite + React + TS 기본 구조를 '직접' 생성 (무프롬프트)
94
+ // ─────────────────────────────────────────────
95
+ console.log(cyan(`\n▶ Vite + React + TypeScript 기본 파일 생성…`));
96
+ ensureDir(target);
97
+
98
+ // package.json
99
+ const pkgJson = {
100
+ name: appName,
101
+ private: true,
102
+ version: "0.0.0",
103
+ type: "module",
104
+ scripts: {
105
+ dev: "vite",
106
+ build: "tsc -b && vite build",
107
+ preview: "vite preview",
108
+ },
109
+ dependencies: {
110
+ react: "^18.3.1",
111
+ "react-dom": "^18.3.1",
112
+ "react-router-dom": "^6.26.1",
113
+ axios: "^1.7.7",
114
+ },
115
+ devDependencies: {
116
+ typescript: "^5.5.4",
117
+ vite: "^5.4.2",
118
+ "@vitejs/plugin-react": "^4.3.1",
119
+ tailwindcss: "^3.4.10",
120
+ postcss: "^8.4.47",
121
+ autoprefixer: "^10.4.20",
122
+ },
123
+ };
124
+ writeJSON(path.join(target, "package.json"), pkgJson);
333
125
 
334
- write(path.join(target, "tsconfig.json"), `{
126
+ // tsconfig
127
+ write(
128
+ path.join(target, "tsconfig.json"),
129
+ `{
335
130
  "compilerOptions": {
336
131
  "target": "ES2020",
337
132
  "useDefineForClassFields": true,
@@ -346,8 +141,11 @@ process.on("unhandledRejection", () => { showCursor(); process.exit(1); });
346
141
  },
347
142
  "include": ["src"]
348
143
  }
349
- `);
350
- write(path.join(target, "tsconfig.node.json"), `{
144
+ `
145
+ );
146
+ write(
147
+ path.join(target, "tsconfig.node.json"),
148
+ `{
351
149
  "compilerOptions": {
352
150
  "composite": true,
353
151
  "module": "ESNext",
@@ -356,18 +154,33 @@ process.on("unhandledRejection", () => { showCursor(); process.exit(1); });
356
154
  },
357
155
  "include": ["vite.config.ts"]
358
156
  }
359
- `);
360
- write(path.join(target, "vite.config.ts"), `import { defineConfig } from 'vite'
157
+ `
158
+ );
159
+
160
+ // vite.config.ts
161
+ write(
162
+ path.join(target, "vite.config.ts"),
163
+ `import { defineConfig } from 'vite'
361
164
  import react from '@vitejs/plugin-react'
362
165
  export default defineConfig({ plugins: [react()] })
363
- `);
364
- write(path.join(target, ".gitignore"), `node_modules
166
+ `
167
+ );
168
+
169
+ // .gitignore (Windows/Unix 공통)
170
+ write(
171
+ path.join(target, ".gitignore"),
172
+ `node_modules
365
173
  dist
366
174
  .cache
367
175
  .vscode
368
176
  .DS_Store
369
- `);
370
- write(path.join(target, "index.html"), `<!doctype html>
177
+ `
178
+ );
179
+
180
+ // index.html
181
+ write(
182
+ path.join(target, "index.html"),
183
+ `<!doctype html>
371
184
  <html lang="ko">
372
185
  <head>
373
186
  <meta charset="UTF-8" />
@@ -379,10 +192,18 @@ dist
379
192
  <script type="module" src="/src/main.tsx"></script>
380
193
  </body>
381
194
  </html>
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'
195
+ `
196
+ );
197
+
198
+ // Tailwind
199
+ write(
200
+ path.join(target, "postcss.config.js"),
201
+ `export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
202
+ `
203
+ );
204
+ write(
205
+ path.join(target, "tailwind.config.ts"),
206
+ `import type { Config } from 'tailwindcss'
386
207
  export default {
387
208
  content: ['./index.html', './src/**/*.{ts,tsx}'],
388
209
  theme: {
@@ -392,8 +213,13 @@ export default {
392
213
  },
393
214
  plugins: []
394
215
  } satisfies Config
395
- `);
396
- write(path.join(target, "src", "index.css"), `@tailwind base;
216
+ `
217
+ );
218
+
219
+ // src 엔트리
220
+ write(
221
+ path.join(target, "src", "index.css"),
222
+ `@tailwind base;
397
223
  @tailwind components;
398
224
  @tailwind utilities;
399
225
 
@@ -411,8 +237,11 @@ export default {
411
237
  .header{@apply sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-gray-200}
412
238
  .container{@apply max-w-5xl mx-auto px-4}
413
239
  }
414
- `);
415
- write(path.join(target, "src", "main.tsx"), `import React from 'react'
240
+ `
241
+ );
242
+ write(
243
+ path.join(target, "src", "main.tsx"),
244
+ `import React from 'react'
416
245
  import ReactDOM from 'react-dom/client'
417
246
  import { BrowserRouter } from 'react-router-dom'
418
247
  import App from './App'
@@ -428,8 +257,11 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
428
257
  </AuthProvider>
429
258
  </React.StrictMode>
430
259
  )
431
- `);
432
- write(path.join(target, "src", "App.tsx"), `import Header from './components/Header'
260
+ `
261
+ );
262
+ write(
263
+ path.join(target, "src", "App.tsx"),
264
+ `import Header from './components/Header'
433
265
  import RoutesView from './routes/index'
434
266
  export default function App(){
435
267
  return <>
@@ -439,53 +271,40 @@ export default function App(){
439
271
  </main>
440
272
  </>
441
273
  }
442
- `);
443
-
444
- // 2) 설치 (진행바 + TTY/ENV 폴백)
445
- console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
446
-
447
- const silentFlag =
448
- pm === "npm" ? "--silent" :
449
- pm === "yarn" ? "--silent" :
450
- pm === "pnpm" ? "--silent" : "";
451
-
452
- const isCI = process.env.CI === "true";
453
- const args =
454
- pm === "npm"
455
- ? [isCI ? "ci" : "install", ...(silentFlag ? [silentFlag] : [])]
456
- : ["install", ...(silentFlag ? [silentFlag] : [])];
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
- }
471
-
472
- console.log(green(" → 의존성 설치 완료!\n"));
473
-
474
- // 3) 추가 스캐폴딩 (경량 스피너)
475
- const sp = makeSpinner("스캐폴딩 적용");
476
- sp.start();
477
- try {
274
+ `
275
+ );
276
+
277
+ // ─────────────────────────────────────────────
278
+ // 2) 1회 설치 (조용히 설치: fund/audit 등 장황 로그 숨김)
279
+ // ─────────────────────────────────────────────
280
+ console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
281
+
282
+ // 패키지 매니저별 silent 옵션
283
+ const silentFlag =
284
+ pm === "npm" ? "--silent" :
285
+ pm === "yarn" ? "--silent" :
286
+ pm === "pnpm" ? "--silent" :
287
+ "";
288
+
289
+ runQuiet(pm, ["install", ...(silentFlag ? [silentFlag] : [])], { cwd: target });
290
+ console.log(green(" → 의존성 설치 완료!\n"));
291
+
292
+ // ─────────────────────────────────────────────
293
+ // 3) 인증/라우팅/axios/Tailwind 추가 파일 (파일만 생성, 설치 없음)
294
+ // ─────────────────────────────────────────────
295
+ console.log(yellow("▶ 인증/라우팅/Tailwind/axios 스캐폴딩 적용…"));
478
296
  await applyScaffold({ root: target });
479
- sp.succeed("스캐폴딩 적용");
480
- } catch (e) {
481
- sp.fail("스캐폴딩 실패");
482
- throw e;
483
- }
484
297
 
485
- // 4) dev 서버 실행
486
- console.log(green("\n✅ 프로젝트 준비 완료!\n"));
487
- console.log(cyan("▶ 개발 서버를 시작합니다…\n"));
298
+ // ─────────────────────────────────────────────
299
+ // 4) 자동 cd + dev 서버 실행
300
+ // ─────────────────────────────────────────────
301
+ console.log(green("\n✅ 프로젝트 준비 완료!\n"));
302
+ console.log(cyan("▶ 개발 서버를 시작합니다…\n"));
303
+
304
+ // 작업 디렉토리 전환 후 dev 실행
305
+ process.chdir(target);
488
306
 
489
- process.chdir(target);
490
- runPrint(pm, ["run", "dev"], { cwd: target });
307
+ // npm / yarn / pnpm 공통: pm run dev
308
+ runPrint(pm, ["run", "dev"], { cwd: target });
491
309
  })();
310
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vite-react-boot",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Create a Vite + React + TS app with Tailwind, axios, AuthContext, login/register, routing.",
5
5
  "type": "module",
6
6
  "bin": {