kcg-code 0.2.1 → 0.2.2
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/dist/web/chunk-0k94bd7w.js +1 -0
- package/dist/web/chunk-1hsbtrt7.js +1 -0
- package/dist/web/chunk-1w20559j.js +2 -0
- package/dist/web/chunk-2g1d0p8k.js +1 -0
- package/dist/web/chunk-2y991ert.js +1 -0
- package/dist/web/chunk-3zr9b6sh.js +1 -0
- package/dist/web/chunk-4587k1kk.js +1 -0
- package/dist/web/chunk-4n8esv61.js +2 -0
- package/dist/web/chunk-5a2wt1wn.js +1 -0
- package/dist/web/chunk-6f4fhpq5.js +1 -0
- package/dist/web/chunk-70gs5473.js +2 -0
- package/dist/web/chunk-7fh46gy9.js +1 -0
- package/dist/web/chunk-7hpamxpk.js +1 -0
- package/dist/web/chunk-7k2ksd6m.js +1 -0
- package/dist/web/chunk-7r3qwgvk.js +1 -0
- package/dist/web/chunk-7vp7mn8t.js +1 -0
- package/dist/web/chunk-7zd31pph.js +2 -0
- package/dist/web/chunk-98m1ye59.js +1 -0
- package/dist/web/chunk-99ac747k.js +98 -0
- package/dist/web/chunk-9e08jfg6.js +1 -0
- package/dist/web/chunk-a4kekjaw.js +2 -0
- package/dist/web/chunk-a7dtx8vc.js +2 -0
- package/dist/web/chunk-aat7sg2w.js +2 -0
- package/dist/web/chunk-adfxb1sz.js +141 -0
- package/dist/web/chunk-c29x9yr7.js +1 -0
- package/dist/web/chunk-c84m5n2b.js +1 -0
- package/dist/web/chunk-deqmvh2y.js +1 -0
- package/dist/web/chunk-dwa0v44t.js +1 -0
- package/dist/web/chunk-ewkw0yxq.js +1 -0
- package/dist/web/chunk-fwhn9kja.js +1 -0
- package/dist/web/chunk-gzmwacrs.js +1 -0
- package/dist/web/chunk-jcnm8ap3.js +1 -0
- package/dist/web/chunk-jhww4q3p.css +1 -0
- package/dist/web/chunk-jt618p78.js +1 -0
- package/dist/web/chunk-k74h7e4d.js +2 -0
- package/dist/web/chunk-n2293q4w.js +2 -0
- package/dist/web/chunk-neqyf1jm.js +1 -0
- package/dist/web/chunk-nnhyekc9.js +1 -0
- package/dist/web/chunk-p5m4kcyp.js +1 -0
- package/dist/web/chunk-pae7wjqv.js +1 -0
- package/dist/web/chunk-pbh1r1h9.js +2 -0
- package/dist/web/chunk-qnky191x.js +1 -0
- package/dist/web/chunk-rachd7gd.js +1 -0
- package/dist/web/chunk-rcvsf7wj.js +2 -0
- package/dist/web/chunk-rj1164tm.js +1 -0
- package/dist/web/chunk-s0wykzpf.js +1 -0
- package/dist/web/chunk-sm5g3apm.js +1 -0
- package/dist/web/chunk-srr11f10.js +2 -0
- package/dist/web/chunk-tsyvyw54.js +2 -0
- package/dist/web/chunk-twh19bw5.js +2 -0
- package/dist/web/chunk-v0b1rz8b.js +1 -0
- package/dist/web/chunk-vcj3m7wp.js +1 -0
- package/dist/web/chunk-wqjjkgh9.js +1 -0
- package/dist/web/chunk-xgcdt3gc.js +2 -0
- package/dist/web/chunk-yenm06gj.js +1 -0
- package/dist/web/chunk-yvhjz3k7.js +12 -0
- package/dist/web/chunk-yymd449s.js +1 -0
- package/dist/web/chunk-z5ns215x.js +2 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo-sg2x36yb.svg +31 -0
- package/dist/web/logo.svg +1 -0
- package/dist/web/manifest.json +18 -0
- package/dist/web/sw.js +67 -0
- package/package.json +4 -3
- package/public/sw.js +20 -2
- package/src/cli/commands.ts +6 -1
- package/src/paths.ts +3 -0
- package/src/server/__tests__/prebuilt-web.test.ts +35 -0
- package/src/server/app.ts +8 -0
- package/src/server/prebuilt-web.ts +46 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "KCG Code",
|
|
3
|
+
"short_name": "KCG Code",
|
|
4
|
+
"description": "Kontrol CLI AI Agent (OpenCode / Claude Code) dari perangkat mobile.",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#0a0a0a",
|
|
8
|
+
"theme_color": "#0a0a0a",
|
|
9
|
+
"lang": "id",
|
|
10
|
+
"icons": [
|
|
11
|
+
{
|
|
12
|
+
"src": "/logo.svg",
|
|
13
|
+
"sizes": "any",
|
|
14
|
+
"type": "image/svg+xml",
|
|
15
|
+
"purpose": "any"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/dist/web/sw.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// KCG Code service worker (Requirement 8.1).
|
|
2
|
+
// - Cache-first untuk asset statis (JS/CSS bundle hasil build.ts).
|
|
3
|
+
// - Network-only (pass-through) untuk `/api/*` dan koneksi WebSocket `/ws`,
|
|
4
|
+
// agar kontrol CLI_Agent tidak pernah disajikan dari cache basi.
|
|
5
|
+
|
|
6
|
+
// Naikkan versi bila strategi cache berubah — cache lama dihapus saat activate.
|
|
7
|
+
const CACHE_NAME = "kcg-code-v2";
|
|
8
|
+
const PRECACHE = ["/", "/manifest.json", "/logo.svg"];
|
|
9
|
+
|
|
10
|
+
self.addEventListener("install", (event) => {
|
|
11
|
+
event.waitUntil(
|
|
12
|
+
caches
|
|
13
|
+
.open(CACHE_NAME)
|
|
14
|
+
.then((cache) => cache.addAll(PRECACHE))
|
|
15
|
+
.catch(() => {}),
|
|
16
|
+
);
|
|
17
|
+
self.skipWaiting();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
self.addEventListener("activate", (event) => {
|
|
21
|
+
event.waitUntil(
|
|
22
|
+
caches
|
|
23
|
+
.keys()
|
|
24
|
+
.then((keys) =>
|
|
25
|
+
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
self.clients.claim();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
self.addEventListener("fetch", (event) => {
|
|
32
|
+
const url = new URL(event.request.url);
|
|
33
|
+
|
|
34
|
+
// Network-only untuk API dan WebSocket.
|
|
35
|
+
if (url.pathname.startsWith("/api/") || url.pathname === "/ws") return;
|
|
36
|
+
|
|
37
|
+
// Network-first untuk halaman: shell HTML lama (chunk usang) tidak boleh
|
|
38
|
+
// terjebak di cache setelah update. Cache hanya dipakai saat offline.
|
|
39
|
+
if (event.request.mode === "navigate") {
|
|
40
|
+
event.respondWith(
|
|
41
|
+
fetch(event.request)
|
|
42
|
+
.then((response) => {
|
|
43
|
+
if (response.ok) {
|
|
44
|
+
const clone = response.clone();
|
|
45
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
46
|
+
}
|
|
47
|
+
return response;
|
|
48
|
+
})
|
|
49
|
+
.catch(() => caches.match(event.request).then((cached) => cached ?? caches.match("/"))),
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cache-first untuk asset statis (chunk ber-hash).
|
|
55
|
+
event.respondWith(
|
|
56
|
+
caches.match(event.request).then((cached) => {
|
|
57
|
+
if (cached) return cached;
|
|
58
|
+
return fetch(event.request).then((response) => {
|
|
59
|
+
if (response.ok && url.origin === self.location.origin) {
|
|
60
|
+
const clone = response.clone();
|
|
61
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
62
|
+
}
|
|
63
|
+
return response;
|
|
64
|
+
});
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcg-code",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Control CLI AI agents (OpenCode / Claude Code) from your phone — mobile PWA + local headless bridge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"src",
|
|
13
13
|
"public",
|
|
14
|
+
"dist/web",
|
|
14
15
|
"styles",
|
|
15
16
|
"bun-env.d.ts",
|
|
16
17
|
"tsconfig.json",
|
|
@@ -40,8 +41,8 @@
|
|
|
40
41
|
"lint:fix": "biome check --write .",
|
|
41
42
|
"format": "biome format --write .",
|
|
42
43
|
"typecheck": "tsc --noEmit",
|
|
43
|
-
"prepack": "bun scripts/stamp-build-config.ts",
|
|
44
|
-
"postpack": "bun scripts/stamp-build-config.ts --reset"
|
|
44
|
+
"prepack": "bun scripts/stamp-build-config.ts && bun run build.ts --outdir dist/web --no-sourcemap",
|
|
45
|
+
"postpack": "bun scripts/stamp-build-config.ts --reset && bun -e \"require('node:fs').rmSync('dist/web', { recursive: true, force: true })\""
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@anthropic-ai/claude-agent-sdk": "^0.3.296",
|
package/public/sw.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// - Network-only (pass-through) untuk `/api/*` dan koneksi WebSocket `/ws`,
|
|
4
4
|
// agar kontrol CLI_Agent tidak pernah disajikan dari cache basi.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Naikkan versi bila strategi cache berubah — cache lama dihapus saat activate.
|
|
7
|
+
const CACHE_NAME = "kcg-code-v2";
|
|
7
8
|
const PRECACHE = ["/", "/manifest.json", "/logo.svg"];
|
|
8
9
|
|
|
9
10
|
self.addEventListener("install", (event) => {
|
|
@@ -33,7 +34,24 @@ self.addEventListener("fetch", (event) => {
|
|
|
33
34
|
// Network-only untuk API dan WebSocket.
|
|
34
35
|
if (url.pathname.startsWith("/api/") || url.pathname === "/ws") return;
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
+
// Network-first untuk halaman: shell HTML lama (chunk usang) tidak boleh
|
|
38
|
+
// terjebak di cache setelah update. Cache hanya dipakai saat offline.
|
|
39
|
+
if (event.request.mode === "navigate") {
|
|
40
|
+
event.respondWith(
|
|
41
|
+
fetch(event.request)
|
|
42
|
+
.then((response) => {
|
|
43
|
+
if (response.ok) {
|
|
44
|
+
const clone = response.clone();
|
|
45
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
46
|
+
}
|
|
47
|
+
return response;
|
|
48
|
+
})
|
|
49
|
+
.catch(() => caches.match(event.request).then((cached) => cached ?? caches.match("/"))),
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cache-first untuk asset statis (chunk ber-hash).
|
|
37
55
|
event.respondWith(
|
|
38
56
|
caches.match(event.request).then((cached) => {
|
|
39
57
|
if (cached) return cached;
|
package/src/cli/commands.ts
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
resolveEffectiveConfigPath,
|
|
14
14
|
resolveEffectiveDbPath,
|
|
15
15
|
resolveEffectiveUploadsDir,
|
|
16
|
+
WEB_DIST_DIR,
|
|
16
17
|
} from "../paths";
|
|
17
18
|
import { isCliMode } from "../runtime";
|
|
18
19
|
import { createKcgServer } from "../server/app";
|
|
19
20
|
import { resolveHostname } from "../server/host";
|
|
21
|
+
import { loadPrebuiltWeb } from "../server/prebuilt-web";
|
|
20
22
|
import { createAuthService } from "../server/services/auth";
|
|
21
23
|
import type { CliArgs } from "./args";
|
|
22
24
|
import { attachToDaemon } from "./attach";
|
|
@@ -110,10 +112,13 @@ export async function runStart(args: CliArgs): Promise<void> {
|
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// Paket npm membawa frontend prebuilt; checkout git jatuh ke bundling runtime.
|
|
116
|
+
const web = loadPrebuiltWeb(WEB_DIST_DIR);
|
|
113
117
|
const boot = (port: number) =>
|
|
114
118
|
createKcgServer({
|
|
115
119
|
config,
|
|
116
|
-
spa,
|
|
120
|
+
spa: web?.spa ?? spa,
|
|
121
|
+
assets: web?.assets,
|
|
117
122
|
spaPaths: SPA_PATHS,
|
|
118
123
|
port,
|
|
119
124
|
hostname,
|
package/src/paths.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const PACKAGE_ROOT = path.resolve(import.meta.dir, "..");
|
|
|
20
20
|
/** Direktori asset statis PWA di dalam paket. */
|
|
21
21
|
export const PUBLIC_DIR = path.join(PACKAGE_ROOT, "public");
|
|
22
22
|
|
|
23
|
+
/** Frontend hasil build yang ikut paket npm (`build.ts --outdir dist/web`, saat prepack). */
|
|
24
|
+
export const WEB_DIST_DIR = path.join(PACKAGE_ROOT, "dist", "web");
|
|
25
|
+
|
|
23
26
|
/** Direktori aplikasi milik user: `~/.kcgcode`. */
|
|
24
27
|
export const KCG_HOME = path.join(homedir(), ".kcgcode");
|
|
25
28
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { loadPrebuiltWeb } from "../prebuilt-web";
|
|
6
|
+
|
|
7
|
+
describe("loadPrebuiltWeb", () => {
|
|
8
|
+
let dir = "";
|
|
9
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
10
|
+
|
|
11
|
+
test("null bila belum di-build", () => {
|
|
12
|
+
dir = mkdtempSync(path.join(tmpdir(), "kcg-web-"));
|
|
13
|
+
expect(loadPrebuiltWeb(dir)).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("shell SPA + rute chunk; sourcemap tidak diekspos", async () => {
|
|
17
|
+
dir = mkdtempSync(path.join(tmpdir(), "kcg-web-"));
|
|
18
|
+
writeFileSync(path.join(dir, "index.html"), "<!doctype html><title>x</title>");
|
|
19
|
+
writeFileSync(path.join(dir, "chunk-abc123.js"), "console.log(1)");
|
|
20
|
+
writeFileSync(path.join(dir, "chunk-abc123.js.map"), "{}");
|
|
21
|
+
mkdirSync(path.join(dir, "icons"));
|
|
22
|
+
writeFileSync(path.join(dir, "icons", "a.png"), "png");
|
|
23
|
+
|
|
24
|
+
const web = loadPrebuiltWeb(dir);
|
|
25
|
+
expect(web).not.toBeNull();
|
|
26
|
+
if (!web) return;
|
|
27
|
+
expect(Object.keys(web.assets).sort()).toEqual(["/chunk-abc123.js", "/icons/a.png"]);
|
|
28
|
+
expect(web.spa.headers.get("content-type")).toContain("text/html");
|
|
29
|
+
|
|
30
|
+
const chunk = web.assets["/chunk-abc123.js"]?.() as Response;
|
|
31
|
+
expect(await chunk.text()).toBe("console.log(1)");
|
|
32
|
+
expect(chunk.headers.get("cache-control")).toContain("immutable");
|
|
33
|
+
expect(web.assets["/icons/a.png"]?.().headers.get("cache-control")).toBe("no-cache");
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/server/app.ts
CHANGED
|
@@ -114,6 +114,11 @@ export interface KcgServerOptions {
|
|
|
114
114
|
* (`src/index.ts`) persis seperti daftar rute SPA kcgcode; path lain 404.
|
|
115
115
|
*/
|
|
116
116
|
spaPaths?: string[];
|
|
117
|
+
/**
|
|
118
|
+
* Asset frontend hasil build (`dist/web`) per path URL — dipakai binary
|
|
119
|
+
* global sebagai pengganti bundling HTML saat runtime.
|
|
120
|
+
*/
|
|
121
|
+
assets?: Record<string, () => Response>;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
export interface KcgServer {
|
|
@@ -321,6 +326,9 @@ export function createKcgServer(opts: KcgServerOptions = {}): KcgServer {
|
|
|
321
326
|
hostname,
|
|
322
327
|
port,
|
|
323
328
|
routes: {
|
|
329
|
+
// ---- Chunk frontend prebuilt (bila ada); rute eksplisit di bawah menang ----
|
|
330
|
+
...opts.assets,
|
|
331
|
+
|
|
324
332
|
// ---- Asset statis PWA dari root paket (bukan cwd) — aman utk global install ----
|
|
325
333
|
"/manifest.json": () =>
|
|
326
334
|
staticFile(path.join(PUBLIC_DIR, "manifest.json"), "application/manifest+json"),
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend hasil build (`build.ts --outdir dist/web`) yang ikut paket npm.
|
|
3
|
+
*
|
|
4
|
+
* Binary global memakai ini, bukan bundling HTML import saat runtime: di
|
|
5
|
+
* node_modules global bisa ada dua salinan React (library ter-hoist memakai
|
|
6
|
+
* React milik paket lain → "Cannot read properties of null (reading
|
|
7
|
+
* 'useState')"), dan plugin Tailwind dari bunfig.toml hanya terbaca bila cwd
|
|
8
|
+
* = root repo. Build dilakukan sekali di repo (prepack) dengan satu React.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
export type AssetRoutes = Record<string, () => Response>;
|
|
14
|
+
|
|
15
|
+
export interface PrebuiltWeb {
|
|
16
|
+
/** Shell SPA (index.html) untuk semua `spaPaths`. */
|
|
17
|
+
spa: Response;
|
|
18
|
+
/** Chunk JS/CSS & asset lain, per path URL. */
|
|
19
|
+
assets: AssetRoutes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Chunk ber-hash aman di-cache selamanya; selain itu selalu revalidasi. */
|
|
23
|
+
function cacheControl(file: string): string {
|
|
24
|
+
return /^chunk-[a-z0-9]+\./.test(path.basename(file)) || /-[a-z0-9]{8}\.\w+$/.test(file)
|
|
25
|
+
? "public, max-age=31536000, immutable"
|
|
26
|
+
: "no-cache";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Rute frontend dari `dir`; null bila belum di-build (mis. checkout git). */
|
|
30
|
+
export function loadPrebuiltWeb(dir: string): PrebuiltWeb | null {
|
|
31
|
+
const indexPath = path.join(dir, "index.html");
|
|
32
|
+
if (!existsSync(indexPath)) return null;
|
|
33
|
+
|
|
34
|
+
const assets: AssetRoutes = {};
|
|
35
|
+
for (const rel of new Bun.Glob("**/*").scanSync({ cwd: dir })) {
|
|
36
|
+
if (rel === "index.html" || rel.endsWith(".map")) continue;
|
|
37
|
+
const file = path.join(dir, rel);
|
|
38
|
+
const headers = { "cache-control": cacheControl(rel) };
|
|
39
|
+
assets[`/${rel.split(path.sep).join("/")}`] = () => new Response(Bun.file(file), { headers });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const spa = new Response(readFileSync(indexPath), {
|
|
43
|
+
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" },
|
|
44
|
+
});
|
|
45
|
+
return { spa, assets };
|
|
46
|
+
}
|