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.
- package/bin/cli.js +275 -283
- 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
|
|
36
|
-
|
|
37
|
-
|
|
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; // 추정치(출력 길이
|
|
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; //
|
|
82
|
+
let emaRate = 0; // byte/s
|
|
45
83
|
const alpha = 0.15;
|
|
46
84
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
118
|
-
if (j.msg) lastMsg = j.msg;
|
|
119
|
-
} catch { /* ignore
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
209
|
+
safeWrite(mkLine(lastMsg));
|
|
167
210
|
});
|
|
168
211
|
|
|
169
|
-
// 주기적
|
|
170
|
-
const iv = setInterval(() =>
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
494
|
-
|
|
485
|
+
// 4) dev 서버 실행
|
|
486
|
+
console.log(green("\n✅ 프로젝트 준비 완료!\n"));
|
|
487
|
+
console.log(cyan("▶ 개발 서버를 시작합니다…\n"));
|
|
495
488
|
|
|
496
|
-
|
|
497
|
-
|
|
489
|
+
process.chdir(target);
|
|
490
|
+
runPrint(pm, ["run", "dev"], { cwd: target });
|
|
498
491
|
})();
|
|
499
|
-
|