gencow 0.1.76 → 0.1.78
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/lib/__tests__/deploy-auditor.test.ts +125 -0
- package/lib/__tests__/env-parser.test.ts +134 -0
- package/lib/__tests__/project-validator.test.ts +119 -0
- package/lib/__tests__/readme-codegen.test.ts +439 -0
- package/lib/deploy-auditor.mjs +227 -0
- package/lib/env-parser.mjs +82 -0
- package/lib/project-validator.mjs +89 -0
- package/lib/readme-codegen.mjs +510 -0
- package/package.json +3 -2
- package/scripts/pre-publish-check.mjs +118 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildReadmeMarkdown,
|
|
4
|
+
buildApiTable,
|
|
5
|
+
buildReactUsage,
|
|
6
|
+
buildFrontendSetup,
|
|
7
|
+
buildAuthSection,
|
|
8
|
+
buildRpcSection,
|
|
9
|
+
buildAiPrompt,
|
|
10
|
+
buildAiUsageSection,
|
|
11
|
+
buildDeploySection,
|
|
12
|
+
buildCronSection,
|
|
13
|
+
buildDevTips,
|
|
14
|
+
extractComponentsBlock,
|
|
15
|
+
COMP_START,
|
|
16
|
+
COMP_END,
|
|
17
|
+
} from "../readme-codegen.mjs";
|
|
18
|
+
|
|
19
|
+
// ─── Sample API objects for testing ──────────────────────
|
|
20
|
+
const SIMPLE_API = {
|
|
21
|
+
tasks: { queries: ["list"], mutations: ["create", "delete"] },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const MULTI_NS_API = {
|
|
25
|
+
tasks: { queries: ["list", "getById"], mutations: ["create"] },
|
|
26
|
+
users: { queries: ["me"], mutations: ["updateProfile"] },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const EMPTY_API = {};
|
|
30
|
+
|
|
31
|
+
const QUERY_ONLY_API = {
|
|
32
|
+
products: { queries: ["list", "search"], mutations: [] },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const MUTATION_ONLY_API = {
|
|
36
|
+
orders: { queries: [], mutations: ["place", "cancel"] },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════════════════
|
|
40
|
+
describe("buildReadmeMarkdown — 전체 생성", () => {
|
|
41
|
+
it("헤더에 타이틀과 타임스탬프 포함", () => {
|
|
42
|
+
const md = buildReadmeMarkdown(SIMPLE_API, { timestamp: "2026-03-26" });
|
|
43
|
+
expect(md).toContain("# Gencow API Guide");
|
|
44
|
+
expect(md).toContain("2026-03-26");
|
|
45
|
+
expect(md).toContain("Auto-generated");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("빈 API 객체도 에러 없이 생성", () => {
|
|
49
|
+
const md = buildReadmeMarkdown(EMPTY_API, { timestamp: "test" });
|
|
50
|
+
expect(md).toContain("# Gencow API Guide");
|
|
51
|
+
expect(md).toContain("Available APIs");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("기존 컴포넌트 블록 보존", () => {
|
|
55
|
+
const compBlock = `${COMP_START}\n### My Component\n${COMP_END}`;
|
|
56
|
+
const md = buildReadmeMarkdown(SIMPLE_API, {
|
|
57
|
+
timestamp: "test",
|
|
58
|
+
existingComponentsBlock: compBlock,
|
|
59
|
+
});
|
|
60
|
+
expect(md).toContain(COMP_START);
|
|
61
|
+
expect(md).toContain("My Component");
|
|
62
|
+
expect(md).toContain(COMP_END);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("모든 필수 섹션 포함", () => {
|
|
66
|
+
const md = buildReadmeMarkdown(SIMPLE_API, { timestamp: "test" });
|
|
67
|
+
expect(md).toContain("## 📦 Available APIs");
|
|
68
|
+
expect(md).toContain("## ⚡ 데이터 사용법");
|
|
69
|
+
expect(md).toContain("## 🏗️ 프론트엔드 초기 설정");
|
|
70
|
+
expect(md).toContain("## 🔐 인증");
|
|
71
|
+
expect(md).toContain("## 🤖 AI Vibe-Coding Prompt");
|
|
72
|
+
expect(md).toContain("## 🤖 AI 사용법");
|
|
73
|
+
expect(md).toContain("## 🚀 배포");
|
|
74
|
+
expect(md).toContain("## ⏰ Cron Jobs");
|
|
75
|
+
expect(md).toContain("## 💡 개발 팁");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ═══════════════════════════════════════════════════════════
|
|
80
|
+
describe("buildApiTable — API 레퍼런스 테이블", () => {
|
|
81
|
+
it("네임스페이스별 테이블 생성", () => {
|
|
82
|
+
const md = buildApiTable(SIMPLE_API);
|
|
83
|
+
expect(md).toContain("### `tasks`");
|
|
84
|
+
expect(md).toContain("api.tasks.list");
|
|
85
|
+
expect(md).toContain("api.tasks.create");
|
|
86
|
+
expect(md).toContain("api.tasks.delete");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("query는 query 타입, mutation은 mutation 타입", () => {
|
|
90
|
+
const md = buildApiTable(SIMPLE_API);
|
|
91
|
+
expect(md).toContain("| `api.tasks.list` | `query`");
|
|
92
|
+
expect(md).toContain("| `api.tasks.create` | `mutation`");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("여러 네임스페이스 처리", () => {
|
|
96
|
+
const md = buildApiTable(MULTI_NS_API);
|
|
97
|
+
expect(md).toContain("### `tasks`");
|
|
98
|
+
expect(md).toContain("### `users`");
|
|
99
|
+
expect(md).toContain("api.users.me");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("빈 API → 섹션 헤더만 존재", () => {
|
|
103
|
+
const md = buildApiTable(EMPTY_API);
|
|
104
|
+
expect(md).toContain("## 📦 Available APIs");
|
|
105
|
+
expect(md).not.toContain("###");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ═══════════════════════════════════════════════════════════
|
|
110
|
+
describe("buildReactUsage — React Hook 사용법", () => {
|
|
111
|
+
it("useQuery/useMutation import 포함", () => {
|
|
112
|
+
const md = buildReactUsage(SIMPLE_API);
|
|
113
|
+
expect(md).toContain("import { useQuery, useMutation }");
|
|
114
|
+
expect(md).toContain("@gencow/react");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("쿼리용 useQuery 예제 생성", () => {
|
|
118
|
+
const md = buildReactUsage(SIMPLE_API);
|
|
119
|
+
expect(md).toContain("useQuery(api.tasks.list)");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("뮤테이션용 useMutation 예제 생성", () => {
|
|
123
|
+
const md = buildReactUsage(SIMPLE_API);
|
|
124
|
+
expect(md).toContain("useMutation(api.tasks.create)");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("네임스페이스 대문자화 (capitalize)", () => {
|
|
128
|
+
const md = buildReactUsage(SIMPLE_API);
|
|
129
|
+
expect(md).toContain("Tasks"); // capitalize('tasks')
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("query만 있는 네임스페이스", () => {
|
|
133
|
+
const md = buildReactUsage(QUERY_ONLY_API);
|
|
134
|
+
expect(md).toContain("useQuery(api.products.list)");
|
|
135
|
+
expect(md).not.toContain("useMutation(api.products");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("mutation만 있는 네임스페이스", () => {
|
|
139
|
+
const md = buildReactUsage(MUTATION_ONLY_API);
|
|
140
|
+
expect(md).not.toContain("useQuery(api.orders");
|
|
141
|
+
expect(md).toContain("useMutation(api.orders.place)");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("안티패턴 경고 포함", () => {
|
|
145
|
+
const md = buildReactUsage(SIMPLE_API);
|
|
146
|
+
expect(md).toContain("❌ 절대 하지 마세요");
|
|
147
|
+
expect(md).toContain("fetch()로 직접 API 호출하지 마세요");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("조건부 쿼리 (skip/enabled) 포함", () => {
|
|
151
|
+
const md = buildReactUsage(SIMPLE_API);
|
|
152
|
+
expect(md).toContain("skip");
|
|
153
|
+
expect(md).toContain("enabled");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ═══════════════════════════════════════════════════════════
|
|
158
|
+
describe("buildAuthSection — 인증 섹션", () => {
|
|
159
|
+
it("better-auth 엔드포인트 테이블", () => {
|
|
160
|
+
const md = buildAuthSection();
|
|
161
|
+
expect(md).toContain("/api/auth/sign-up/email");
|
|
162
|
+
expect(md).toContain("/api/auth/sign-in/email");
|
|
163
|
+
expect(md).toContain("/api/auth/sign-out");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("credentials include 경고", () => {
|
|
167
|
+
const md = buildAuthSection();
|
|
168
|
+
expect(md).toContain('credentials: "include"');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("프론트엔드 프록시 설정 + VITE_API_URL 안내", () => {
|
|
172
|
+
const md = buildAuthSection();
|
|
173
|
+
expect(md).toContain("VITE_API_URL");
|
|
174
|
+
expect(md).toContain("프록시");
|
|
175
|
+
expect(md).toContain("gencowAuth(import.meta.env.VITE_API_URL)");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ═══════════════════════════════════════════════════════════
|
|
180
|
+
describe("buildRpcSection — RPC 직접 호출", () => {
|
|
181
|
+
it("details 접힘 블록", () => {
|
|
182
|
+
const md = buildRpcSection(SIMPLE_API, ["tasks"]);
|
|
183
|
+
expect(md).toContain("<details>");
|
|
184
|
+
expect(md).toContain("</details>");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("실제 네임스페이스 이름 반영", () => {
|
|
188
|
+
const md = buildRpcSection(SIMPLE_API, ["tasks"]);
|
|
189
|
+
expect(md).toContain("tasks.list");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("mutation 예제 포함 (mutation 있을 때)", () => {
|
|
193
|
+
const md = buildRpcSection(SIMPLE_API, ["tasks"]);
|
|
194
|
+
expect(md).toContain("tasks.create");
|
|
195
|
+
expect(md).toContain("/api/mutation");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("query만 있으면 mutation 예제 없음", () => {
|
|
199
|
+
const md = buildRpcSection(QUERY_ONLY_API, ["products"]);
|
|
200
|
+
expect(md).not.toContain("/api/mutation");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("빈 API → fallback 네임스페이스", () => {
|
|
204
|
+
const md = buildRpcSection(EMPTY_API, []);
|
|
205
|
+
expect(md).toContain("namespace.functionName");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ═══════════════════════════════════════════════════════════
|
|
210
|
+
describe("buildAiPrompt — AI Vibe-Coding 프롬프트", () => {
|
|
211
|
+
it("API 구조 포함", () => {
|
|
212
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
213
|
+
expect(md).toContain("[tasks]");
|
|
214
|
+
expect(md).toContain("api.tasks.list (query)");
|
|
215
|
+
expect(md).toContain("api.tasks.create (mutation)");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("Hook 사용법 포함", () => {
|
|
219
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
220
|
+
expect(md).toContain("useQuery(api.namespace.fnName)");
|
|
221
|
+
expect(md).toContain("useMutation(api.namespace.fnName)");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("mutation 제한 경고 포함", () => {
|
|
225
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
226
|
+
expect(md).toContain("mutation은 10초 이내");
|
|
227
|
+
expect(md).toContain("scheduler.runAfter");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("크론 잡 사용법 포함", () => {
|
|
231
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
232
|
+
expect(md).toContain("cronJobs()");
|
|
233
|
+
expect(md).toContain("crons.interval");
|
|
234
|
+
expect(md).toContain("export default crons");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("배포 규칙 포함", () => {
|
|
238
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
239
|
+
expect(md).toContain("npx gencow deploy");
|
|
240
|
+
expect(md).toContain("--static");
|
|
241
|
+
expect(md).toContain("--no-backend");
|
|
242
|
+
expect(md).toContain("백엔드가 감지되면");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("여러 네임스페이스 모두 나열", () => {
|
|
246
|
+
const md = buildAiPrompt(MULTI_NS_API, ["tasks", "users"]);
|
|
247
|
+
expect(md).toContain("[tasks]");
|
|
248
|
+
expect(md).toContain("[users]");
|
|
249
|
+
expect(md).toContain("api.users.me (query)");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ═══════════════════════════════════════════════════════════
|
|
254
|
+
describe("buildAiUsageSection — AI ctx.ai 사용법", () => {
|
|
255
|
+
it("ctx.ai.chat 예제 포함", () => {
|
|
256
|
+
const md = buildAiUsageSection();
|
|
257
|
+
expect(md).toContain("ctx.ai.chat");
|
|
258
|
+
expect(md).toContain("gpt-4o-mini");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("OpenAI SDK 직접 사용 금지 경고", () => {
|
|
262
|
+
const md = buildAiUsageSection();
|
|
263
|
+
expect(md).toContain('import OpenAI from "openai"');
|
|
264
|
+
expect(md).toContain("❌ OpenAI SDK를 직접 설치하지 마세요");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("OPENAI_API_KEY 로컬 설정 안내", () => {
|
|
268
|
+
const md = buildAiUsageSection();
|
|
269
|
+
expect(md).toContain("OPENAI_API_KEY");
|
|
270
|
+
expect(md).toContain("로컬 전용");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ═══════════════════════════════════════════════════════════
|
|
275
|
+
describe("buildDeploySection — 배포 가이드", () => {
|
|
276
|
+
it("3단계 배포 프로세스", () => {
|
|
277
|
+
const md = buildDeploySection();
|
|
278
|
+
expect(md).toContain("gencow login");
|
|
279
|
+
expect(md).toContain("gencow deploy");
|
|
280
|
+
expect(md).toContain("gencow env set");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("환경변수 관리 명령어 테이블", () => {
|
|
284
|
+
const md = buildDeploySection();
|
|
285
|
+
expect(md).toContain("gencow env list");
|
|
286
|
+
expect(md).toContain("gencow env unset");
|
|
287
|
+
expect(md).toContain("gencow env push");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("프론트엔드 배포 가이드", () => {
|
|
291
|
+
const md = buildDeploySection();
|
|
292
|
+
expect(md).toContain("VITE_API_URL");
|
|
293
|
+
expect(md).toContain("--static dist/");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("CORS 설정 안내", () => {
|
|
297
|
+
const md = buildDeploySection();
|
|
298
|
+
expect(md).toContain("CORS_ORIGINS");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("풀스택 통합 배포 (백엔드 자동 감지) 안내", () => {
|
|
302
|
+
const md = buildDeploySection();
|
|
303
|
+
expect(md).toContain("백엔드가 감지되면 자동으로");
|
|
304
|
+
expect(md).toContain("gencow/");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("--no-backend 옵션 안내", () => {
|
|
308
|
+
const md = buildDeploySection();
|
|
309
|
+
expect(md).toContain("--no-backend");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ═══════════════════════════════════════════════════════════
|
|
314
|
+
describe("buildCronSection — 크론 잡", () => {
|
|
315
|
+
it("모든 크론 타입 예제", () => {
|
|
316
|
+
const md = buildCronSection();
|
|
317
|
+
expect(md).toContain("crons.interval");
|
|
318
|
+
expect(md).toContain("crons.daily");
|
|
319
|
+
expect(md).toContain("crons.weekly");
|
|
320
|
+
expect(md).toContain("crons.cron");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("export default 경고", () => {
|
|
324
|
+
const md = buildCronSection();
|
|
325
|
+
expect(md).toContain("export default crons");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("self-fetch 팁", () => {
|
|
329
|
+
const md = buildCronSection();
|
|
330
|
+
expect(md).toContain("GENCOW_INTERNAL_URL");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ═══════════════════════════════════════════════════════════
|
|
335
|
+
describe("buildDevTips — 개발 팁", () => {
|
|
336
|
+
it("핵심 팁 포함", () => {
|
|
337
|
+
const md = buildDevTips();
|
|
338
|
+
expect(md).toContain("자동으로 재생성");
|
|
339
|
+
expect(md).toContain("db:push");
|
|
340
|
+
expect(md).toContain("MCP 서버");
|
|
341
|
+
expect(md).toContain("gencow dev");
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ═══════════════════════════════════════════════════════════
|
|
346
|
+
describe("buildFrontendSetup — 프론트엔드 초기 설정", () => {
|
|
347
|
+
it("3단계 헤더 포함", () => {
|
|
348
|
+
const md = buildFrontendSetup();
|
|
349
|
+
expect(md).toContain("프론트엔드 초기 설정 (3단계)");
|
|
350
|
+
expect(md).toContain("1단계");
|
|
351
|
+
expect(md).toContain("2단계");
|
|
352
|
+
expect(md).toContain("3단계");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("gencowAuth 설정 예제 포함", () => {
|
|
356
|
+
const md = buildFrontendSetup();
|
|
357
|
+
expect(md).toContain("gencowAuth");
|
|
358
|
+
expect(md).toContain("signIn, signUp, signOut, useAuth");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("GencowProvider 올바른 props 예제", () => {
|
|
362
|
+
const md = buildFrontendSetup();
|
|
363
|
+
expect(md).toContain("GencowProvider");
|
|
364
|
+
expect(md).toContain("baseUrl={baseUrl}");
|
|
365
|
+
expect(md).toContain("token={token}");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("api.ts import 패턴", () => {
|
|
369
|
+
const md = buildFrontendSetup();
|
|
370
|
+
expect(md).toContain('@/gencow/api');
|
|
371
|
+
expect(md).toContain("gencow dev가 자동 생성");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("흔한 실수 경고 — 문자열 전달 금지", () => {
|
|
375
|
+
const md = buildFrontendSetup();
|
|
376
|
+
expect(md).toContain("흔한 실수");
|
|
377
|
+
expect(md).toContain('useQuery("tasks.list")');
|
|
378
|
+
expect(md).toContain("TS2345");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("흔한 실수 경고 — GencowProvider props", () => {
|
|
382
|
+
const md = buildFrontendSetup();
|
|
383
|
+
expect(md).toContain('url="http://..."');
|
|
384
|
+
expect(md).toContain("url' prop은 없습니다");
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// ═══════════════════════════════════════════════════════════
|
|
389
|
+
describe("buildDeploySection — 해싱/암호화 가이드", () => {
|
|
390
|
+
it("Web Crypto API sha256 스니펫 포함", () => {
|
|
391
|
+
const md = buildDeploySection();
|
|
392
|
+
expect(md).toContain("crypto.subtle.digest");
|
|
393
|
+
expect(md).toContain("SHA-256");
|
|
394
|
+
expect(md).toContain("sha256");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("simpleHash 충돌 경고", () => {
|
|
398
|
+
const md = buildDeploySection();
|
|
399
|
+
expect(md).toContain("simpleHash");
|
|
400
|
+
expect(md).toContain("충돌");
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// ═══════════════════════════════════════════════════════════
|
|
405
|
+
describe("buildAiPrompt — 해싱 가이드 포함", () => {
|
|
406
|
+
it("AI 프롬프트에 crypto 대안 안내", () => {
|
|
407
|
+
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
408
|
+
expect(md).toContain("crypto.subtle.digest");
|
|
409
|
+
expect(md).toContain("node:crypto");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ═══════════════════════════════════════════════════════════
|
|
414
|
+
describe("extractComponentsBlock — 컴포넌트 블록 추출", () => {
|
|
415
|
+
it("컴포넌트 블록이 있으면 추출", () => {
|
|
416
|
+
const content = `# README\n${COMP_START}\n### Button\n${COMP_END}\nEnd`;
|
|
417
|
+
const block = extractComponentsBlock(content);
|
|
418
|
+
expect(block).toContain(COMP_START);
|
|
419
|
+
expect(block).toContain("Button");
|
|
420
|
+
expect(block).toContain(COMP_END);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("컴포넌트 블록이 없으면 undefined", () => {
|
|
424
|
+
expect(extractComponentsBlock("# No components")).toBeUndefined();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("빈 문자열 → undefined", () => {
|
|
428
|
+
expect(extractComponentsBlock("")).toBeUndefined();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("null/undefined → undefined", () => {
|
|
432
|
+
expect(extractComponentsBlock(null)).toBeUndefined();
|
|
433
|
+
expect(extractComponentsBlock(undefined)).toBeUndefined();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("종료 마커 없으면 undefined", () => {
|
|
437
|
+
expect(extractComponentsBlock(`${COMP_START}\nno end`)).toBeUndefined();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Auditor — Pure function for pre-deploy dependency validation.
|
|
3
|
+
*
|
|
4
|
+
* Checks if user code imports packages that are not available
|
|
5
|
+
* in the Gencow cloud runtime. This prevents "new version unhealthy"
|
|
6
|
+
* deploy failures caused by missing third-party dependencies.
|
|
7
|
+
*
|
|
8
|
+
* How it works:
|
|
9
|
+
* 1. Uses esbuild metafile to extract all external imports
|
|
10
|
+
* 2. Filters out relative imports, Node built-ins, and platform packages
|
|
11
|
+
* 3. Returns list of unsupported packages with source file info
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const result = await auditDeployDependencies("./gencow/index.ts");
|
|
15
|
+
* if (!result.passed) { ... block deploy ... }
|
|
16
|
+
*/
|
|
17
|
+
import { build } from "esbuild";
|
|
18
|
+
|
|
19
|
+
// ── Platform packages (available in cloud runtime NODE_PATH) ──
|
|
20
|
+
// These are installed in the platform's node_modules and accessible
|
|
21
|
+
// via NODE_PATH at runtime. Keep in sync with server NODE_PATH setup.
|
|
22
|
+
const PLATFORM_PACKAGES = new Set([
|
|
23
|
+
// Gencow core
|
|
24
|
+
"@gencow/core",
|
|
25
|
+
"gencow",
|
|
26
|
+
// ORM
|
|
27
|
+
"drizzle-orm",
|
|
28
|
+
"drizzle-kit",
|
|
29
|
+
// Auth
|
|
30
|
+
"better-auth",
|
|
31
|
+
// DB drivers
|
|
32
|
+
"postgres",
|
|
33
|
+
"@electric-sql/pglite",
|
|
34
|
+
// HTTP framework
|
|
35
|
+
"hono",
|
|
36
|
+
// Build tools (used by auditor itself)
|
|
37
|
+
"esbuild",
|
|
38
|
+
// AI SDK
|
|
39
|
+
"ai",
|
|
40
|
+
"@ai-sdk/openai",
|
|
41
|
+
"@ai-sdk/anthropic",
|
|
42
|
+
"@ai-sdk/google",
|
|
43
|
+
// Validation
|
|
44
|
+
"zod",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// ── Node.js built-in modules (always available) ──
|
|
48
|
+
const NODE_BUILTINS = new Set([
|
|
49
|
+
"assert", "buffer", "child_process", "cluster", "crypto",
|
|
50
|
+
"dgram", "dns", "events", "fs", "http", "http2", "https",
|
|
51
|
+
"net", "os", "path", "perf_hooks", "process", "querystring",
|
|
52
|
+
"readline", "stream", "string_decoder", "timers", "tls",
|
|
53
|
+
"tty", "url", "util", "v8", "vm", "worker_threads", "zlib",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} UnsupportedDep
|
|
58
|
+
* @property {string} packageName - The npm package name
|
|
59
|
+
* @property {string} importedFrom - The source file that imports it
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} AuditResult
|
|
64
|
+
* @property {boolean} passed - true if no unsupported deps found
|
|
65
|
+
* @property {UnsupportedDep[]} unsupported - List of unsupported packages
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a module specifier is a platform-provided package.
|
|
70
|
+
*
|
|
71
|
+
* Handles subpath imports like "drizzle-orm/pg-core" → "drizzle-orm".
|
|
72
|
+
* Handles scoped packages like "@gencow/core/reactive" → "@gencow/core".
|
|
73
|
+
*
|
|
74
|
+
* @param {string} specifier - The import specifier
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
export function isPlatformPackage(specifier) {
|
|
78
|
+
// Exact match
|
|
79
|
+
if (PLATFORM_PACKAGES.has(specifier)) return true;
|
|
80
|
+
|
|
81
|
+
// Subpath: "drizzle-orm/pg-core" → check "drizzle-orm"
|
|
82
|
+
// Scoped: "@gencow/core/reactive" → check "@gencow/core"
|
|
83
|
+
let root;
|
|
84
|
+
if (specifier.startsWith("@")) {
|
|
85
|
+
// @scope/name/subpath → @scope/name
|
|
86
|
+
const parts = specifier.split("/");
|
|
87
|
+
root = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
88
|
+
} else {
|
|
89
|
+
// name/subpath → name
|
|
90
|
+
root = specifier.split("/")[0];
|
|
91
|
+
}
|
|
92
|
+
return PLATFORM_PACKAGES.has(root);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a module specifier is a Node.js built-in.
|
|
97
|
+
*
|
|
98
|
+
* Handles "node:" prefix: "node:fs" → "fs".
|
|
99
|
+
*
|
|
100
|
+
* @param {string} specifier
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function isNodeBuiltin(specifier) {
|
|
104
|
+
const bare = specifier.startsWith("node:") ? specifier.slice(5) : specifier;
|
|
105
|
+
// Handle subpaths like "fs/promises"
|
|
106
|
+
const root = bare.split("/")[0];
|
|
107
|
+
return NODE_BUILTINS.has(root);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Audit user functions for unsupported third-party dependencies.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} entryPoint - Path to the gencow/ entry point (e.g. "./gencow/index.ts")
|
|
114
|
+
* @returns {Promise<AuditResult>}
|
|
115
|
+
*/
|
|
116
|
+
export async function auditDeployDependencies(entryPoint) {
|
|
117
|
+
/** @type {UnsupportedDep[]} */
|
|
118
|
+
const unsupported = [];
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await build({
|
|
122
|
+
entryPoints: [entryPoint],
|
|
123
|
+
bundle: true,
|
|
124
|
+
write: false,
|
|
125
|
+
metafile: true,
|
|
126
|
+
platform: "node",
|
|
127
|
+
format: "esm",
|
|
128
|
+
packages: "external", // Don't bundle node_modules, just collect imports
|
|
129
|
+
logLevel: "silent",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!result.metafile) {
|
|
133
|
+
return { passed: true, unsupported: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Collect all unique external imports with their source files
|
|
137
|
+
/** @type {Map<string, string>} packageName → first importedFrom */
|
|
138
|
+
const seen = new Map();
|
|
139
|
+
|
|
140
|
+
for (const [filePath, fileData] of Object.entries(result.metafile.inputs)) {
|
|
141
|
+
for (const imp of /** @type {any[]} */ (fileData.imports)) {
|
|
142
|
+
const spec = imp.path;
|
|
143
|
+
if (!spec) continue;
|
|
144
|
+
|
|
145
|
+
// Skip relative/absolute imports (user's own code)
|
|
146
|
+
if (spec.startsWith(".") || spec.startsWith("/")) continue;
|
|
147
|
+
|
|
148
|
+
// Skip Node builtins
|
|
149
|
+
if (isNodeBuiltin(spec)) continue;
|
|
150
|
+
|
|
151
|
+
// Skip platform packages
|
|
152
|
+
if (isPlatformPackage(spec)) continue;
|
|
153
|
+
|
|
154
|
+
// Extract package root for dedup
|
|
155
|
+
let pkgRoot;
|
|
156
|
+
if (spec.startsWith("@")) {
|
|
157
|
+
const parts = spec.split("/");
|
|
158
|
+
pkgRoot = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
|
|
159
|
+
} else {
|
|
160
|
+
pkgRoot = spec.split("/")[0];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!seen.has(pkgRoot)) {
|
|
164
|
+
// Clean up file path for display
|
|
165
|
+
const cleanPath = filePath.replace(/^\.\//, "");
|
|
166
|
+
seen.set(pkgRoot, cleanPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const [packageName, importedFrom] of seen) {
|
|
172
|
+
unsupported.push({ packageName, importedFrom });
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// If esbuild analysis fails (e.g. syntax error), warn but don't block
|
|
176
|
+
console.warn("[deploy-auditor] Dependency audit skipped — could not analyze:", err.message);
|
|
177
|
+
return { passed: true, unsupported: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
passed: unsupported.length === 0,
|
|
182
|
+
unsupported,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Format audit result as a human-readable error message for CLI output.
|
|
188
|
+
*
|
|
189
|
+
* @param {AuditResult} result
|
|
190
|
+
* @returns {string} Formatted message (empty string if passed)
|
|
191
|
+
*/
|
|
192
|
+
export function formatAuditError(result) {
|
|
193
|
+
if (result.passed) return "";
|
|
194
|
+
|
|
195
|
+
const lines = [
|
|
196
|
+
"",
|
|
197
|
+
"╔═══════════════════════════════════════════════════════════════╗",
|
|
198
|
+
"║ ⛔ DEPLOY BLOCKED — Unsupported Dependencies Detected ║",
|
|
199
|
+
"╚═══════════════════════════════════════════════════════════════╝",
|
|
200
|
+
"",
|
|
201
|
+
" The following packages are NOT available in the Gencow cloud runtime:",
|
|
202
|
+
"",
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
for (const dep of result.unsupported) {
|
|
206
|
+
lines.push(` ✗ ${dep.packageName} (imported in ${dep.importedFrom})`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push("");
|
|
210
|
+
lines.push(" Gencow 클라우드에서 사용 가능한 패키지:");
|
|
211
|
+
lines.push(" @gencow/core, drizzle-orm, better-auth, postgres, hono, ai, zod");
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(" 해결 방법:");
|
|
214
|
+
lines.push(" 1. 해당 패키지 사용을 제거하거나 동적 import (try/catch)로 변경");
|
|
215
|
+
lines.push(" 2. gencow deploy --force 로 강제 배포 (서버 크래시 위험)");
|
|
216
|
+
lines.push("");
|
|
217
|
+
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get the list of platform-provided packages (for display/docs).
|
|
223
|
+
* @returns {string[]}
|
|
224
|
+
*/
|
|
225
|
+
export function getPlatformPackageList() {
|
|
226
|
+
return [...PLATFORM_PACKAGES].sort();
|
|
227
|
+
}
|