@m1kapp/kit 0.0.47 → 0.0.49
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/README.md +16 -0
- package/bin/claim.mjs +1 -1
- package/bin/m1kkit.mjs +6 -0
- package/bin/pwa.mjs +200 -0
- package/bin/skills/m1kapp-clean.md +2 -2
- package/bin/skills/m1kapp-pwa.md +4 -0
- package/bin/skills/m1kapp-stats.md +3 -3
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +3 -3
- package/dist/meta.json +2 -2
- package/dist/pwa.js +1 -1
- package/dist/pwa.mjs +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -542,6 +542,22 @@ npx m1kkit favicon --appname="My App" --slogan="한 줄 소개" --color=#3b82f6
|
|
|
542
542
|
`manifest.json` 7개가 `public/`(자동 감지)에 한 번에 생기고, `<head>`에 붙여넣을
|
|
543
543
|
태그 스니펫도 콘솔에 출력됩니다.
|
|
544
544
|
|
|
545
|
+
### 오프라인 + 스플래시 (`m1kkit pwa`)
|
|
546
|
+
|
|
547
|
+
favicon 다음에 한 번 돌리면 홈 화면 앱이 **비행기 모드에서도 열리고**, 아이폰에서 흰 화면 대신 스플래시가 뜹니다.
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
npx m1kkit pwa # public/icon-512.png 를 흰 배경 가운데에
|
|
551
|
+
npx m1kkit pwa --bg=#0f0f1a # 앱 첫 화면 색과 맞춘다(다르면 열릴 때 번쩍인다)
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
`sw.js` · `splash/<가로>x<세로>.png` 13종이 생기고, `manifest.json` 의 `background_color`(안드로이드 스플래시)를 같은 색으로 맞춥니다.
|
|
555
|
+
Vite 는 `index.html` `<head>` 에 태그가 들어가고(다시 돌리면 교체), Next 는 출력된 태그를 layout 에 붙이세요.
|
|
556
|
+
서비스워커는 localhost 에선 등록하지 않습니다 — 확인은 배포 뒤에.
|
|
557
|
+
|
|
558
|
+
갱신: 온라인으로 열면 매번 새 배포(페이지는 네트워크 먼저). 켜 둔 채 다시 켜는 앱은 돌아올 때 30분에 한 번 새 배포를 확인해 새로고침합니다.
|
|
559
|
+
`sw.js` 는 배포마다 바꿀 필요가 없고, kit 을 올린 뒤 `npx m1kkit pwa` 를 다시 돌리면 그 판의 서비스워커로 바뀝니다.
|
|
560
|
+
|
|
545
561
|
### 폰트 & 이모지
|
|
546
562
|
|
|
547
563
|
```tsx
|
package/bin/claim.mjs
CHANGED
|
@@ -70,7 +70,7 @@ if (!flags["no-stats"]) {
|
|
|
70
70
|
if (typeof flags.out === "string") statsArgs.push(`--out=${flags.out}`);
|
|
71
71
|
const r = spawnSync(process.execPath, statsArgs, { stdio: "inherit" });
|
|
72
72
|
if (r.status !== 0) {
|
|
73
|
-
console.log("\n(분석은 건너뛰었어요 — 나중에 'npx m1kkit stats' 로 직접 실행할 수 있어요)");
|
|
73
|
+
console.log("\n(분석은 건너뛰었어요 — 나중에 'npx --package=@m1kapp/kit m1kkit stats' 로 직접 실행할 수 있어요)");
|
|
74
74
|
} else {
|
|
75
75
|
console.log("\n✓ 분석 결과는 kit-stats.json 에 저장됐고, 하단 'powered by' 크레딧 시트에서도 보여요.");
|
|
76
76
|
}
|
package/bin/m1kkit.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* m1kkit favicon — 파비콘 생성
|
|
7
7
|
* m1kkit skills — Claude Code 스킬 설치
|
|
8
8
|
* m1kkit stats — 코드 분석 & kit 사용 현황 생성
|
|
9
|
+
* m1kkit pwa — 오프라인 실행(sw.js) + iOS 스플래시
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const [,, command, ...rest] = process.argv;
|
|
@@ -17,6 +18,7 @@ m1kkit — @m1kapp/kit CLI
|
|
|
17
18
|
Commands:
|
|
18
19
|
m1kkit new <name> 새 앱 생성 (Vite + React + kit 앱쉘 템플릿)
|
|
19
20
|
m1kkit favicon [options] 파비콘 자동 생성
|
|
21
|
+
m1kkit pwa [options] 오프라인 실행(sw.js) + iOS 스플래시 — favicon 다음에
|
|
20
22
|
m1kkit skills [options] Claude Code 스킬 설치
|
|
21
23
|
m1kkit stats [options] 코드 분석 & kit 사용 현황 생성
|
|
22
24
|
m1kkit track <url> m1k 방문자 트래커에 사이트 등록(무로그인)
|
|
@@ -29,6 +31,7 @@ Options:
|
|
|
29
31
|
Examples:
|
|
30
32
|
m1kkit new moodlog --color=#e2603f
|
|
31
33
|
m1kkit favicon --text=K --color=#3B82F6
|
|
34
|
+
m1kkit pwa --bg=#ffffff
|
|
32
35
|
m1kkit skills
|
|
33
36
|
m1kkit skills --list
|
|
34
37
|
m1kkit skills m1kapp-init
|
|
@@ -52,6 +55,9 @@ if (command === "new") {
|
|
|
52
55
|
} else if (command === "favicon") {
|
|
53
56
|
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
54
57
|
await import(path.join(__dirname, "favicon.mjs"));
|
|
58
|
+
} else if (command === "pwa") {
|
|
59
|
+
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
60
|
+
await import(path.join(__dirname, "pwa.mjs"));
|
|
55
61
|
} else if (command === "skills") {
|
|
56
62
|
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
57
63
|
await import(path.join(__dirname, "skills.mjs"));
|
package/bin/pwa.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* m1kkit pwa — 오프라인 실행 + iOS 스플래시
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx m1kkit pwa # public/manifest.json · public/icon-512.png 를 읽는다
|
|
7
|
+
* npx m1kkit pwa --bg=#ffffff # 스플래시 배경색(기본: 흰색 — 앱 첫 화면과 같은 색이라야 번쩍임이 없다)
|
|
8
|
+
* npx m1kkit pwa --icon=public/logo.png --out=static
|
|
9
|
+
*
|
|
10
|
+
* 생성: sw.js · splash/<가로>x<세로>.png (아이폰 세로 화면별)
|
|
11
|
+
* Vite 처럼 루트에 index.html 이 있으면 <head> 에 태그까지 넣는다. Next 는 출력된 태그를 layout 에 붙인다.
|
|
12
|
+
* manifest.json 이 있으면 background_color 를 스플래시 배경과 맞춘다(안드로이드 스플래시가 그 색).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ImageResponse } from "@vercel/og";
|
|
16
|
+
import { createElement as h } from "react";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
|
|
20
|
+
const args = Object.fromEntries(
|
|
21
|
+
process.argv.slice(2).filter((a) => a.startsWith("--")).map((a) => {
|
|
22
|
+
const [k, ...v] = a.slice(2).split("=");
|
|
23
|
+
return [k, v.join("=") || true];
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const outDir = path.resolve(cwd, args.out ?? "public");
|
|
29
|
+
const iconPath = path.resolve(cwd, args.icon ?? path.join(outDir, "icon-512.png"));
|
|
30
|
+
const bg = args.bg ?? "#ffffff";
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(iconPath)) {
|
|
33
|
+
console.error(`아이콘이 없어요: ${path.relative(cwd, iconPath)} — 먼저 npx m1kkit favicon 을 돌리거나 --icon 으로 지정하세요.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function write(rel, data) {
|
|
38
|
+
const file = path.join(outDir, rel);
|
|
39
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
+
fs.writeFileSync(file, data);
|
|
41
|
+
console.log(`✓ ${path.relative(cwd, file)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ─── 서비스워커 ─── */
|
|
45
|
+
write("sw.js", `// m1kkit pwa 가 만든 파일 — 오프라인에서도 앱이 열리게 한다. 다시 만들려면 npx m1kkit pwa
|
|
46
|
+
// 페이지: 네트워크 먼저, 3초 안에 안 오거나 끊겨 있으면 저장본(여행지 로밍처럼 느린 망에서도 바로 뜬다).
|
|
47
|
+
// 그래서 새 배포는 온라인이면 다음 실행에 바로 보인다. 이 파일은 배포마다 바꿀 필요가 없다.
|
|
48
|
+
// JS·CSS·폰트·이미지: 저장본을 바로 주고 뒤에서 새로 받아 둔다. API(fetch) 요청은 건드리지 않는다.
|
|
49
|
+
// ponytail: 캐시 이름이 하나라 배포마다 옛 해시 파일이 조금씩 쌓인다 — 커지면 savePage 에서 안 쓰는 것 정리
|
|
50
|
+
const CACHE = "m1kkit-pwa";
|
|
51
|
+
const ASSET = ["script", "style", "font", "image", "manifest"];
|
|
52
|
+
|
|
53
|
+
// SPA 호스팅(Vercel rewrites 등)은 없는 파일에도 index.html 을 200 으로 준다.
|
|
54
|
+
// 그걸 JS 자리에 담으면 다음 오프라인 실행이 하얗게 뜬다 — 페이지가 아닌데 HTML 이면 안 담는다.
|
|
55
|
+
const keepable = (req, res) => res.type === "opaque" ||
|
|
56
|
+
(res.ok && (req.mode === "navigate" || !(res.headers.get("content-type") ?? "").includes("text/html")));
|
|
57
|
+
|
|
58
|
+
// 페이지는 그 페이지가 부르는 JS·CSS 를 다 담은 뒤에 담는다 — 저장된 페이지가 없는 파일을 가리키는 순간이 없게.
|
|
59
|
+
// 하나라도 못 받으면 페이지는 옛 판을 그대로 둔다(옛 판의 파일은 남아 있다).
|
|
60
|
+
async function savePage(c, key, res) {
|
|
61
|
+
const html = await res.clone().text();
|
|
62
|
+
const own = [...html.matchAll(/(?:src|href)="(\\/[^"/][^"]*)"/g)].map((m) => new Request(m[1]));
|
|
63
|
+
// CDN 폰트 CSS 도(Pretendard·Tossface 같은) — 빠지면 오프라인에서 글꼴·국기 이모지가 바뀐다. 실패해도 페이지는 담는다
|
|
64
|
+
const cdn = [...html.matchAll(/href="(https:\\/\\/[^"]+\\.css)"/g)].map((m) => new Request(m[1], { mode: "no-cors" }));
|
|
65
|
+
const got = (req) => c.match(req, { ignoreVary: true }).then(async (hit) => {
|
|
66
|
+
if (hit) return true;
|
|
67
|
+
const r = await fetch(req).catch(() => null); // add() 는 opaque 를 거절해서 직접 담는다
|
|
68
|
+
return !!r && keepable(req, r) && c.put(req, r).then(() => true);
|
|
69
|
+
});
|
|
70
|
+
const ok = await Promise.all(own.map(got));
|
|
71
|
+
await Promise.all(cdn.map(got));
|
|
72
|
+
if (ok.every(Boolean)) await c.put(key, res);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
self.addEventListener("install", (e) => {
|
|
76
|
+
e.waitUntil((async () => {
|
|
77
|
+
const res = await fetch("/", { cache: "no-store" });
|
|
78
|
+
if (res.ok) await savePage(await caches.open(CACHE), "/", res);
|
|
79
|
+
await self.skipWaiting();
|
|
80
|
+
})());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
|
|
84
|
+
|
|
85
|
+
self.addEventListener("fetch", (e) => {
|
|
86
|
+
const r = e.request;
|
|
87
|
+
if (r.method !== "GET" || !r.url.startsWith("http")) return;
|
|
88
|
+
if (r.mode === "navigate") {
|
|
89
|
+
const net = fetch(r);
|
|
90
|
+
// 복사는 응답이 화면으로 가기 전에, 같은 차례에서 — 늦으면 body already used
|
|
91
|
+
e.waitUntil(net.then((res) => { if (!res.ok) return; const copy = res.clone(); return caches.open(CACHE).then((c) => savePage(c, r, copy)); }).catch(() => {}));
|
|
92
|
+
return e.respondWith(page(r, net));
|
|
93
|
+
}
|
|
94
|
+
const same = new URL(r.url).origin === location.origin;
|
|
95
|
+
if (same ? ASSET.includes(r.destination) : r.destination === "style" || r.destination === "font") e.respondWith(stale(r));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
async function page(r, net) {
|
|
99
|
+
const c = await caches.open(CACHE);
|
|
100
|
+
const saved = () => c.match(r, { ignoreSearch: true, ignoreVary: true }).then((m) => m ?? c.match("/"));
|
|
101
|
+
const slow = new Promise((ok) => setTimeout(ok, 3000)).then(async () => (await saved()) ?? net);
|
|
102
|
+
try { return await Promise.race([net, slow]); } catch { return (await saved()) ?? Response.error(); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function stale(r) {
|
|
106
|
+
const c = await caches.open(CACHE);
|
|
107
|
+
const hit = await c.match(r, { ignoreVary: true }); // Vary: Origin 서버에서 저장본을 못 찾던 것
|
|
108
|
+
const net = fetch(r).then((res) => { if (keepable(r, res)) c.put(r, res.clone()); return res; });
|
|
109
|
+
if (!hit) return net;
|
|
110
|
+
net.catch(() => {});
|
|
111
|
+
return hit;
|
|
112
|
+
}
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
/* ─── iOS 스플래시 ─── */
|
|
116
|
+
// iOS 는 manifest 를 안 보고 기기 해상도와 정확히 같은 그림만 쓴다. 안 맞으면 흰 화면.
|
|
117
|
+
// [CSS 폭, CSS 높이, 배율] — 세로 화면만(앱이 세로 고정). 아이패드는 뺐다.
|
|
118
|
+
const DEVICES = [
|
|
119
|
+
[440, 956, 3], // 16 Pro Max · 17 Pro Max
|
|
120
|
+
[402, 874, 3], // 16 Pro · 17 · 17 Pro
|
|
121
|
+
[420, 912, 3], // Air
|
|
122
|
+
[430, 932, 3], // 14 Pro Max · 15 Plus · 15 Pro Max · 16 Plus
|
|
123
|
+
[393, 852, 3], // 14 Pro · 15 · 15 Pro · 16
|
|
124
|
+
[428, 926, 3], // 12 Pro Max · 13 Pro Max · 14 Plus
|
|
125
|
+
[390, 844, 3], // 12 · 13 · 14 · 16e
|
|
126
|
+
[375, 812, 3], // X · XS · 11 Pro · 12 mini · 13 mini
|
|
127
|
+
[414, 896, 3], // XS Max · 11 Pro Max
|
|
128
|
+
[414, 896, 2], // XR · 11
|
|
129
|
+
[414, 736, 3], // 6+ ~ 8 Plus
|
|
130
|
+
[375, 667, 2], // 6 ~ 8 · SE 2·3
|
|
131
|
+
[320, 568, 2], // SE 1
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const icon = `data:image/png;base64,${fs.readFileSync(iconPath).toString("base64")}`;
|
|
135
|
+
const links = [];
|
|
136
|
+
for (const [w, h_, dpr] of DEVICES) {
|
|
137
|
+
const W = w * dpr, H = h_ * dpr, size = Math.round(W * 0.28); // 아이콘은 화면 폭의 28% — 홈 화면 아이콘이 커지며 열리는 느낌
|
|
138
|
+
const res = new ImageResponse(
|
|
139
|
+
h("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: bg } },
|
|
140
|
+
h("img", { src: icon, width: size, height: size, style: { borderRadius: size * 0.225 } })),
|
|
141
|
+
{ width: W, height: H }
|
|
142
|
+
);
|
|
143
|
+
write(`splash/${W}x${H}.png`, Buffer.from(await res.arrayBuffer()));
|
|
144
|
+
links.push(`<link rel="apple-touch-startup-image" media="(device-width: ${w}px) and (device-height: ${h_}px) and (-webkit-device-pixel-ratio: ${dpr}) and (orientation: portrait)" href="/splash/${W}x${H}.png" />`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ─── manifest 배경색 ─── */
|
|
148
|
+
const manifestPath = path.join(outDir, "manifest.json");
|
|
149
|
+
let title;
|
|
150
|
+
if (fs.existsSync(manifestPath)) {
|
|
151
|
+
const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
152
|
+
title = m.short_name ?? m.name;
|
|
153
|
+
if (m.background_color !== bg) {
|
|
154
|
+
m.background_color = bg;
|
|
155
|
+
write("manifest.json", JSON.stringify(m, null, 2) + "\n");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ─── <head> 태그 ─── */
|
|
160
|
+
// apple-mobile-web-app-capable 이 없으면 iOS 가 스플래시를 아예 안 쓴다.
|
|
161
|
+
// 서비스워커는 localhost 에선 안 건다 — 개발 중 HMR 이 저장본에 막히지 않게.
|
|
162
|
+
// 등록 스크립트는 켜 둔 앱이 돌아올 때 새 배포를 확인한다 — 내 JS 파일 이름이 새 HTML 에 없으면 새로고침.
|
|
163
|
+
const head = [
|
|
164
|
+
`<!-- m1kkit:pwa -->`,
|
|
165
|
+
`<meta name="apple-mobile-web-app-capable" content="yes" />`,
|
|
166
|
+
`<meta name="mobile-web-app-capable" content="yes" />`,
|
|
167
|
+
`<meta name="apple-mobile-web-app-status-bar-style" content="default" />`,
|
|
168
|
+
...(title ? [`<meta name="apple-mobile-web-app-title" content="${title}" />`] : []),
|
|
169
|
+
`<meta name="theme-color" content="${bg}" />`,
|
|
170
|
+
...links,
|
|
171
|
+
`<script>
|
|
172
|
+
if ("serviceWorker" in navigator && !/^(localhost|127\\.|\\[::1\\])/.test(location.hostname)) {
|
|
173
|
+
navigator.serviceWorker.register("/sw.js");
|
|
174
|
+
// 홈 화면 앱은 닫히지 않고 며칠씩 다시 켜지기만 한다 — 돌아올 때(30분에 한 번) 새 배포가 있으면 새로고침
|
|
175
|
+
let mine = [], checked = Date.now();
|
|
176
|
+
addEventListener("DOMContentLoaded", () => { mine = [...document.scripts].map((s) => s.getAttribute("src")).filter((s) => s && s[0] === "/"); });
|
|
177
|
+
document.addEventListener("visibilitychange", () => {
|
|
178
|
+
if (document.hidden || Date.now() - checked < 18e5) return;
|
|
179
|
+
checked = Date.now();
|
|
180
|
+
fetch(location.pathname, { cache: "no-store" }).then((r) => (r.ok ? r.text() : ""))
|
|
181
|
+
.then((html) => { if (html && mine.some((s) => !html.includes(s))) location.reload(); }, () => {});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
</script>`,
|
|
185
|
+
`<!-- /m1kkit:pwa -->`,
|
|
186
|
+
].join("\n ");
|
|
187
|
+
|
|
188
|
+
const indexPath = path.join(cwd, "index.html");
|
|
189
|
+
if (fs.existsSync(indexPath)) {
|
|
190
|
+
const html = fs.readFileSync(indexPath, "utf8");
|
|
191
|
+
const next = /<!-- m1kkit:pwa -->[\s\S]*?<!-- \/m1kkit:pwa -->/.test(html)
|
|
192
|
+
? html.replace(/<!-- m1kkit:pwa -->[\s\S]*?<!-- \/m1kkit:pwa -->/, head)
|
|
193
|
+
: html.replace(/\s*<\/head>/, `\n ${head}\n </head>`);
|
|
194
|
+
fs.writeFileSync(indexPath, next);
|
|
195
|
+
console.log(`✓ index.html <head> 에 PWA 태그`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(`\n<head> 에 붙여넣기:\n ${head}\n`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(`\n🎉 오프라인 + 스플래시 ${DEVICES.length}종 (배경 ${bg}). 배포 후 홈 화면 앱을 비행기 모드로 열어 확인하세요.`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
`npx m1kkit stats`(내부적으로 fixearly)가 낸 점수를 근거로, **동작을 바꾸지 않고** 코드를 정리한다.
|
|
1
|
+
`npx --package=@m1kapp/kit m1kkit stats`(내부적으로 fixearly)가 낸 점수를 근거로, **동작을 바꾸지 않고** 코드를 정리한다.
|
|
2
2
|
목적은 등급 숫자가 아니라 **다음에 이 코드를 고칠 때 드는 비용**을 줄이는 것이다.
|
|
3
3
|
|
|
4
4
|
---
|
|
@@ -60,7 +60,7 @@ cognitive = 중첩 깊이 가중(SonarSource S3776). `a && b && c`는 1, 5중
|
|
|
60
60
|
## Step 0: 진단
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
npm run stats:report # 또는 npx m1kkit stats --report
|
|
63
|
+
npm run stats:report # 또는 npx --package=@m1kapp/kit m1kkit stats --report
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
리포트(HTML)에서 이 순서로 읽는다:
|
package/bin/skills/m1kapp-pwa.md
CHANGED
|
@@ -13,6 +13,7 @@ description: "PWA 설정을 점검하고 @m1kapp/kit/pwa 유틸로 자동 적용
|
|
|
13
13
|
- 아이콘: `public/icon-192.png`, `public/icon-512.png` 여부
|
|
14
14
|
- `public/favicon.ico` 여부
|
|
15
15
|
- 설치 유도 버튼 (`useInstallPrompt`) 사용 여부
|
|
16
|
+
- 오프라인: `public/sw.js` 와 등록 스크립트 · iOS 스플래시: `apple-touch-startup-image` 태그
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -45,6 +46,9 @@ manifest가 없으면:
|
|
|
45
46
|
- Vite 등 App Router가 없는 프로젝트면 `npx m1kkit favicon`이 만든
|
|
46
47
|
`public/manifest.json`을 그대로 쓰면 됨 — 따로 만들 필요 없음.
|
|
47
48
|
|
|
49
|
+
sw.js 나 스플래시가 없으면: 아이콘이 생긴 뒤 `npx m1kkit pwa --bg="[앱 첫 화면 배경색]"`.
|
|
50
|
+
배경색은 테마색이 아니라 **앱이 처음 그리는 화면색**(보통 흰색) — 다르면 스플래시에서 앱으로 넘어갈 때 번쩍인다.
|
|
51
|
+
|
|
48
52
|
viewport가 없으면: `mobileViewport` 적용
|
|
49
53
|
|
|
50
54
|
```ts
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
올바른 `--dir`/`--out`으로 한 번 돌려 데이터가 생기는지 확인한다.
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx m1kkit stats --dir=. --out=public # App Router(루트가 app/)
|
|
22
|
+
npx --package=@m1kapp/kit m1kkit stats --dir=. --out=public # App Router(루트가 app/)
|
|
23
23
|
# 또는
|
|
24
|
-
npx m1kkit stats --dir=src --out=public # src/ 구조
|
|
24
|
+
npx --package=@m1kapp/kit m1kkit stats --dir=src --out=public # src/ 구조
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
- 성공하면 `public/kit-stats.json`이 생기고 `컴포넌트 N/30 · 절약 X줄` 요약이 출력된다.
|
|
@@ -52,7 +52,7 @@ npx m1kkit stats --dir=src --out=public # src/ 구조
|
|
|
52
52
|
## Step 3: 패널에서 확인
|
|
53
53
|
|
|
54
54
|
- `PoweredByKit`(보통 `Watermark`가 자동 포함)을 열어 데이터가 뜨는지 본다.
|
|
55
|
-
- **데이터가 이미 있으면** 패널 하단에 `갱신하려면 npx m1kkit stats` 안내가 보인다 — 빈 상태(명령어 실행 안내)는 더 이상 안 나온다.
|
|
55
|
+
- **데이터가 이미 있으면** 패널 하단에 `갱신하려면 npx --package=@m1kapp/kit m1kkit stats` 안내가 보인다 — 빈 상태(명령어 실행 안내)는 더 이상 안 나온다.
|
|
56
56
|
- 컴포넌트 사용률이 낮으면(예: 7/30) 더 쓸 만한 kit 컴포넌트를 권하고, 채택 후 stats를 **다시 돌려** 수치를 갱신한다.
|
|
57
57
|
|
|
58
58
|
---
|
package/dist/index.d.mts
CHANGED
|
@@ -1013,7 +1013,7 @@ interface PoweredByKitProps {
|
|
|
1013
1013
|
slug?: string;
|
|
1014
1014
|
/** Tracker host. Default: "www.m1k.app" (apex m1k.app는 www로 307 리다이렉트 → cross-origin fetch가 CORS로 깨지므로 www 직결) */
|
|
1015
1015
|
trackerHost?: string;
|
|
1016
|
-
/** Set false to disable the visitor beacon
|
|
1016
|
+
/** Set false to disable the visitor beacon; counts and carousel still display when a slug is present */
|
|
1017
1017
|
track?: boolean;
|
|
1018
1018
|
/**
|
|
1019
1019
|
* Whether the registered site has been claimed (귀속/인증) to an m1k.app account.
|
package/dist/index.d.ts
CHANGED
|
@@ -1013,7 +1013,7 @@ interface PoweredByKitProps {
|
|
|
1013
1013
|
slug?: string;
|
|
1014
1014
|
/** Tracker host. Default: "www.m1k.app" (apex m1k.app는 www로 307 리다이렉트 → cross-origin fetch가 CORS로 깨지므로 www 직결) */
|
|
1015
1015
|
trackerHost?: string;
|
|
1016
|
-
/** Set false to disable the visitor beacon
|
|
1016
|
+
/** Set false to disable the visitor beacon; counts and carousel still display when a slug is present */
|
|
1017
1017
|
track?: boolean;
|
|
1018
1018
|
/**
|
|
1019
1019
|
* Whether the registered site has been claimed (귀속/인증) to an m1k.app account.
|