create-vite-react-boot 1.0.15 → 1.0.17

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 (3) hide show
  1. package/bin/cli.js +183 -1
  2. package/lib/apply.js +169 -5
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -7,6 +7,187 @@ import spawn from "cross-spawn"; // 크로스플랫폼 스폰
7
7
  import { green, yellow, red, cyan, bold } from "kolorist";
8
8
  import { applyScaffold } from "../lib/apply.js";
9
9
 
10
+ import readline from "node:readline";
11
+
12
+ function formatHMS(s) {
13
+ const m = Math.floor(s / 60).toString().padStart(2, "0");
14
+ const ss = Math.floor(s % 60).toString().padStart(2, "0");
15
+ return `${m}:${ss}`;
16
+ }
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
+ function termSize() {
28
+ const cols = process.stdout.columns || 80;
29
+ const rows = process.stdout.rows || 24;
30
+ return { cols, rows };
31
+ }
32
+
33
+ async function runWithProgress(cmd, args = [], { cwd, env } = {}, label = "작업") {
34
+ const start = Date.now();
35
+ const { cols } = termSize();
36
+ const barWidth = Math.min(40, Math.max(20, Math.floor(cols * 0.35)));
37
+ let linesReserved = 3; // 헤더 + 메인바 + 로그요약 1줄
38
+
39
+ // 상태
40
+ let percent = null; // 0..1 (정확 모드에서만)
41
+ let downloadedBytes = 0; // 추정치(출력 길이 기반, npm fallback)
42
+ let totalHint = null; // yarn/pnpm가 알려주면 총량
43
+ let lastTick = Date.now();
44
+ let emaRate = 0; // 지수이동평균(바이트/초), ETA 추정용
45
+ const alpha = 0.15;
46
+
47
+ // 화면 세팅
48
+ const startTop = process.stdout.rows ? (process.stdout.rows - 1) : 0;
49
+ process.stdout.write("\x1B[?25l"); // 커서 숨김
50
+ const header = () => {
51
+ const elapsed = (Date.now() - start) / 1000;
52
+ const eta = (() => {
53
+ if (percent != null && percent > 0 && totalHint) {
54
+ const remain = (1 - percent) * totalHint;
55
+ const rate = emaRate > 1 ? emaRate : null;
56
+ return rate ? formatHMS(remain / rate) : "--:--";
57
+ }
58
+ if (emaRate > 100) {
59
+ const remainBytes = totalHint ? (totalHint - downloadedBytes) : null;
60
+ if (remainBytes && remainBytes > 0) return formatHMS(remainBytes / emaRate);
61
+ }
62
+ return "--:--";
63
+ })();
64
+ return `${label} [${formatHMS(elapsed)}] ${totalHint ? `${(downloadedBytes / 1048576).toFixed(1)} / ${(totalHint / 1048576).toFixed(1)} MiB` : `${(downloadedBytes / 1048576).toFixed(1)} MiB`} (eta: ${eta})`;
65
+ };
66
+
67
+ // 렌더러
68
+ function render(lastMsg = "") {
69
+ const elapsed = (Date.now() - start) / 1000;
70
+ const ratio = percent != null ? percent : Math.min(0.95, Math.max(0.1, 1 - 1 / Math.max(2, elapsed / 3)));
71
+ const line0 = header();
72
+ const line1 = `${barLine({ width: barWidth, ratio })}`;
73
+ const line2 = lastMsg ? `… ${lastMsg.slice(0, cols - 4)}` : "";
74
+
75
+ // 위에서 3줄 확보해 덮어쓰기
76
+ writeLine(0, line0.padEnd(cols));
77
+ writeLine(1, line1.padEnd(cols));
78
+ writeLine(2, line2.padEnd(cols));
79
+ }
80
+
81
+ // 프로세스 실행
82
+ await new Promise((resolve, reject) => {
83
+ // yarn/pnpm에선 JSON/ndjson 모드로 이벤트 파싱을 노려본다
84
+ let a = args.slice();
85
+ const isYarn = /yarn/i.test(cmd);
86
+ const isPnpm = /pnpm/i.test(cmd);
87
+
88
+ if (isYarn && !a.includes("--json")) a.push("--json");
89
+ if (isPnpm && !a.includes("--reporter")) a.push("--reporter", "ndjson");
90
+
91
+ const child = spawn(cmd, a, {
92
+ stdio: ["ignore", "pipe", "pipe"],
93
+ cwd,
94
+ env: { ...process.env, ...env },
95
+ shell: false,
96
+ });
97
+
98
+ let bufOut = "", bufErr = "";
99
+ let lastMsg = "";
100
+
101
+ const handleJSONLine = (line) => {
102
+ try {
103
+ const j = JSON.parse(line);
104
+ // yarn classic: progressStart/progressTick/progressFinish
105
+ if (j.type === "progressStart" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
106
+ const total = j.data.total;
107
+ totalHint = total; // 단위는 'tick'이지만 총량 힌트로 사용
108
+ }
109
+ if (j.type === "progressTick" && j.data && typeof j.data.current === "number" && typeof j.data.total === "number") {
110
+ percent = Math.max(0, Math.min(1, j.data.current / j.data.total));
111
+ // tick 기반으로도 바이트 추정치 보정
112
+ downloadedBytes += 64 * 1024; // 대략치
113
+ }
114
+ if (j.type === "step" && j.data) {
115
+ lastMsg = j.data;
116
+ }
117
+ // pnpm ndjson: msg에 Progress 문구가 섞임
118
+ if (j.msg) lastMsg = j.msg;
119
+ } catch { /* ignore non-json */ }
120
+ };
121
+
122
+ const tickRate = (chunkLen) => {
123
+ const now = Date.now();
124
+ const dt = (now - lastTick) / 1000;
125
+ lastTick = now;
126
+ if (dt > 0) {
127
+ const inst = chunkLen / dt;
128
+ emaRate = emaRate ? (emaRate * (1 - alpha) + inst * alpha) : inst;
129
+ }
130
+ };
131
+
132
+ child.stdout.on("data", (b) => {
133
+ const s = b.toString("utf8");
134
+ bufOut += s;
135
+ downloadedBytes += b.length; // npm fallback 시 “대략 MiB”로 사용
136
+ tickRate(b.length);
137
+
138
+ // yarn/pnpm JSON 라인 파싱
139
+ if (isYarn || isPnpm) {
140
+ const lines = s.split(/\r?\n/);
141
+ for (const ln of lines) {
142
+ if (!ln.trim()) continue;
143
+ handleJSONLine(ln);
144
+ }
145
+ } else {
146
+ lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
147
+ }
148
+ render(lastMsg);
149
+ });
150
+ child.stderr.on("data", (b) => {
151
+ const s = b.toString("utf8");
152
+ bufErr += s;
153
+ downloadedBytes += b.length;
154
+ tickRate(b.length);
155
+
156
+ if (isYarn || isPnpm) {
157
+ const lines = s.split(/\r?\n/);
158
+ for (const ln of lines) {
159
+ if (!ln.trim()) continue;
160
+ // 일부 PM은 stderr에도 json이 섞여온다
161
+ if ((ln.startsWith("{") && ln.endsWith("}"))) handleJSONLine(ln);
162
+ }
163
+ } else {
164
+ lastMsg = s.trim().split(/\r?\n/).pop() || lastMsg;
165
+ }
166
+ render(lastMsg);
167
+ });
168
+
169
+ // 주기적 리렌더 (출력이 잠시 멎어도 바 갱신)
170
+ const iv = setInterval(() => render(lastMsg), 80);
171
+
172
+ child.on("error", (e) => {
173
+ clearInterval(iv);
174
+ // 마지막 화면 정리
175
+ render("에러 발생");
176
+ process.stdout.write("\x1B[?25h");
177
+ reject(e);
178
+ });
179
+ child.on("close", (code) => {
180
+ clearInterval(iv);
181
+ percent = 1;
182
+ render("완료");
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 }));
186
+ });
187
+ });
188
+ }
189
+
190
+
10
191
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
192
  const cwd = process.cwd();
