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,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Auditor — Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the pure functions in deploy-auditor.mjs.
|
|
5
|
+
* Note: auditDeployDependencies() requires esbuild and actual files,
|
|
6
|
+
* so we only test the utility functions here.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
isPlatformPackage,
|
|
11
|
+
isNodeBuiltin,
|
|
12
|
+
formatAuditError,
|
|
13
|
+
getPlatformPackageList,
|
|
14
|
+
} from "../deploy-auditor.mjs";
|
|
15
|
+
|
|
16
|
+
describe("isPlatformPackage", () => {
|
|
17
|
+
it("recognizes exact platform packages", () => {
|
|
18
|
+
expect(isPlatformPackage("@gencow/core")).toBe(true);
|
|
19
|
+
expect(isPlatformPackage("drizzle-orm")).toBe(true);
|
|
20
|
+
expect(isPlatformPackage("better-auth")).toBe(true);
|
|
21
|
+
expect(isPlatformPackage("postgres")).toBe(true);
|
|
22
|
+
expect(isPlatformPackage("hono")).toBe(true);
|
|
23
|
+
expect(isPlatformPackage("esbuild")).toBe(true);
|
|
24
|
+
expect(isPlatformPackage("ai")).toBe(true);
|
|
25
|
+
expect(isPlatformPackage("zod")).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("recognizes subpath imports of platform packages", () => {
|
|
29
|
+
expect(isPlatformPackage("drizzle-orm/pg-core")).toBe(true);
|
|
30
|
+
expect(isPlatformPackage("drizzle-orm/node-postgres")).toBe(true);
|
|
31
|
+
expect(isPlatformPackage("hono/cors")).toBe(true);
|
|
32
|
+
expect(isPlatformPackage("hono/logger")).toBe(true);
|
|
33
|
+
expect(isPlatformPackage("hono/bun")).toBe(true);
|
|
34
|
+
expect(isPlatformPackage("better-auth/plugins")).toBe(true);
|
|
35
|
+
expect(isPlatformPackage("@gencow/core/reactive")).toBe(true);
|
|
36
|
+
expect(isPlatformPackage("@ai-sdk/openai")).toBe(true);
|
|
37
|
+
expect(isPlatformPackage("@ai-sdk/openai/something")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects non-platform packages", () => {
|
|
41
|
+
expect(isPlatformPackage("langfuse")).toBe(false);
|
|
42
|
+
expect(isPlatformPackage("openai")).toBe(false);
|
|
43
|
+
expect(isPlatformPackage("axios")).toBe(false);
|
|
44
|
+
expect(isPlatformPackage("lodash")).toBe(false);
|
|
45
|
+
expect(isPlatformPackage("@prisma/client")).toBe(false);
|
|
46
|
+
expect(isPlatformPackage("cheerio")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("isNodeBuiltin", () => {
|
|
51
|
+
it("recognizes Node builtins", () => {
|
|
52
|
+
expect(isNodeBuiltin("fs")).toBe(true);
|
|
53
|
+
expect(isNodeBuiltin("path")).toBe(true);
|
|
54
|
+
expect(isNodeBuiltin("crypto")).toBe(true);
|
|
55
|
+
expect(isNodeBuiltin("http")).toBe(true);
|
|
56
|
+
expect(isNodeBuiltin("events")).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("recognizes node: prefixed builtins", () => {
|
|
60
|
+
expect(isNodeBuiltin("node:fs")).toBe(true);
|
|
61
|
+
expect(isNodeBuiltin("node:path")).toBe(true);
|
|
62
|
+
expect(isNodeBuiltin("node:crypto")).toBe(true);
|
|
63
|
+
expect(isNodeBuiltin("node:stream")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("recognizes subpath builtins", () => {
|
|
67
|
+
expect(isNodeBuiltin("fs/promises")).toBe(true);
|
|
68
|
+
expect(isNodeBuiltin("node:fs/promises")).toBe(true);
|
|
69
|
+
expect(isNodeBuiltin("stream/web")).toBe(true);
|
|
70
|
+
expect(isNodeBuiltin("util/types")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rejects non-builtins", () => {
|
|
74
|
+
expect(isNodeBuiltin("langfuse")).toBe(false);
|
|
75
|
+
expect(isNodeBuiltin("drizzle-orm")).toBe(false);
|
|
76
|
+
expect(isNodeBuiltin("@gencow/core")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("formatAuditError", () => {
|
|
81
|
+
it("returns empty string when passed", () => {
|
|
82
|
+
expect(formatAuditError({ passed: true, unsupported: [] })).toBe("");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("formats error with unsupported packages", () => {
|
|
86
|
+
const result = {
|
|
87
|
+
passed: false,
|
|
88
|
+
unsupported: [
|
|
89
|
+
{ packageName: "langfuse", importedFrom: "gencow/llmActions.ts" },
|
|
90
|
+
{ packageName: "cheerio", importedFrom: "gencow/crawlPipeline.ts" },
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
const output = formatAuditError(result);
|
|
94
|
+
expect(output).toContain("DEPLOY BLOCKED");
|
|
95
|
+
expect(output).toContain("langfuse");
|
|
96
|
+
expect(output).toContain("gencow/llmActions.ts");
|
|
97
|
+
expect(output).toContain("cheerio");
|
|
98
|
+
expect(output).toContain("gencow/crawlPipeline.ts");
|
|
99
|
+
expect(output).toContain("--force");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("includes platform package list in error", () => {
|
|
103
|
+
const result = {
|
|
104
|
+
passed: false,
|
|
105
|
+
unsupported: [{ packageName: "moment", importedFrom: "gencow/utils.ts" }],
|
|
106
|
+
};
|
|
107
|
+
const output = formatAuditError(result);
|
|
108
|
+
expect(output).toContain("@gencow/core");
|
|
109
|
+
expect(output).toContain("drizzle-orm");
|
|
110
|
+
expect(output).toContain("better-auth");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("getPlatformPackageList", () => {
|
|
115
|
+
it("returns sorted list of platform packages", () => {
|
|
116
|
+
const list = getPlatformPackageList();
|
|
117
|
+
expect(list).toContain("@gencow/core");
|
|
118
|
+
expect(list).toContain("drizzle-orm");
|
|
119
|
+
expect(list).toContain("hono");
|
|
120
|
+
expect(Array.isArray(list)).toBe(true);
|
|
121
|
+
// Verify sorted
|
|
122
|
+
const sorted = [...list].sort();
|
|
123
|
+
expect(list).toEqual(sorted);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
parseEnvFile,
|
|
4
|
+
isValidEnvKey,
|
|
5
|
+
serializeEnvFile,
|
|
6
|
+
} from "../env-parser.mjs";
|
|
7
|
+
|
|
8
|
+
describe("parseEnvFile", () => {
|
|
9
|
+
it("기본 KEY=VALUE 파싱", () => {
|
|
10
|
+
const result = parseEnvFile("FOO=bar\nBAZ=qux");
|
|
11
|
+
expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("빈 문자열 → 빈 객체", () => {
|
|
15
|
+
expect(parseEnvFile("")).toEqual({});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("null/undefined → 빈 객체", () => {
|
|
19
|
+
expect(parseEnvFile(null)).toEqual({});
|
|
20
|
+
expect(parseEnvFile(undefined)).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("주석 무시 (#으로 시작)", () => {
|
|
24
|
+
const result = parseEnvFile("# comment\nFOO=bar\n# another");
|
|
25
|
+
expect(result).toEqual({ FOO: "bar" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("빈 줄 무시", () => {
|
|
29
|
+
const result = parseEnvFile("\n\nFOO=bar\n\n\nBAZ=1\n\n");
|
|
30
|
+
expect(result).toEqual({ FOO: "bar", BAZ: "1" });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("따옴표 제거 (큰따옴표)", () => {
|
|
34
|
+
const result = parseEnvFile('KEY="value with spaces"');
|
|
35
|
+
expect(result).toEqual({ KEY: "value with spaces" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("따옴표 제거 (작은따옴표)", () => {
|
|
39
|
+
const result = parseEnvFile("KEY='value with spaces'");
|
|
40
|
+
expect(result).toEqual({ KEY: "value with spaces" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("값에 = 포함 (첫 번째 = 기준 분리)", () => {
|
|
44
|
+
const result = parseEnvFile("DATABASE_URL=postgres://user:pass@host/db?ssl=true");
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
DATABASE_URL: "postgres://user:pass@host/db?ssl=true",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("값이 빈 경우", () => {
|
|
51
|
+
const result = parseEnvFile("EMPTY=");
|
|
52
|
+
expect(result).toEqual({ EMPTY: "" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("공백만 있는 줄 무시", () => {
|
|
56
|
+
const result = parseEnvFile(" \n \t \nFOO=bar");
|
|
57
|
+
expect(result).toEqual({ FOO: "bar" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("= 없는 줄 무시", () => {
|
|
61
|
+
const result = parseEnvFile("INVALID_LINE\nFOO=bar");
|
|
62
|
+
expect(result).toEqual({ FOO: "bar" });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("키 앞뒤 공백 trim", () => {
|
|
66
|
+
const result = parseEnvFile(" KEY = value ");
|
|
67
|
+
expect(result).toEqual({ KEY: "value" });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("실제 .env 파일 시뮬레이션", () => {
|
|
71
|
+
const content = `# Gencow Environment
|
|
72
|
+
DATABASE_URL=postgres://localhost:5432/mydb
|
|
73
|
+
OPENAI_API_KEY=sk-1234567890
|
|
74
|
+
|
|
75
|
+
# Optional
|
|
76
|
+
DEBUG=false
|
|
77
|
+
APP_NAME="My App"
|
|
78
|
+
`;
|
|
79
|
+
const result = parseEnvFile(content);
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
DATABASE_URL: "postgres://localhost:5432/mydb",
|
|
82
|
+
OPENAI_API_KEY: "sk-1234567890",
|
|
83
|
+
DEBUG: "false",
|
|
84
|
+
APP_NAME: "My App",
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("isValidEnvKey", () => {
|
|
90
|
+
it("유효한 키", () => {
|
|
91
|
+
expect(isValidEnvKey("FOO")).toBe(true);
|
|
92
|
+
expect(isValidEnvKey("_PRIVATE")).toBe(true);
|
|
93
|
+
expect(isValidEnvKey("DATABASE_URL")).toBe(true);
|
|
94
|
+
expect(isValidEnvKey("key123")).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("무효한 키", () => {
|
|
98
|
+
expect(isValidEnvKey("123FOO")).toBe(false);
|
|
99
|
+
expect(isValidEnvKey("FOO-BAR")).toBe(false);
|
|
100
|
+
expect(isValidEnvKey("FOO BAR")).toBe(false);
|
|
101
|
+
expect(isValidEnvKey("")).toBe(false);
|
|
102
|
+
expect(isValidEnvKey("a.b")).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("serializeEnvFile", () => {
|
|
107
|
+
it("기본 직렬화", () => {
|
|
108
|
+
const result = serializeEnvFile({ FOO: "bar", BAZ: "1" });
|
|
109
|
+
expect(result).toBe("FOO=bar\nBAZ=1\n");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("공백 포함 값은 따옴표 감싸기", () => {
|
|
113
|
+
const result = serializeEnvFile({ KEY: "hello world" });
|
|
114
|
+
expect(result).toBe('KEY="hello world"\n');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("# 포함 값은 따옴표 감싸기", () => {
|
|
118
|
+
const result = serializeEnvFile({ KEY: "value#comment" });
|
|
119
|
+
expect(result).toBe('KEY="value#comment"\n');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("빈 객체 → 빈 줄", () => {
|
|
123
|
+
const result = serializeEnvFile({});
|
|
124
|
+
expect(result).toBe("\n");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("roundtrip — parse → serialize → parse", () => {
|
|
128
|
+
const original = "FOO=bar\nDB=postgres://localhost/db\n";
|
|
129
|
+
const parsed = parseEnvFile(original);
|
|
130
|
+
const serialized = serializeEnvFile(parsed);
|
|
131
|
+
const reparsed = parseEnvFile(serialized);
|
|
132
|
+
expect(reparsed).toEqual(parsed);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
validateProjectStructure,
|
|
4
|
+
shouldExcludeFromDeploy,
|
|
5
|
+
} from "../project-validator.mjs";
|
|
6
|
+
|
|
7
|
+
describe("validateProjectStructure", () => {
|
|
8
|
+
it("유효한 프로젝트 — 에러 없음", () => {
|
|
9
|
+
const result = validateProjectStructure({
|
|
10
|
+
files: ["gencow/index.ts", "gencow/schema.ts", "package.json"],
|
|
11
|
+
});
|
|
12
|
+
expect(result.valid).toBe(true);
|
|
13
|
+
expect(result.errors).toHaveLength(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("index.ts 누락 → 에러", () => {
|
|
17
|
+
const result = validateProjectStructure({
|
|
18
|
+
files: ["gencow/schema.ts", "package.json"],
|
|
19
|
+
});
|
|
20
|
+
expect(result.valid).toBe(false);
|
|
21
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("schema.ts 누락 → 에러", () => {
|
|
25
|
+
const result = validateProjectStructure({
|
|
26
|
+
files: ["gencow/index.ts", "package.json"],
|
|
27
|
+
});
|
|
28
|
+
expect(result.valid).toBe(false);
|
|
29
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("package.json 누락 → 경고 (에러 아님)", () => {
|
|
33
|
+
const result = validateProjectStructure({
|
|
34
|
+
files: ["gencow/index.ts", "gencow/schema.ts"],
|
|
35
|
+
});
|
|
36
|
+
expect(result.valid).toBe(true);
|
|
37
|
+
expect(result.warnings.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("crons.ts 존재 시 → 경고", () => {
|
|
41
|
+
const result = validateProjectStructure({
|
|
42
|
+
files: [
|
|
43
|
+
"gencow/index.ts",
|
|
44
|
+
"gencow/schema.ts",
|
|
45
|
+
"gencow/crons.ts",
|
|
46
|
+
"package.json",
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
expect(result.valid).toBe(true);
|
|
50
|
+
expect(result.warnings.some((w) => w.includes("crons"))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("커스텀 functionsDir 지원", () => {
|
|
54
|
+
const result = validateProjectStructure({
|
|
55
|
+
files: ["backend/index.ts", "backend/schema.ts", "package.json"],
|
|
56
|
+
functionsDir: "backend",
|
|
57
|
+
});
|
|
58
|
+
expect(result.valid).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it(".js 파일도 허용", () => {
|
|
62
|
+
const result = validateProjectStructure({
|
|
63
|
+
files: ["gencow/index.js", "gencow/schema.js", "package.json"],
|
|
64
|
+
});
|
|
65
|
+
expect(result.valid).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("완전히 빈 프로젝트 → 에러 2개", () => {
|
|
69
|
+
const result = validateProjectStructure({ files: [] });
|
|
70
|
+
expect(result.valid).toBe(false);
|
|
71
|
+
expect(result.errors.length).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("shouldExcludeFromDeploy", () => {
|
|
76
|
+
it("node_modules 제외", () => {
|
|
77
|
+
expect(shouldExcludeFromDeploy("node_modules/foo/index.js")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it(".git 제외", () => {
|
|
81
|
+
expect(shouldExcludeFromDeploy(".git/HEAD")).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it(".gencow 제외", () => {
|
|
85
|
+
expect(shouldExcludeFromDeploy(".gencow/data")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it(".env 제외", () => {
|
|
89
|
+
expect(shouldExcludeFromDeploy(".env")).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it(".env.local 제외", () => {
|
|
93
|
+
expect(shouldExcludeFromDeploy(".env.local")).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("테스트 파일 제외", () => {
|
|
97
|
+
expect(shouldExcludeFromDeploy("src/foo.test.ts")).toBe(true);
|
|
98
|
+
expect(shouldExcludeFromDeploy("src/foo.spec.js")).toBe(true);
|
|
99
|
+
expect(shouldExcludeFromDeploy("__tests__/bar.ts")).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("dist 제외", () => {
|
|
103
|
+
expect(shouldExcludeFromDeploy("dist/index.js")).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("coverage 제외", () => {
|
|
107
|
+
expect(shouldExcludeFromDeploy("coverage/lcov-report/index.html")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("일반 소스 파일은 포함", () => {
|
|
111
|
+
expect(shouldExcludeFromDeploy("gencow/index.ts")).toBe(false);
|
|
112
|
+
expect(shouldExcludeFromDeploy("gencow/schema.ts")).toBe(false);
|
|
113
|
+
expect(shouldExcludeFromDeploy("package.json")).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it(".DS_Store 제외", () => {
|
|
117
|
+
expect(shouldExcludeFromDeploy(".DS_Store")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|