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.
- package/bin/cli.js +188 -369
- 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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
82
|
+
const target = path.resolve(cwd, appName);
|
|
302
83
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
84
|
+
// 같은 폴더에서 다시 실행 방지: 이미 뭔가 있으면 중단
|
|
85
|
+
if (fs.existsSync(target) && !isEmptyDir(target)) {
|
|
86
|
+
console.log(red(`✖ 대상 폴더가 비어있지 않습니다: ${target}`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
306
89
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
490
|
-
|
|
307
|
+
// npm / yarn / pnpm 공통: pm run dev
|
|
308
|
+
runPrint(pm, ["run", "dev"], { cwd: target });
|
|
491
309
|
})();
|
|
310
|
+
|