12
193
 
@@ -277,7 +458,8 @@ export default function App(){
277
458
  // ─────────────────────────────────────────────
278
459
  // 2) 1회 설치 (조용히 설치: fund/audit 등 장황 로그 숨김)
279
460
  // ─────────────────────────────────────────────
280
- console.log(yellow(`\n▶ 의존성 설치 중입니다. 잠시만 기다려주세요…\n`));
461
+ await runWithProgress(pm, args, { cwd: target }, "의존성 설치");
462
+
281
463
 
282
464
  // 패키지 매니저별 silent 옵션
283
465
  const silentFlag =
package/lib/apply.js CHANGED
@@ -94,11 +94,175 @@ export default function ProtectedRoute({children}:{children:ReactNode}){
94
94
  return <>{children}</>
95
95
  }
96
96
  `,
97
- home: `export default function Home(){
98
- return <section className="card">
99
- <h2 className="text-xl font-semibold">홈</h2>
100
- <p className="mt-2 text-muted">Tailwind + axios + 로그인/회원가입/보호 라우트 기본 제공</p>
101
- </section>
97
+ home: `import { useState } from "react";
98
+
99
+ function CodeBlock({ children }: { children: string }) {
100
+ const [copied, setCopied] = useState(false);
101
+ return (
102
+ <div className="relative">
103
+ <pre className="mt-2 p-3 bg-gradient-to-br from-blue-50 to-teal-50 border border-pri/30 rounded-lg overflow-x-auto text-sm">
104
+ {children}
105
+ </pre>
106
+ <button
107
+ className="absolute top-2 right-2 rounded-md border border-pri/40 px-2.5 py-1 text-xs text-white bg-pri hover:opacity-90 shadow-sm"
108
+ onClick={async () => {
109
+ await navigator.clipboard.writeText(children);
110
+ setCopied(true);
111
+ setTimeout(() => setCopied(false), 1200);
112
+ }}
113
+ aria-label="copy"
114
+ title="Copy"
115
+ >
116
+ {copied ? "Copied" : "Copy"}
117
+ </button>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ function Pill({ label, tone = "blue" }: { label: string; tone?: "blue" | "emerald" | "indigo" | "violet" | "cyan" }) {
123
+ const toneMap: Record<string, string> = {
124
+ blue: "bg-blue-50 text-blue-700 border-blue-200",
125
+ emerald: "bg-emerald-50 text-emerald-700 border-emerald-200",
126
+ indigo: "bg-indigo-50 text-indigo-700 border-indigo-200",
127
+ violet: "bg-violet-50 text-violet-700 border-violet-200",
128
+ cyan: "bg-cyan-50 text-cyan-700 border-cyan-200",
129
+ };
130
+ return (
131
+ <span className={\`inline-flex items-center rounded-full px-2.5 py-1 text-xs border \${toneMap[tone]} whitespace-nowrap\`}>
132
+ {label}
133
+ </span>
134
+ );
135
+ }
136
+
137
+ export default function Home() {
138
+ return (
139
+ <section className="space-y-7">
140
+ {/* HERO */}
141
+ <div className="relative overflow-hidden rounded-2xl border border-pri/20">
142
+ <div
143
+ className="absolute inset-0"
144
+ style={{
145
+ background:
146
+ "radial-gradient(700px 300px at -10% 0%, rgba(37,99,235,0.22), transparent 60%), radial-gradient(700px 300px at 110% 10%, rgba(16,185,129,0.20), transparent 60%), linear-gradient(180deg, rgba(255,255,255,0.9), rgba(255,255,255,0.85))",
147
+ }}
148
+ />
149
+ <div className="relative p-6 md:p-9">
150
+ <div className="flex flex-wrap gap-2">
151
+ <Pill label="Vite" tone="blue" />
152
+ <Pill label="React 18" tone="indigo" />
153
+ <Pill label="TypeScript" tone="violet" />
154
+ <Pill label="Tailwind" tone="emerald" />
155
+ <Pill label="React Router" tone="cyan" />
156
+ </div>
157
+ <h1 className="mt-4 text-2xl md:text-3xl font-semibold tracking-tight text-pri">
158
+ create-vite-react-boot
159
+ </h1>
160
+ <p className="text-muted mt-2 max-w-2xl">
161
+ Vite + React + TypeScript + Tailwind 기반 스타터입니다. 로그인/회원가입은
162
+ <strong> localStorage</strong>만 사용하며 서버는 없습니다. 생성·설치·스캐폴딩·개발 서버 실행까지 한 번에 처리합니다.
163
+ </p>
164
+ <div className="mt-5 flex gap-10 text-sm">
165
+ <a href="/login" className="btn btn-primary">로그인</a>
166
+ <a href="/about" className="btn">About</a>
167
+ </div>
168
+ </div>
169
+ <div className="h-1 bg-gradient-to-r from-blue-500 via-cyan-400 to-emerald-500" />
170
+ </div>
171
+
172
+ {/* 사용자 안내 */}
173
+ <div className="card border-l-4 border-l-emerald-400">
174
+ <h2 className="text-lg font-semibold text-emerald-700">사용자 안내</h2>
175
+ <ul className="list-disc pl-5 mt-2 space-y-1 text-sm">
176
+ <li>계정 정보는 <code>localStorage</code>에 저장됩니다 (예: <code>session_v1</code>).</li>
177
+ <li>암호화/서버 검증이 없으므로 민감한 정보는 입력하지 마세요.</li>
178
+ <li>같은 브라우저에서만 유지됩니다. 시크릿 모드/브라우저 변경 시 사라질 수 있습니다.</li>
179
+ <li>초기화: 개발자 도구 → Application/저장소 → <code>localStorage</code> 키 삭제.</li>
180
+ </ul>
181
+ </div>
182
+
183
+ {/* 설치 & 실행 */}
184
+ <div className="card border-l-4 border-l-blue-500">
185
+ <h2 className="text-lg font-semibold text-blue-700">설치 & 실행</h2>
186
+
187
+ <div className="mt-3">
188
+ <div className="text-sm font-medium text-ink">1) 프로젝트 생성 (자동 설치/실행)</div>
189
+ <CodeBlock>{\`# npm
190
+ npm create vite-react-boot@latest
191
+
192
+ # pnpm
193
+ pnpm dlx create-vite-react-boot@latest
194
+
195
+ # yarn
196
+ yarn create vite-react-boot\`}</CodeBlock>
197
+ <p className="text-xs text-muted mt-2">
198
+ 프로젝트 이름 입력 후 설치/스캐폴딩이 완료되면 개발 서버가 자동 시작됩니다.
199
+ </p>
200
+ </div>
201
+
202
+ <div className="mt-4">
203
+ <div className="text-sm font-medium text-ink">2) 자동 실행이 안 된 경우</div>
204
+ <CodeBlock>{\`cd <프로젝트명>
205
+ npm run dev # 또는 pnpm dev / yarn dev\`}</CodeBlock>
206
+ </div>
207
+
208
+ <div className="mt-4">
209
+ <div className="text-sm font-medium text-ink">3) 수동 설치가 필요한 경우</div>
210
+ <CodeBlock>{\`cd <프로젝트명>
211
+ npm install # 또는 pnpm install / yarn install
212
+ npm run dev\`}</CodeBlock>
213
+ </div>
214
+
215
+ <div className="mt-4">
216
+ <div className="text-sm font-medium text-ink">4) 빌드 & 프리뷰</div>
217
+ <CodeBlock>{\`npm run build
218
+ npm run preview\`}</CodeBlock>
219
+ </div>
220
+ </div>
221
+
222
+ {/* 요약 */}
223
+ <div className="grid md:grid-cols-3 gap-4">
224
+ <div className="card border-t-2 border-t-blue-400">
225
+ <h3 className="font-medium text-blue-700">기능 요약</h3>
226
+ <ul className="list-disc pl-5 mt-2 space-y-1 text-sm">
227
+ <li>생성 → 설치(조용히) → 스캐폴딩 → 자동 실행</li>
228
+ <li>홈/로그인/About 기본 라우트</li>
229
+ <li>localStorage 세션 유지</li>
230
+ <li>기본 UI 유틸 클래스(.card, .btn, .input)</li>
231
+ </ul>
232
+ </div>
233
+
234
+ <div className="card border-t-2 border-t-violet-400">
235
+ <h3 className="font-medium text-violet-700">라우트</h3>
236
+ <ul className="list-disc pl-5 mt-2 space-y-1 text-sm">
237
+ <li><code>/</code> : 홈</li>
238
+ <li><code>/login</code> : 로그인</li>
239
+ <li><code>/about</code> : 소개</li>
240
+ </ul>
241
+ <p className="text-xs text-muted mt-2">
242
+ 로그인 실패 시 서버 메시지는 숨기고 기본 안내 문구만 표시합니다.
243
+ </p>
244
+ </div>
245
+
246
+ <div className="card border-t-2 border-t-emerald-400">
247
+ <h3 className="font-medium text-emerald-700">스크립트 & 스택</h3>
248
+ <div className="mt-2 text-sm">
249
+ <div className="mb-2">
250
+ <div className="text-muted">scripts</div>
251
+ <pre className="mt-1 p-2 bg-emerald-50/60 border border-emerald-200 rounded text-xs overflow-x-auto">{\`"dev": "vite",
252
+ "build": "tsc -b && vite build",
253
+ "preview": "vite preview"\`}</pre>
254
+ </div>
255
+ <div>
256
+ <div className="text-muted">stack</div>
257
+ <p className="mt-1 text-sm text-muted">
258
+ Vite · React 18 · TypeScript · Tailwind · React Router
259
+ </p>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </section>
265
+ );
102
266
  }
103
267
  `,
104
268
  login: `import { FormEvent, useState } from 'react'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vite-react-boot",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Create a Vite + React + TS app with Tailwind, axios, AuthContext, login/register, routing.",
5
5
  "type": "module",
6
6
  "bin": {