@brightsideoy/kama 1.0.0-alpha.4 → 1.0.0-alpha.6
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/cli.js +156 -278
- package/brightsideoy-kama-1.0.0-alpha.5.tgz +0 -0
- package/dist/assets/index-CHQAZAAH.css +1 -0
- package/dist/assets/{index-CiW_gEtr.js → index-g1H_KYcg.js} +46 -46
- package/dist/{dist-rKCoLpZH.js → dist-BmDMKgKk.js} +415 -367
- package/dist/index.html +2 -2
- package/dist/index.js +415 -367
- package/dist/schema-compiler.js +1 -1
- package/package.json +1 -1
- package/templates/astro-starter/src/middleware.ts +115 -42
- package/templates/astro-starter/src/pages/[...slug].astro +51 -31
- package/dist/assets/index-DHKk9t5r.css +0 -1
package/dist/schema-compiler.js
CHANGED
|
@@ -30,7 +30,7 @@ async function f() {
|
|
|
30
30
|
if (n(f)) l = (await import(t(f).href)).compileDefinitions;
|
|
31
31
|
else if (n(p)) l = (await import(t(p).href)).compileDefinitions;
|
|
32
32
|
else try {
|
|
33
|
-
l = (await import("./dist-
|
|
33
|
+
l = (await import("./dist-BmDMKgKk.js")).compileDefinitions;
|
|
34
34
|
} catch (e) {
|
|
35
35
|
console.error("\x1B[31m ✖\x1B[0m \x1B[31mCould not resolve Kama Schema Engine:\x1B[0m", e), process.exit(1);
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -85,7 +85,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING
|
|
88
|
+
// 🎯 0B. DIRECT MEDIA PROXY VIA SERVICE BINDING WITH SURGICAL WORKER CACHING
|
|
88
89
|
if (pathname.startsWith("/media/")) {
|
|
90
|
+
const cache = typeof caches !== "undefined" && caches.default ? caches.default : null;
|
|
91
|
+
// Construct a clean cache key ignoring session headers
|
|
92
|
+
const cacheKey = new Request(context.url.toString(), { method: "GET" });
|
|
93
|
+
|
|
94
|
+
// 1. Check surgical Worker Edge Cache for pre-encoded media (15ms HIT)
|
|
95
|
+
if (!isDev && cache) {
|
|
96
|
+
try {
|
|
97
|
+
const cachedResponse = await cache.match(cacheKey);
|
|
98
|
+
if (cachedResponse) {
|
|
99
|
+
return cachedResponse;
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// Fall back to fetch if cache read fails
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 2. Cold Fetch: Pass to CMS worker for AVIF transformation
|
|
89
107
|
const cmsWorker = workerEnv?.CMS_WORKER;
|
|
90
108
|
const envApiUrl = import.meta.env?.PUBLIC_API_URL;
|
|
91
109
|
const workerApiUrl = workerEnv?.PUBLIC_API_URL;
|
|
@@ -93,12 +111,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
93
111
|
const cleanApiUrl = baseApiUrl.endsWith("/") ? baseApiUrl.slice(0, -1) : baseApiUrl;
|
|
94
112
|
|
|
95
113
|
const mediaUrl = `${cleanApiUrl}${pathname}${context.url.search}`;
|
|
96
|
-
const mediaReq = new Request(mediaUrl, {
|
|
97
|
-
headers: context.request.headers
|
|
98
|
-
});
|
|
114
|
+
const mediaReq = new Request(mediaUrl, { headers: context.request.headers });
|
|
99
115
|
|
|
100
116
|
let mediaResponse: Response;
|
|
101
|
-
|
|
102
117
|
if (cmsWorker && typeof cmsWorker.fetch === "function") {
|
|
103
118
|
mediaResponse = await cmsWorker.fetch(mediaReq);
|
|
104
119
|
} else {
|
|
@@ -107,17 +122,27 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
107
122
|
|
|
108
123
|
const newHeaders = new Headers(mediaResponse.headers);
|
|
109
124
|
|
|
110
|
-
// 🎯 DEV FIX: Only apply 1-year cache on media in Production
|
|
111
125
|
if (!isDev) {
|
|
112
126
|
newHeaders.set("Cache-Control", "public, max-age=31536000, s-maxage=31536000, immutable");
|
|
113
127
|
}
|
|
114
128
|
newHeaders.set("Access-Control-Allow-Origin", "*");
|
|
115
129
|
|
|
116
|
-
|
|
130
|
+
const finalResponse = new Response(mediaResponse.body, {
|
|
117
131
|
status: mediaResponse.status,
|
|
118
132
|
statusText: mediaResponse.statusText,
|
|
119
133
|
headers: newHeaders
|
|
120
134
|
});
|
|
135
|
+
|
|
136
|
+
// 3. Store ONLY successful image transformations in Worker Edge Cache
|
|
137
|
+
if (!isDev && cache && mediaResponse.status === 200) {
|
|
138
|
+
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
139
|
+
cfContext.waitUntil(cache.put(cacheKey, finalResponse.clone()));
|
|
140
|
+
} else {
|
|
141
|
+
cache.put(cacheKey, finalResponse.clone()).catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return finalResponse;
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
// 🎯 0C. DIRECT EDGE INTERCEPT FOR CACHE PURGE ENDPOINT
|
|
@@ -147,8 +172,11 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
147
172
|
// A. Purge Workers Caching Layer (Astro cfContext)
|
|
148
173
|
if (typeof cfContext?.cache?.purge === 'function') {
|
|
149
174
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
// 🎯 FIX: Only execute global purge if the CMS explicitly requested it!
|
|
176
|
+
if (body.purge_everything) {
|
|
177
|
+
await cfContext.cache.purge({ purgeEverything: true });
|
|
178
|
+
console.log("✅ New Cloudflare Workers Cache Engine Purged (Global)");
|
|
179
|
+
}
|
|
152
180
|
} catch (e) {
|
|
153
181
|
console.error("❌ Failed to purge new Workers Cache Engine", e);
|
|
154
182
|
}
|
|
@@ -161,22 +189,27 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
161
189
|
|
|
162
190
|
if (body.purge_everything) {
|
|
163
191
|
// 🎯 GLOBAL PURGE: Bumping site_version instantly invalidates ALL KV pages globally!
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
try {
|
|
193
|
+
await HTML_CACHE.put("site_version", now);
|
|
194
|
+
await HTML_CACHE.put("v:/", now);
|
|
195
|
+
console.log(`✅ [Version Tagging] Global site_version updated to ${now}`);
|
|
196
|
+
} catch (kvLimitErr: any) {
|
|
197
|
+
console.warn(`[KV Warning] Skipped global version bump: ${kvLimitErr.message}`);
|
|
198
|
+
}
|
|
169
199
|
} else if (Array.isArray(body.urls) && body.urls.length > 0) {
|
|
170
|
-
// 🎯 SURGICAL PURGE: Parallel execution for page version bumps
|
|
171
|
-
const purgeTasks =
|
|
172
|
-
for (const targetUrl of body.urls) {
|
|
200
|
+
// 🎯 SURGICAL PURGE: Parallel execution for page version bumps safely wrapped for quota
|
|
201
|
+
const purgeTasks = body.urls.map(async (targetUrl: string) => {
|
|
173
202
|
const urlPath = new URL(targetUrl).pathname;
|
|
174
203
|
const cleanPath = urlPath.replace(/\/$/, "") || "/";
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
try {
|
|
205
|
+
await HTML_CACHE.put(`v:${cleanPath}`, now);
|
|
206
|
+
} catch (kvLimitErr: any) {
|
|
207
|
+
console.warn(`[KV Warning] Skipped version tag for ${cleanPath}: ${kvLimitErr.message}`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
178
210
|
await Promise.all(purgeTasks);
|
|
179
211
|
console.log(`✅ [Version Tagging] Updated versions for ${body.urls.length} URLs`);
|
|
212
|
+
// CMS CacheManager will automatically send the X-Cache-Warmer requests now!
|
|
180
213
|
}
|
|
181
214
|
} catch (kvErr) {
|
|
182
215
|
console.error("❌ KV Version Tagging Error:", kvErr);
|
|
@@ -246,18 +279,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
246
279
|
let purgedKv = false;
|
|
247
280
|
|
|
248
281
|
if (HTML_CACHE) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
282
|
+
try {
|
|
283
|
+
const [siteVer, pageVer] = await Promise.all([
|
|
284
|
+
HTML_CACHE.get("site_version"),
|
|
285
|
+
HTML_CACHE.get(`v:${cleanPath}`)
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
if (siteVer || pageVer) {
|
|
289
|
+
const activeCacheKey = `${cleanPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
290
|
+
await HTML_CACHE.delete(activeCacheKey);
|
|
291
|
+
purgedKv = true;
|
|
292
|
+
}
|
|
293
|
+
await HTML_CACHE.delete(`v:${cleanPath}`);
|
|
294
|
+
} catch (kvLimitErr: any) {
|
|
295
|
+
console.warn(`[KV Warning] Skipped deletion for ${cleanPath}: ${kvLimitErr.message}`);
|
|
258
296
|
}
|
|
259
|
-
|
|
260
|
-
await HTML_CACHE.delete(`v:${cleanPath}`);
|
|
261
297
|
}
|
|
262
298
|
|
|
263
299
|
const cleanUrl = targetUrl.split('?')[0];
|
|
@@ -318,17 +354,33 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
318
354
|
HTML_CACHE.get(`v:${cleanLookupPath}`)
|
|
319
355
|
]);
|
|
320
356
|
|
|
357
|
+
// 🎯 Combine versions deterministically
|
|
321
358
|
activeCacheKey = `${cleanLookupPath}:v:${siteVer || "1"}:${pageVer || "1"}`;
|
|
322
359
|
|
|
323
|
-
const
|
|
324
|
-
if (
|
|
325
|
-
|
|
360
|
+
const cachedData = await HTML_CACHE.get(activeCacheKey);
|
|
361
|
+
if (cachedData) {
|
|
362
|
+
let finalHtml = cachedData;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (cachedData.startsWith("{")) {
|
|
366
|
+
const parsed = JSON.parse(cachedData);
|
|
367
|
+
if (parsed.html) {
|
|
368
|
+
finalHtml = parsed.html;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch (jsonErr) {
|
|
372
|
+
// Fallback to raw HTML
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return new Response(finalHtml, {
|
|
326
376
|
status: 200,
|
|
327
377
|
headers: {
|
|
328
378
|
"Content-Type": "text/html; charset=utf-8",
|
|
329
379
|
"X-Cache-Status": "HIT-KV-STATICFILE",
|
|
330
380
|
"X-Cache-Key": activeCacheKey,
|
|
331
|
-
|
|
381
|
+
// 🎯 ARCHITECTURE FIX: Prevent Cloudflare Edge from locking stale HTML in RAM.
|
|
382
|
+
// Force Edge to revalidate with Worker logic while keeping browser cache fresh.
|
|
383
|
+
"Cache-Control": "public, max-age=0, must-revalidate"
|
|
332
384
|
}
|
|
333
385
|
});
|
|
334
386
|
}
|
|
@@ -343,26 +395,47 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
343
395
|
// 🎯 2. KV STATIC "FILE" CACHE WRITE (Save HTML under active Version Key)
|
|
344
396
|
if (
|
|
345
397
|
!isDev &&
|
|
346
|
-
isHtmlPage &&
|
|
347
|
-
HTML_CACHE &&
|
|
398
|
+
isHtmlPage && HTML_CACHE &&
|
|
348
399
|
!isTimeTraveling &&
|
|
349
400
|
response.status === 200 &&
|
|
350
401
|
response.headers.get("content-type")?.includes("text/html")
|
|
351
402
|
) {
|
|
352
403
|
const htmlText = await response.clone().text();
|
|
353
404
|
|
|
405
|
+
// Extract the TTL injected by Astro SSR
|
|
406
|
+
const kvTtlHeader = response.headers.get("X-KV-Expiration");
|
|
407
|
+
const kvTtl = kvTtlHeader ? parseInt(kvTtlHeader, 10) : undefined;
|
|
408
|
+
|
|
409
|
+
// Clean up the internal header so it doesn't leak to the public browser
|
|
410
|
+
response.headers.delete("X-KV-Expiration");
|
|
411
|
+
|
|
354
412
|
const writeTask = (async () => {
|
|
355
413
|
if (activeCacheKey === cleanLookupPath || isWarmer) {
|
|
356
414
|
const forcedVersion = Date.now().toString();
|
|
415
|
+
try {
|
|
416
|
+
await HTML_CACHE.put(`v:${cleanLookupPath}`, forcedVersion);
|
|
417
|
+
const siteVer = await HTML_CACHE.get("site_version") || "1";
|
|
418
|
+
activeCacheKey = `${cleanLookupPath}:v:${siteVer}:${forcedVersion}`;
|
|
419
|
+
} catch (kvLimitErr: any) {
|
|
420
|
+
console.warn(`[KV Warning] Skipped prewarm version bump: ${kvLimitErr.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
357
423
|
|
|
358
|
-
|
|
424
|
+
// 🎯 Architecture Fix: Save TTL alongside HTML so CDN can properly cache reads
|
|
425
|
+
const storedTtl = kvTtl !== undefined ? Math.max(60, kvTtl) : 31536000;
|
|
426
|
+
const payload = JSON.stringify({ html: htmlText, ttl: storedTtl });
|
|
359
427
|
|
|
360
|
-
|
|
361
|
-
|
|
428
|
+
try {
|
|
429
|
+
if (kvTtl !== undefined) {
|
|
430
|
+
await HTML_CACHE.put(activeCacheKey, payload, { expirationTtl: storedTtl });
|
|
431
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Expires in ${storedTtl}s)`);
|
|
432
|
+
} else {
|
|
433
|
+
await HTML_CACHE.put(activeCacheKey, payload);
|
|
434
|
+
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey} (Infinite TTL)`);
|
|
435
|
+
}
|
|
436
|
+
} catch (kvLimitErr: any) {
|
|
437
|
+
console.warn(`[KV Warning] Skipped HTML cache save (Quota limit exceeded): ${kvLimitErr.message}`);
|
|
362
438
|
}
|
|
363
|
-
|
|
364
|
-
await HTML_CACHE.put(activeCacheKey, htmlText);
|
|
365
|
-
console.log(`✅ [Middleware] Saved HTML to KV under key: ${activeCacheKey}`);
|
|
366
439
|
})();
|
|
367
440
|
|
|
368
441
|
if (cfContext && typeof cfContext.waitUntil === 'function') {
|
|
@@ -26,7 +26,7 @@ Astro.response.headers.set(
|
|
|
26
26
|
"public, no-cache, must-revalidate",
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
-
// 1. STATIC ASSET
|
|
29
|
+
// 1. STATIC ASSET CHECK
|
|
30
30
|
const staticExts = [
|
|
31
31
|
".ico",
|
|
32
32
|
".png",
|
|
@@ -36,33 +36,13 @@ const staticExts = [
|
|
|
36
36
|
".txt",
|
|
37
37
|
".xml",
|
|
38
38
|
".webmanifest",
|
|
39
|
-
".astro",
|
|
40
|
-
".ts",
|
|
41
|
-
".tsx",
|
|
42
|
-
".js",
|
|
43
|
-
".jsx",
|
|
44
|
-
".css",
|
|
45
|
-
".map",
|
|
46
|
-
".json",
|
|
47
39
|
];
|
|
48
|
-
|
|
49
40
|
const slugStr = String(slug || "");
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Block Vite client internal hits, source file paths, and node_modules
|
|
57
|
-
const isSystemPath =
|
|
58
|
-
cleanSlug.startsWith("src/") ||
|
|
59
|
-
cleanSlug.startsWith("@vite/") ||
|
|
60
|
-
cleanSlug.startsWith("node_modules/") ||
|
|
61
|
-
cleanSlug.startsWith("users/") ||
|
|
62
|
-
cleanSlug.includes("components/") ||
|
|
63
|
-
cleanSlug.includes("npm-test/");
|
|
64
|
-
|
|
65
|
-
if (isStaticAsset || isSystemPath) {
|
|
41
|
+
const isStaticAsset =
|
|
42
|
+
!slugStr.includes("/") &&
|
|
43
|
+
staticExts.some(ext => slugStr.toLowerCase().endsWith(ext));
|
|
44
|
+
|
|
45
|
+
if (isStaticAsset) {
|
|
66
46
|
return new Response(null, { status: 404 });
|
|
67
47
|
}
|
|
68
48
|
|
|
@@ -70,6 +50,11 @@ if (isStaticAsset || isSystemPath) {
|
|
|
70
50
|
const safeEnv = (Astro.locals as any).safeEnv || {};
|
|
71
51
|
const cmsWorker = safeEnv.CMS_WORKER;
|
|
72
52
|
|
|
53
|
+
// Extract environment configuration & fallback slug
|
|
54
|
+
const envHomeSlug = import.meta.env.PUBLIC_HOME_SLUG;
|
|
55
|
+
const workerHomeSlug = safeEnv.PUBLIC_HOME_SLUG;
|
|
56
|
+
const homeSlug = String(envHomeSlug || workerHomeSlug || "index").trim();
|
|
57
|
+
|
|
73
58
|
const envApiUrl = import.meta.env.PUBLIC_API_URL;
|
|
74
59
|
const workerApiUrl = safeEnv.PUBLIC_API_URL;
|
|
75
60
|
const baseApiUrl = String(envApiUrl || workerApiUrl || "http://localhost:8787");
|
|
@@ -121,7 +106,8 @@ const treeId = getTreeId();
|
|
|
121
106
|
// 3. PATH & CONCURRENT FETCH RESOLUTION
|
|
122
107
|
const rawSlug = Array.isArray(slug) ? slug.join("/") : slugStr;
|
|
123
108
|
const pathParts = rawSlug.split("/").filter(part => part.trim() !== "");
|
|
124
|
-
const
|
|
109
|
+
const requestedSlug = pathParts.join("/");
|
|
110
|
+
const targetSlug = requestedSlug || homeSlug;
|
|
125
111
|
|
|
126
112
|
let pageData: NodeResponse | null = null;
|
|
127
113
|
let navTree: TreeNode[] = [];
|
|
@@ -133,7 +119,6 @@ try {
|
|
|
133
119
|
console.error(`❌ [SSR Error] Failed to fetch tree "${treeId}":`, err);
|
|
134
120
|
return [];
|
|
135
121
|
}),
|
|
136
|
-
// 🎯 We just pass the targetSlug (even if it's empty). The backend handles it!
|
|
137
122
|
getNodeByPath(targetSlug, channelId, isPreview, customFetch).catch(err => {
|
|
138
123
|
console.error(
|
|
139
124
|
`❌ [SSR Error] Failed to fetch page data for path "${targetSlug}":`,
|
|
@@ -144,7 +129,7 @@ try {
|
|
|
144
129
|
]);
|
|
145
130
|
|
|
146
131
|
navTree = treeResult || [];
|
|
147
|
-
pageData = nodeResult;
|
|
132
|
+
pageData = nodeResult as NodeResponse | null;
|
|
148
133
|
} catch (err) {
|
|
149
134
|
console.error("❌ [SSR Error] Failed concurrent fetch execution:", err);
|
|
150
135
|
}
|
|
@@ -156,8 +141,43 @@ if (!pageData || !pageData.node) {
|
|
|
156
141
|
const node = pageData.node;
|
|
157
142
|
const content = pageData.content || {};
|
|
158
143
|
|
|
159
|
-
// 🎯
|
|
160
|
-
|
|
144
|
+
// 🎯 SMART EDGE CACHING LOGIC (API-Driven TTL)
|
|
145
|
+
// Rely on the backend API's meta.cacheTtl which calculates the closest publishing event for the node AND its blocks.
|
|
146
|
+
let cacheTtl = pageData?.meta?.cacheTtl;
|
|
147
|
+
|
|
148
|
+
if (cacheTtl === undefined || cacheTtl === null) {
|
|
149
|
+
cacheTtl = 31536000; // Default: 1 Year (Forever)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clamp to Cloudflare's 60-second minimum for KV expirations
|
|
153
|
+
cacheTtl = Math.max(60, cacheTtl);
|
|
154
|
+
|
|
155
|
+
if (isTimeTraveling) {
|
|
156
|
+
Astro.response.headers.set(
|
|
157
|
+
"Cache-Control",
|
|
158
|
+
"no-cache, no-store, must-revalidate",
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
// 🎯 FIX: max-age=0 prevents local browser caching. s-maxage handles Cloudflare CDN!
|
|
162
|
+
Astro.response.headers.set(
|
|
163
|
+
"Cache-Control",
|
|
164
|
+
`public, max-age=0, s-maxage=${cacheTtl}, must-revalidate`,
|
|
165
|
+
);
|
|
166
|
+
// Tell middleware how long to store in KV
|
|
167
|
+
Astro.response.headers.set("X-KV-Expiration", cacheTtl.toString());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rootTreeSlug =
|
|
171
|
+
navTree && navTree.length > 0
|
|
172
|
+
? navTree[0].slug || (navTree[0] as any).key
|
|
173
|
+
: "";
|
|
174
|
+
|
|
175
|
+
const isHomePage =
|
|
176
|
+
targetSlug === homeSlug ||
|
|
177
|
+
(rootTreeSlug && targetSlug === rootTreeSlug) ||
|
|
178
|
+
node?.path === "/" ||
|
|
179
|
+
pathParts.length === 0;
|
|
180
|
+
|
|
161
181
|
const normalizedSlug = isHomePage ? "" : targetSlug;
|
|
162
182
|
|
|
163
183
|
const headerBlocks = content?.header || [];
|