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.
- package/bin/cli.js +233 -252
- 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; // 추정치(출력 길이
|
|
77
|
+
let downloadedBytes = 0; // 추정치(출력 길이 기반)
|
|
42
78
|
let totalHint = null; // yarn/pnpm가 알려주면 총량
|
|
43
79
|
let lastTick = Date.now();
|
|
44
|
-
let emaRate = 0; // 지수이동평균(바이트/초)
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
158
|
-
for (const ln of lines) {
|
|
191
|
+
for (const ln of s.split(/\r?\n/)) {
|
|
159
192
|
if (!ln.trim()) continue;
|
|
160
|
-
|
|
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
|
|
215
|
+
else reject(Object.assign(new Error(`${cmd} exited ${code}`), { code }));
|
|
186
216
|
});
|
|
187
217
|
});
|
|
188
218
|
}
|
|
189
219
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
494
|
-
|
|
474
|
+
// 4) dev 서버 실행
|
|
475
|
+
console.log(green("\n✅ 프로젝트 준비 완료!\n"));
|
|
476
|
+
console.log(cyan("▶ 개발 서버를 시작합니다…\n"));
|
|
495
477
|
|
|
496
|
-
|
|
497
|
-
|
|
478
|
+
process.chdir(target);
|
|
479
|
+
runPrint(pm, ["run", "dev"], { cwd: target });
|
|
498
480
|
})();
|
|
499
|
-
|