@m1kapp/kit 0.0.23 → 0.0.25

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/stats.mjs ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * m1kkit stats — 프로젝트의 코드량과 kit 사용 현황을 분석해서 .kit-stats.json 생성
4
+ *
5
+ * Usage:
6
+ * m1kkit stats # src/ 기준 분석
7
+ * m1kkit stats --dir=app # 특정 디렉토리 기준
8
+ * m1kkit stats --out=public # 출력 위치 지정
9
+ */
10
+
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { createRequire } from "module";
14
+
15
+ const args = process.argv.slice(2);
16
+ const getFlag = (name) => {
17
+ const found = args.find((a) => a.startsWith(`--${name}=`));
18
+ return found ? found.split("=")[1] : undefined;
19
+ };
20
+
21
+ const srcDir = path.resolve(process.cwd(), getFlag("dir") || "src");
22
+ const outDir = path.resolve(process.cwd(), getFlag("out") || "public");
23
+
24
+ // kit의 meta.json에서 실제 측정된 LOC 로드
25
+ let KIT_FEATURES = {};
26
+ let kitVersion = "unknown";
27
+ let kitTotalFeatures = { component: 0, hook: 0, util: 0 };
28
+
29
+ // meta.json 탐색: require.resolve → node_modules 직접 탐색 → 상위 디렉토리
30
+ function findMeta() {
31
+ // 1. require.resolve
32
+ try {
33
+ const require = createRequire(path.resolve(process.cwd(), "package.json"));
34
+ return require.resolve("@m1kapp/kit/dist/meta.json");
35
+ } catch {}
36
+
37
+ // 2. node_modules에서 직접 탐색
38
+ let dir = process.cwd();
39
+ while (dir !== path.dirname(dir)) {
40
+ const candidate = path.join(dir, "node_modules", "@m1kapp", "kit", "dist", "meta.json");
41
+ if (fs.existsSync(candidate)) return candidate;
42
+ dir = path.dirname(dir);
43
+ }
44
+
45
+ // 3. 이 스크립트가 kit 안에 있으면 형제 dist/ 탐색
46
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
47
+ const siblingMeta = path.join(scriptDir, "..", "dist", "meta.json");
48
+ if (fs.existsSync(siblingMeta)) return siblingMeta;
49
+
50
+ return null;
51
+ }
52
+
53
+ const metaPath = findMeta();
54
+ if (!metaPath) {
55
+ console.error(" @m1kapp/kit/dist/meta.json을 찾을 수 없습니다.");
56
+ console.error(" kit을 먼저 빌드하거나 npm install 후 다시 시도하세요.\n");
57
+ process.exit(1);
58
+ }
59
+
60
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
61
+ KIT_FEATURES = meta.features;
62
+ kitVersion = meta.version;
63
+
64
+ // kit이 제공하는 전체 요소 수 카운트
65
+ for (const f of Object.values(meta.features)) {
66
+ kitTotalFeatures[f.category] = (kitTotalFeatures[f.category] || 0) + 1;
67
+ }
68
+
69
+ console.log(` meta.json 로드 완료 (v${kitVersion}, ${Object.keys(KIT_FEATURES).length}개 요소)\n`);
70
+
71
+ // 소스 파일 수집
72
+ function collectFiles(dir, exts = [".ts", ".tsx", ".js", ".jsx"]) {
73
+ const results = [];
74
+ if (!fs.existsSync(dir)) return results;
75
+
76
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ const fullPath = path.join(dir, entry.name);
79
+ if (entry.isDirectory()) {
80
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
81
+ results.push(...collectFiles(fullPath, exts));
82
+ } else if (exts.some((ext) => entry.name.endsWith(ext))) {
83
+ results.push(fullPath);
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
89
+ // 줄 수 카운트 (빈 줄, 주석만 있는 줄 제외)
90
+ function countLines(content) {
91
+ const lines = content.split("\n");
92
+ let total = 0;
93
+ let code = 0;
94
+ for (const line of lines) {
95
+ total++;
96
+ const trimmed = line.trim();
97
+ if (trimmed && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) {
98
+ code++;
99
+ }
100
+ }
101
+ return { total, code };
102
+ }
103
+
104
+ // kit import 감지
105
+ function detectKitImports(content) {
106
+ const found = new Set();
107
+ // import { X, Y } from "@m1kapp/kit" 또는 "@m1kapp/kit/..." 패턴
108
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@m1kapp\/kit(?:\/[^"']*)?["']/g;
109
+ let match;
110
+ while ((match = importRegex.exec(content)) !== null) {
111
+ const names = match[1].split(",").map((s) => s.trim().split(" as ")[0].trim());
112
+ for (const name of names) {
113
+ if (name && !name.startsWith("type ")) {
114
+ found.add(name);
115
+ }
116
+ }
117
+ }
118
+ // import type은 제외 — 타입만 쓰는 건 코드 절약 아님
119
+ return found;
120
+ }
121
+
122
+ // 실행
123
+ console.log(`\n 분석 중... ${srcDir}\n`);
124
+
125
+ const files = collectFiles(srcDir);
126
+ if (files.length === 0) {
127
+ console.error(` 파일을 찾을 수 없습니다: ${srcDir}`);
128
+ process.exit(1);
129
+ }
130
+
131
+ let totalLines = 0;
132
+ let codeLines = 0;
133
+ const allImports = new Set();
134
+
135
+ for (const file of files) {
136
+ const content = fs.readFileSync(file, "utf-8");
137
+ const counts = countLines(content);
138
+ totalLines += counts.total;
139
+ codeLines += counts.code;
140
+ const imports = detectKitImports(content);
141
+ for (const imp of imports) allImports.add(imp);
142
+ }
143
+
144
+ // 절약량 계산
145
+ // 같은 소스 파일에서 여러 export를 쓰더라도 파일 LOC는 한 번만 카운트
146
+ const usedFeatures = [];
147
+ let savedLines = 0;
148
+ const usedByCategory = { component: 0, hook: 0, util: 0 };
149
+ const countedSources = new Set();
150
+
151
+ for (const name of allImports) {
152
+ const meta = KIT_FEATURES[name];
153
+ if (!meta) continue;
154
+
155
+ // 카테고리별 사용 수 (loc 0이어도 카운트 — "Tab"도 사용한 거니까)
156
+ usedByCategory[meta.category] = (usedByCategory[meta.category] || 0) + 1;
157
+
158
+ // LOC 절약은 소스 파일 단위로 1번만
159
+ if (meta.source && countedSources.has(meta.source)) continue;
160
+ if (meta.source) countedSources.add(meta.source);
161
+
162
+ if (meta.loc > 0) {
163
+ usedFeatures.push({ name, loc: meta.loc, category: meta.category });
164
+ savedLines += meta.loc;
165
+ }
166
+ }
167
+
168
+ const estimatedKB = Math.round(savedLines * 40 / 1024);
169
+ const estimatedA4 = Math.round(savedLines / 80);
170
+ const savedPercent = codeLines > 0 ? Math.round((savedLines / (codeLines + savedLines)) * 100) : 0;
171
+
172
+ // 사용률: kit이 제공하는 전체 요소 중 몇 개를 쓰고 있는지
173
+ const usage = {
174
+ component: { used: usedByCategory.component, total: kitTotalFeatures.component, percent: kitTotalFeatures.component > 0 ? Math.round((usedByCategory.component / kitTotalFeatures.component) * 100) : 0 },
175
+ hook: { used: usedByCategory.hook, total: kitTotalFeatures.hook, percent: kitTotalFeatures.hook > 0 ? Math.round((usedByCategory.hook / kitTotalFeatures.hook) * 100) : 0 },
176
+ util: { used: usedByCategory.util, total: kitTotalFeatures.util, percent: kitTotalFeatures.util > 0 ? Math.round((usedByCategory.util / kitTotalFeatures.util) * 100) : 0 },
177
+ };
178
+
179
+ const stats = {
180
+ generatedAt: new Date().toISOString(),
181
+ kitVersion,
182
+ source: {
183
+ dir: path.relative(process.cwd(), srcDir),
184
+ files: files.length,
185
+ totalLines,
186
+ codeLines,
187
+ },
188
+ kit: {
189
+ features: usedFeatures.sort((a, b) => b.loc - a.loc),
190
+ savedLines,
191
+ savedKB: estimatedKB,
192
+ savedA4: estimatedA4,
193
+ savedPercent,
194
+ usage,
195
+ },
196
+ };
197
+
198
+ // 출력
199
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
200
+ const outPath = path.join(outDir, "kit-stats.json");
201
+ fs.writeFileSync(outPath, JSON.stringify(stats, null, 2));
202
+
203
+ console.log(` 파일: ${files.length}개`);
204
+ console.log(` 코드: ${codeLines.toLocaleString()}줄 (전체 ${totalLines.toLocaleString()}줄)`);
205
+ console.log(` kit 사용: ${usedFeatures.length}개 요소`);
206
+ console.log(` 컴포넌트: ${usage.component.used}/${usage.component.total}개 (${usage.component.percent}%)`);
207
+ console.log(` 훅: ${usage.hook.used}/${usage.hook.total}개 (${usage.hook.percent}%)`);
208
+ console.log(` 유틸리티: ${usage.util.used}/${usage.util.total}개 (${usage.util.percent}%)`);
209
+ console.log(` 절약량: 약 ${savedLines.toLocaleString()}줄, ${estimatedKB}KB (A4 ${estimatedA4}장)`);
210
+ console.log(` 비율: 전체의 약 ${savedPercent}%를 kit이 대신 처리`);
211
+ console.log(`\n 저장됨 → ${path.relative(process.cwd(), outPath)}\n`);
package/bin/track.mjs ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * m1kkit track <url> [--host=m1k.app]
4
+ *
5
+ * 로그인 없이 m1k 방문자 트래커에 사이트를 등록하고 배지 slug를 발급받는다.
6
+ * 발급된 claim 토큰은 현재 폴더의 .m1k.json 에 저장 → 나중에 `m1kkit claim`으로 계정 귀속.
7
+ */
8
+ import { writeFileSync, existsSync, readFileSync } from "fs";
9
+ import { resolve } from "path";
10
+
11
+ const args = process.argv.slice(2);
12
+ const flags = Object.fromEntries(
13
+ args.filter((a) => a.startsWith("--")).map((a) => {
14
+ const [k, v] = a.replace(/^--/, "").split("=");
15
+ return [k, v ?? true];
16
+ }),
17
+ );
18
+ const positional = args.filter((a) => !a.startsWith("--"));
19
+ const url = positional[0];
20
+ const host = flags.host || process.env.M1K_HOST || "m1k.app";
21
+
22
+ if (!url || flags.help) {
23
+ console.log(`
24
+ m1kkit track — m1k 방문자 트래커에 사이트 등록 (무로그인)
25
+
26
+ 사용법:
27
+ npx m1kkit track <url> [--host=m1k.app]
28
+
29
+ 예시:
30
+ npx m1kkit track https://myside.app
31
+
32
+ 동작:
33
+ 1) 로그인 없이 사이트를 등록하고 배지 slug를 발급
34
+ 2) 배지 스니펫을 출력 (README/푸터에 붙여넣기 → 바로 수집 시작)
35
+ 3) claim 토큰을 ./.m1k.json 에 저장 → 나중에 'm1kkit claim'으로 내 계정에 귀속
36
+ `);
37
+ process.exit(url ? 0 : 1);
38
+ }
39
+
40
+ const scheme = /^(localhost|127\.|0\.0\.0\.0)/.test(host) ? "http" : "https";
41
+ const base = `${scheme}://${host}`;
42
+ const endpoint = `${base}/api/sites/cli`;
43
+ let res, data;
44
+ try {
45
+ res = await fetch(endpoint, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json" },
48
+ body: JSON.stringify({ url }),
49
+ });
50
+ data = await res.json().catch(() => ({}));
51
+ } catch (e) {
52
+ console.error(`✗ 등록 요청 실패: ${e.message}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ if (!res.ok) {
57
+ console.error(`✗ ${data?.error || data?.message || res.statusText}`);
58
+ process.exit(1);
59
+ }
60
+
61
+ if (data.alreadyRegistered) {
62
+ console.log(`\n⚠ ${data.message}`);
63
+ console.log(` slug: ${data.slug}`);
64
+ console.log(` badge: ${data.badgeUrl}\n`);
65
+ process.exit(0);
66
+ }
67
+
68
+ // .m1k.json 저장 (기존 있으면 병합)
69
+ const file = resolve(process.cwd(), ".m1k.json");
70
+ let store = {};
71
+ if (existsSync(file)) {
72
+ try { store = JSON.parse(readFileSync(file, "utf8")); } catch { /* ignore */ }
73
+ }
74
+ store.host = host;
75
+ store.slug = data.slug;
76
+ store.url = data.url;
77
+ store.claimToken = data.claimToken;
78
+ writeFileSync(file, JSON.stringify(store, null, 2) + "\n");
79
+
80
+ console.log(`
81
+ ✓ 등록 완료! ${data.url}
82
+
83
+ 배지 스니펫 (README/푸터에 붙여넣기):
84
+ ${data.snippet}
85
+
86
+ ⚡ kit을 쓰면 스니펫 없이 자동 집계:
87
+ .env 에 한 줄만 추가하면 Watermark/PoweredByKit 하단 크레딧이 방문을 자동으로 셉니다.
88
+ NEXT_PUBLIC_M1K_SLUG=${data.slug}
89
+ (끄려면: <Watermark track={false}> 또는 그 줄 삭제)
90
+
91
+ claim 토큰을 ./.m1k.json 에 저장했어요.
92
+ 나중에 내 계정에 귀속하려면: npx m1kkit claim
93
+
94
+ 바로 귀속: ${data.claimUrl}
95
+ `);