@edge-base/server 0.2.7 → 0.2.8
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/admin-build/_app/immutable/chunks/{DnpbvAPi.js → B9efkx2V.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BhCO1Fpt.js → BMXWUTG-.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CIOC1v_q.js → Bt4AyT3o.js} +3 -3
- package/admin-build/_app/immutable/chunks/CKVjMXZi.js +1 -0
- package/admin-build/_app/immutable/chunks/{BfpUQYr3.js → CMYgGhZR.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DaXO-sFP.js → CTRjWhGs.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BDYewzou.js → CwyE59Yt.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D1u3u7xu.js → D8aeTKry.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BYyykAbh.js → DGAHkap7.js} +1 -1
- package/admin-build/_app/immutable/chunks/{ejoEf2I5.js → DPgR4-0v.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dz9cUCuv.js → DYtrHeVQ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CvczjTXx.js → DcVb45Ds.js} +1 -1
- package/admin-build/_app/immutable/chunks/Djnkhy-S.js +1 -0
- package/admin-build/_app/immutable/chunks/{Tea2dBJ8.js → fPy6xmgG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{iEyeblJR.js → j4jxnAKj.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BEM1BeVF.js → zl2AUKMP.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.DoUaxnew.js → app.Cmz0WjMl.js} +2 -2
- package/admin-build/_app/immutable/entry/start.JE7dcbK1.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.Dsxi8s7i.js → 0.y6D_QyUb.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.Cp2l-hol.js → 1.CndRxhbH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.4oY6m8Nz.js → 10.CdA5FmXy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.DfcozD4J.js → 11.DG8SzMp_.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.uJgZdCIA.js → 12.CvmQqpFa.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.CaN1kRev.js → 13.BbGNdswT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.DQ5xIi3s.js → 14.CZKsN7-O.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.B_EkebTJ.js → 15.A7-CYgkG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.Tko1ZX8-.js → 16.hgJT9H-x.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.BCmWMJX9.js → 17.DkWZbcN2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.hmGhl1O2.js → 18.sX3Fb5gh.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.D-1infOo.js → 19.VAZUW-1K.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.CY4KKcBL.js → 20.DkIKxacG.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.DOjJlQKc.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.14Vd7bnt.js → 22.BDaHvtaw.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.Be6jK77o.js → 23.BVRzw_pD.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CSTFkr6R.js → 24.CVhSJyG0.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.DRTg8fHc.js → 25.Bme-9bZn.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.DKt-9lwQ.js → 26.Dsx7RIIs.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.D5caPu0F.js → 27.DMGQnzFM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.hJhlnlyY.js → 28.GGwFmEhZ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.CDYBzFyT.js → 29.Dnghr0nk.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.DMyKwkGn.js → 3.Cg7zZJP1.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.BaHNeEmc.js → 30.C0J24z3I.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.C6PV5L-2.js → 31.MdxFI8v6.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.9E118Ftm.js → 4.DCAOVzGE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.D8guAl3v.js → 5.DzUQ-cTc.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.D1u__DtT.js → 6.CptBYTVj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.DWXHnRFf.js → 7.DfeeQ0Rg.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.Dojd8krc.js → 8.CIcvctW7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.CLtrr0K_.js → 9.QKrvq4RA.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-assets.test.ts +7 -7
- package/src/__tests__/frontend-assets.test.ts +75 -0
- package/src/__tests__/frontend-config.test.ts +16 -0
- package/src/__tests__/frontend-routing.test.ts +200 -0
- package/src/durable-objects/room-runtime-base.ts +2 -0
- package/src/index.ts +97 -3
- package/src/lib/admin-assets.ts +5 -5
- package/src/lib/frontend-assets.ts +129 -0
- package/src/lib/frontend-config.ts +11 -0
- package/admin-build/_app/immutable/chunks/BaUG2TJ-.js +0 -1
- package/admin-build/_app/immutable/chunks/qKdzaeX3.js +0 -1
- package/admin-build/_app/immutable/entry/start.MmZh8oBH.js +0 -1
- package/admin-build/_app/immutable/nodes/21.B9lbNUQr.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/
|
|
1
|
+
import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/DcVb45Ds.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{d as Y,s as Z}from"../chunks/CjcrXziO.js";import{P as tt}from"../chunks/BkZCgsc3.js";import{a as et}from"../chunks/Q2nPFxS6.js";import{M as _,T as at}from"../chunks/CTRjWhGs.js";import{D as rt,T as nt}from"../chunks/CMYgGhZR.js";var st=I("<button> </button>"),ot=I('<div class="range-selector"></div>'),lt=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function yt(w,A){j(A,!0);let s=D(!0),o=D(null),p=D("24h");const M=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],T=n(()=>()=>{switch(t(p)){case"1h":return"minute";case"6h":case"24h":return"hour";default:return"day"}});async function $(){try{const e=await Q.fetch(`data/analytics?range=${t(p)}&category=function&metric=overview&groupBy=${t(T)()}`);y(o,e,!0)}catch(e){V(Y(e,"Failed to load functions analytics."))}finally{y(s,!1)}}function k(e){y(p,e,!0),y(s,!0),$()}U(()=>{$();const e=setInterval($,3e4);return()=>clearInterval(e)});const H=n(()=>()=>{var c;if(!((c=t(o))!=null&&c.summary))return 0;const{totalRequests:e,totalErrors:a}=t(o).summary;return e>0?a/e*100:0}),B=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.timeSeries)||[]).map(a=>({timestamp:a.timestamp,value:a.requests??a.value??0}))}),C=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.breakdown)||[]).map(a=>({label:a.label||"other",value:a.count}))}),E=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.topItems)||[]).map(a=>({label:a.label,count:a.count,secondary:`${Math.round(a.avgLatency)}ms · ${a.errorRate.toFixed(1)}% 5xx`}))});tt(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return et},actions:a=>{var c=ot();W(c,21,()=>M,X,(f,d)=>{var l=st();let v;var h=b(l,!0);g(l),J(()=>{v=Z(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>k(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=lt(),d=G(f),l=b(d);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.totalRequests)??0});_(l,{label:"Invocations",get value(){return t(r)},get loading(){return t(s)}})}var v=m(l,2);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.uniqueUsers)??0});_(v,{label:"Unique Users",get value(){return t(r)},get loading(){return t(s)}})}var h=m(v,2);{let r=n(()=>t(H)().toFixed(1));_(h,{label:"5xx Rate",get value(){return t(r)},unit:"%",get loading(){return t(s)}})}var L=m(h,2);{let r=n(()=>{var i,u;return Math.round(((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.avgLatency)??0)});_(L,{label:"Avg Duration",get value(){return t(r)},unit:"ms",get loading(){return t(s)}})}g(d);var x=m(d,2),P=b(x);{let r=n(()=>t(B)());at(P,{get data(){return t(r)},type:"bar",height:240,label:"Function invocations over time",get loading(){return t(s)},color:"#dc2626"})}g(x);var R=m(x,2),q=b(R);{let r=n(()=>t(C)());rt(q,{get items(){return t(r)},title:"Functions by invocation count",get loading(){return t(s)}})}var S=m(q,2);{let r=n(()=>t(E)());nt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{yt as component};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"1775284872870"}
|
package/admin-build/index.html
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>EdgeBase Admin</title>
|
|
7
7
|
<link rel="icon" href="/admin/favicon.svg" type="image/svg+xml" />
|
|
8
|
-
<link href="/admin/_app/immutable/entry/start.
|
|
9
|
-
<link href="/admin/_app/immutable/chunks/
|
|
8
|
+
<link href="/admin/_app/immutable/entry/start.JE7dcbK1.js" rel="modulepreload">
|
|
9
|
+
<link href="/admin/_app/immutable/chunks/CKVjMXZi.js" rel="modulepreload">
|
|
10
10
|
<link href="/admin/_app/immutable/chunks/BdTBlfLy.js" rel="modulepreload">
|
|
11
11
|
<link href="/admin/_app/immutable/chunks/Bn2NtlTj.js" rel="modulepreload">
|
|
12
|
-
<link href="/admin/_app/immutable/chunks/
|
|
13
|
-
<link href="/admin/_app/immutable/entry/app.
|
|
12
|
+
<link href="/admin/_app/immutable/chunks/Djnkhy-S.js" rel="modulepreload">
|
|
13
|
+
<link href="/admin/_app/immutable/entry/app.Cmz0WjMl.js" rel="modulepreload">
|
|
14
14
|
<link href="/admin/_app/immutable/chunks/B2bEC_Hm.js" rel="modulepreload">
|
|
15
15
|
<link href="/admin/_app/immutable/chunks/Bb0e0sAP.js" rel="modulepreload">
|
|
16
16
|
<link href="/admin/_app/immutable/chunks/DtZk82gG.js" rel="modulepreload">
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<div style="display: contents">
|
|
27
27
|
<script>
|
|
28
28
|
{
|
|
29
|
-
|
|
29
|
+
__sveltekit_1wrq4al = {
|
|
30
30
|
base: "/admin",
|
|
31
31
|
assets: "/admin"
|
|
32
32
|
};
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
const element = document.currentScript.parentElement;
|
|
35
35
|
|
|
36
36
|
Promise.all([
|
|
37
|
-
import("/admin/_app/immutable/entry/start.
|
|
38
|
-
import("/admin/_app/immutable/entry/app.
|
|
37
|
+
import("/admin/_app/immutable/entry/start.JE7dcbK1.js"),
|
|
38
|
+
import("/admin/_app/immutable/entry/app.Cmz0WjMl.js")
|
|
39
39
|
]).then(([kit, app]) => {
|
|
40
40
|
kit.start(app, element);
|
|
41
41
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edge-base/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "EdgeBase runtime assets consumed by the EdgeBase CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"jose": "^6.0.0",
|
|
35
35
|
"pg": "^8.16.3",
|
|
36
36
|
"zod": "^4.3.6",
|
|
37
|
-
"@edge-base/core": "0.2.
|
|
38
|
-
"@edge-base/shared": "0.2.
|
|
37
|
+
"@edge-base/core": "0.2.8",
|
|
38
|
+
"@edge-base/shared": "0.2.8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@cloudflare/vitest-pool-workers": "^0.8.71",
|
|
@@ -8,18 +8,18 @@ import {
|
|
|
8
8
|
|
|
9
9
|
describe('admin asset path resolution', () => {
|
|
10
10
|
it('serves index for the admin root', () => {
|
|
11
|
-
expect(resolveAdminAssetPath('/admin')).toBe('/');
|
|
12
|
-
expect(resolveAdminAssetPath('/admin/')).toBe('/');
|
|
11
|
+
expect(resolveAdminAssetPath('/admin')).toBe('/admin/index.html');
|
|
12
|
+
expect(resolveAdminAssetPath('/admin/')).toBe('/admin/index.html');
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
it('strips the /admin prefix for built assets', () => {
|
|
16
|
-
expect(resolveAdminAssetPath('/admin/_app/version.json')).toBe('/_app/version.json');
|
|
17
|
-
expect(resolveAdminAssetPath('/admin/favicon.png')).toBe('/favicon.png');
|
|
16
|
+
expect(resolveAdminAssetPath('/admin/_app/version.json')).toBe('/admin/_app/version.json');
|
|
17
|
+
expect(resolveAdminAssetPath('/admin/favicon.png')).toBe('/admin/favicon.png');
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it('falls back to index for client-side admin routes', () => {
|
|
21
|
-
expect(resolveAdminAssetPath('/admin/login')).toBe('/');
|
|
22
|
-
expect(resolveAdminAssetPath('/admin/database/tables')).toBe('/');
|
|
21
|
+
expect(resolveAdminAssetPath('/admin/login')).toBe('/admin/index.html');
|
|
22
|
+
expect(resolveAdminAssetPath('/admin/database/tables')).toBe('/admin/index.html');
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('rewrites requests without dropping the query string', () => {
|
|
@@ -27,7 +27,7 @@ describe('admin asset path resolution', () => {
|
|
|
27
27
|
const rewritten = createAdminAssetRequest(request);
|
|
28
28
|
const url = new URL(rewritten.url);
|
|
29
29
|
|
|
30
|
-
expect(url.pathname).toBe('/');
|
|
30
|
+
expect(url.pathname).toBe('/admin/index.html');
|
|
31
31
|
expect(url.search).toBe('?next=%2Fadmin%2Fdatabase');
|
|
32
32
|
});
|
|
33
33
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
applyFrontendAssetHeaders,
|
|
4
|
+
createFrontendAssetRequest,
|
|
5
|
+
resolveFrontendAssetPath,
|
|
6
|
+
} from '../lib/frontend-assets.js';
|
|
7
|
+
|
|
8
|
+
describe('frontend asset path resolution', () => {
|
|
9
|
+
it('serves index.html for the root mount path', () => {
|
|
10
|
+
expect(resolveFrontendAssetPath('/', { mountPath: '/' })).toBe('/index.html');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('serves index.html for custom mount roots', () => {
|
|
14
|
+
expect(resolveFrontendAssetPath('/app', { mountPath: '/app' })).toBe('/app/index.html');
|
|
15
|
+
expect(resolveFrontendAssetPath('/app/', { mountPath: '/app' })).toBe('/app/index.html');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('keeps explicit asset paths intact', () => {
|
|
19
|
+
expect(resolveFrontendAssetPath('/assets/main.js', { mountPath: '/' })).toBe('/assets/main.js');
|
|
20
|
+
expect(resolveFrontendAssetPath('/app/assets/main.js', { mountPath: '/app' })).toBe('/app/assets/main.js');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('falls back to index.html only for HTML navigation when enabled', () => {
|
|
24
|
+
expect(resolveFrontendAssetPath('/dashboard/settings', {
|
|
25
|
+
mountPath: '/',
|
|
26
|
+
spaFallback: true,
|
|
27
|
+
method: 'GET',
|
|
28
|
+
accept: 'text/html,application/xhtml+xml',
|
|
29
|
+
})).toBe('/index.html');
|
|
30
|
+
|
|
31
|
+
expect(resolveFrontendAssetPath('/dashboard/settings', {
|
|
32
|
+
mountPath: '/',
|
|
33
|
+
spaFallback: true,
|
|
34
|
+
method: 'GET',
|
|
35
|
+
accept: 'application/json',
|
|
36
|
+
})).toBe('/dashboard/settings');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns null outside the configured mount path', () => {
|
|
40
|
+
expect(resolveFrontendAssetPath('/dashboard', { mountPath: '/app' })).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('rewrites requests without dropping the query string', () => {
|
|
44
|
+
const request = new Request('http://localhost:8787/app/dashboard?tab=settings', {
|
|
45
|
+
headers: { accept: 'text/html' },
|
|
46
|
+
});
|
|
47
|
+
const rewritten = createFrontendAssetRequest(request, {
|
|
48
|
+
directory: './web/dist',
|
|
49
|
+
mountPath: '/app',
|
|
50
|
+
spaFallback: true,
|
|
51
|
+
});
|
|
52
|
+
const url = new URL(rewritten!.url);
|
|
53
|
+
|
|
54
|
+
expect(url.pathname).toBe('/app/index.html');
|
|
55
|
+
expect(url.search).toBe('?tab=settings');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('frontend cache headers', () => {
|
|
60
|
+
it('marks html and PWA entry files as no-cache', () => {
|
|
61
|
+
const response = applyFrontendAssetHeaders(new Response('ok'), '/index.html');
|
|
62
|
+
expect(response.headers.get('Cache-Control')).toBe('no-cache');
|
|
63
|
+
|
|
64
|
+
const manifest = applyFrontendAssetHeaders(new Response('ok'), '/manifest.webmanifest');
|
|
65
|
+
expect(manifest.headers.get('Cache-Control')).toBe('no-cache');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('marks hashed assets as immutable and unhashed assets as short-lived', () => {
|
|
69
|
+
const hashed = applyFrontendAssetHeaders(new Response('ok'), '/assets/app-abc123def456.js');
|
|
70
|
+
expect(hashed.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable');
|
|
71
|
+
|
|
72
|
+
const plain = applyFrontendAssetHeaders(new Response('ok'), '/favicon.ico');
|
|
73
|
+
expect(plain.headers.get('Cache-Control')).toBe('public, max-age=300');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeFrontendMountPath } from '../lib/frontend-config.js';
|
|
3
|
+
|
|
4
|
+
describe('frontend mount path normalization', () => {
|
|
5
|
+
it('defaults to the root mount path when unset', () => {
|
|
6
|
+
expect(normalizeFrontendMountPath(undefined)).toBe('/');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('preserves the root mount path', () => {
|
|
10
|
+
expect(normalizeFrontendMountPath('/')).toBe('/');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('trims a trailing slash from custom mount paths', () => {
|
|
14
|
+
expect(normalizeFrontendMountPath('/app/')).toBe('/app');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('cloudflare:workers', () => ({
|
|
4
|
+
DurableObject: class DurableObject {},
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.resetModules();
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function createExecutionContext(): ExecutionContext {
|
|
13
|
+
return {
|
|
14
|
+
waitUntil(promise: Promise<unknown>) {
|
|
15
|
+
void promise.catch(() => {});
|
|
16
|
+
},
|
|
17
|
+
passThroughOnException() {},
|
|
18
|
+
} as ExecutionContext;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createEnv(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
22
|
+
const logsBinding = {
|
|
23
|
+
idFromName: vi.fn((name: string) => ({ toString: () => name })),
|
|
24
|
+
get: vi.fn(() => ({
|
|
25
|
+
fetch: vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })),
|
|
26
|
+
})),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
DATABASE: {} as never,
|
|
31
|
+
AUTH: {} as never,
|
|
32
|
+
DATABASE_LIVE: {} as never,
|
|
33
|
+
ROOMS: {} as never,
|
|
34
|
+
LOGS: logsBinding as never,
|
|
35
|
+
STORAGE: {} as never,
|
|
36
|
+
KV: {} as never,
|
|
37
|
+
AUTH_DB: {} as never,
|
|
38
|
+
CONTROL_DB: {} as never,
|
|
39
|
+
EDGEBASE_CONFIG: {},
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadWorker() {
|
|
45
|
+
vi.doMock('../lib/runtime-startup.js', () => ({
|
|
46
|
+
ensureServerStartup: vi.fn().mockResolvedValue(undefined),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const mod = await import('../index.js');
|
|
50
|
+
return mod.default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('frontend routing', () => {
|
|
54
|
+
it('serves the frontend root when a root-mounted bundle is configured', async () => {
|
|
55
|
+
const assetsFetch = vi.fn(async (request: Request) => {
|
|
56
|
+
const pathname = new URL(request.url).pathname;
|
|
57
|
+
return new Response(`asset:${pathname}`, { status: 200 });
|
|
58
|
+
});
|
|
59
|
+
const worker = await loadWorker();
|
|
60
|
+
|
|
61
|
+
const response = await worker.fetch(
|
|
62
|
+
new Request('http://localhost:8787/'),
|
|
63
|
+
createEnv({
|
|
64
|
+
EDGEBASE_CONFIG: {
|
|
65
|
+
frontend: {
|
|
66
|
+
directory: './web/dist',
|
|
67
|
+
spaFallback: true,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
ASSETS: { fetch: assetsFetch },
|
|
71
|
+
}) as never,
|
|
72
|
+
createExecutionContext(),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(response.status).toBe(200);
|
|
76
|
+
expect(await response.text()).toBe('asset:/index.html');
|
|
77
|
+
expect(response.headers.get('Cache-Control')).toBe('no-cache');
|
|
78
|
+
expect(assetsFetch).toHaveBeenCalledTimes(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('follows same-origin asset redirects for canonical frontend index routes', async () => {
|
|
82
|
+
const assetPaths: string[] = [];
|
|
83
|
+
const assetsFetch = vi.fn(async (request: Request) => {
|
|
84
|
+
const pathname = new URL(request.url).pathname;
|
|
85
|
+
assetPaths.push(pathname);
|
|
86
|
+
|
|
87
|
+
if (pathname === '/index.html') {
|
|
88
|
+
return new Response(null, {
|
|
89
|
+
status: 307,
|
|
90
|
+
headers: { location: '/' },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Response('asset:/', { status: 200 });
|
|
95
|
+
});
|
|
96
|
+
const worker = await loadWorker();
|
|
97
|
+
|
|
98
|
+
const response = await worker.fetch(
|
|
99
|
+
new Request('http://localhost:8787/'),
|
|
100
|
+
createEnv({
|
|
101
|
+
EDGEBASE_CONFIG: {
|
|
102
|
+
frontend: {
|
|
103
|
+
directory: './web/dist',
|
|
104
|
+
spaFallback: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
ASSETS: { fetch: assetsFetch },
|
|
108
|
+
}) as never,
|
|
109
|
+
createExecutionContext(),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(response.status).toBe(200);
|
|
113
|
+
expect(await response.text()).toBe('asset:/');
|
|
114
|
+
expect(response.headers.get('Cache-Control')).toBe('no-cache');
|
|
115
|
+
expect(assetPaths).toEqual(['/index.html', '/']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('applies SPA fallback only to HTML navigation routes', async () => {
|
|
119
|
+
const assetPaths: string[] = [];
|
|
120
|
+
const assetsFetch = vi.fn(async (request: Request) => {
|
|
121
|
+
const pathname = new URL(request.url).pathname;
|
|
122
|
+
assetPaths.push(pathname);
|
|
123
|
+
return pathname === '/index.html'
|
|
124
|
+
? new Response('frontend-index', { status: 200 })
|
|
125
|
+
: new Response('missing', { status: 404 });
|
|
126
|
+
});
|
|
127
|
+
const worker = await loadWorker();
|
|
128
|
+
|
|
129
|
+
const htmlNavigation = await worker.fetch(
|
|
130
|
+
new Request('http://localhost:8787/dashboard/settings', {
|
|
131
|
+
headers: { accept: 'text/html,application/xhtml+xml' },
|
|
132
|
+
}),
|
|
133
|
+
createEnv({
|
|
134
|
+
EDGEBASE_CONFIG: {
|
|
135
|
+
frontend: {
|
|
136
|
+
directory: './web/dist',
|
|
137
|
+
spaFallback: true,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
ASSETS: { fetch: assetsFetch },
|
|
141
|
+
}) as never,
|
|
142
|
+
createExecutionContext(),
|
|
143
|
+
);
|
|
144
|
+
const missingAsset = await worker.fetch(
|
|
145
|
+
new Request('http://localhost:8787/assets/missing.js', {
|
|
146
|
+
headers: { accept: 'text/html,application/xhtml+xml' },
|
|
147
|
+
}),
|
|
148
|
+
createEnv({
|
|
149
|
+
EDGEBASE_CONFIG: {
|
|
150
|
+
frontend: {
|
|
151
|
+
directory: './web/dist',
|
|
152
|
+
spaFallback: true,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
ASSETS: { fetch: assetsFetch },
|
|
156
|
+
}) as never,
|
|
157
|
+
createExecutionContext(),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(await htmlNavigation.text()).toBe('frontend-index');
|
|
161
|
+
expect(htmlNavigation.status).toBe(200);
|
|
162
|
+
expect(missingAsset.status).toBe(404);
|
|
163
|
+
expect(assetPaths).toEqual(['/index.html', '/assets/missing.js']);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('respects custom mount paths and leaves API routes to the worker', async () => {
|
|
167
|
+
const assetsFetch = vi.fn(async (request: Request) => {
|
|
168
|
+
const pathname = new URL(request.url).pathname;
|
|
169
|
+
return new Response(`asset:${pathname}`, { status: 200 });
|
|
170
|
+
});
|
|
171
|
+
const worker = await loadWorker();
|
|
172
|
+
const env = createEnv({
|
|
173
|
+
EDGEBASE_CONFIG: {
|
|
174
|
+
frontend: {
|
|
175
|
+
directory: './web/dist',
|
|
176
|
+
mountPath: '/app',
|
|
177
|
+
spaFallback: true,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
ASSETS: { fetch: assetsFetch },
|
|
181
|
+
}) as never;
|
|
182
|
+
|
|
183
|
+
const mounted = await worker.fetch(
|
|
184
|
+
new Request('http://localhost:8787/app/dashboard', {
|
|
185
|
+
headers: { accept: 'text/html' },
|
|
186
|
+
}),
|
|
187
|
+
env,
|
|
188
|
+
createExecutionContext(),
|
|
189
|
+
);
|
|
190
|
+
const api = await worker.fetch(
|
|
191
|
+
new Request('http://localhost:8787/api/health'),
|
|
192
|
+
env,
|
|
193
|
+
createExecutionContext(),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(await mounted.text()).toBe('asset:/app/index.html');
|
|
197
|
+
expect(api.status).toBe(200);
|
|
198
|
+
expect(assetsFetch).toHaveBeenCalledTimes(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -1157,6 +1157,8 @@ export class RoomRuntimeBaseDO extends DurableObject<RoomDOEnv> {
|
|
|
1157
1157
|
|
|
1158
1158
|
protected buildRoomServerAPI(): RoomServerAPI {
|
|
1159
1159
|
return {
|
|
1160
|
+
namespace: this.namespace ?? '',
|
|
1161
|
+
roomId: this.roomId ?? '',
|
|
1160
1162
|
getSharedState: (): Record<string, unknown> => {
|
|
1161
1163
|
return cloneState(this.sharedState);
|
|
1162
1164
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HonoEnv } from './lib/hono.js';
|
|
2
2
|
import type { OpenApiSpec } from './lib/openapi.js';
|
|
3
3
|
import type { Env } from './types.js';
|
|
4
|
+
import type { FrontendConfigLike } from './lib/frontend-config.js';
|
|
4
5
|
import { ensureServerStartup } from './lib/runtime-startup.js';
|
|
5
6
|
|
|
6
7
|
// ─── DO Re-exports (wrangler needs exports from main entry) ───
|
|
@@ -11,12 +12,14 @@ export { RoomsDO } from './durable-objects/rooms-do.js';
|
|
|
11
12
|
export { LogsDO } from './durable-objects/logs-do.js';
|
|
12
13
|
|
|
13
14
|
let appPromise: Promise<Awaited<ReturnType<typeof buildApp>>> | null = null;
|
|
15
|
+
const FRONTEND_ASSET_REDIRECT_STATUSES = new Set([301, 302, 307, 308]);
|
|
16
|
+
const FRONTEND_ASSET_REDIRECT_LIMIT = 4;
|
|
14
17
|
|
|
15
18
|
function assetUnavailableMessage(
|
|
16
|
-
assetName: 'admin dashboard' | 'harness assets',
|
|
19
|
+
assetName: 'admin dashboard' | 'frontend bundle' | 'harness assets',
|
|
17
20
|
): string {
|
|
18
21
|
const label = `${assetName[0].toUpperCase()}${assetName.slice(1)}`;
|
|
19
|
-
const verb = assetName === '
|
|
22
|
+
const verb = assetName === 'harness assets' ? 'are' : 'is';
|
|
20
23
|
return `${label} ${verb} not deployed for this worker. Deploy the assets bundle or configure ADMIN_ORIGIN if they are hosted elsewhere.`;
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -56,6 +59,7 @@ async function buildApp() {
|
|
|
56
59
|
analyticsRouteModule,
|
|
57
60
|
adminAssetsModule,
|
|
58
61
|
adminRoutingModule,
|
|
62
|
+
frontendAssetsModule,
|
|
59
63
|
schemasModule,
|
|
60
64
|
pluginMigrationsModule,
|
|
61
65
|
pluginMigrationRoutingModule,
|
|
@@ -95,6 +99,7 @@ async function buildApp() {
|
|
|
95
99
|
import('./routes/analytics-api.js'),
|
|
96
100
|
import('./lib/admin-assets.js'),
|
|
97
101
|
import('./lib/admin-routing.js'),
|
|
102
|
+
import('./lib/frontend-assets.js'),
|
|
98
103
|
import('./lib/schemas.js'),
|
|
99
104
|
import('./lib/plugin-migrations.js'),
|
|
100
105
|
import('./lib/plugin-migration-routing.js'),
|
|
@@ -116,6 +121,7 @@ async function buildApp() {
|
|
|
116
121
|
const { SERVER_VERSION } = versionModule;
|
|
117
122
|
const { createAdminAssetRequest } = adminAssetsModule;
|
|
118
123
|
const { resolveAdminFaviconTarget, resolveAdminRedirectTarget } = adminRoutingModule;
|
|
124
|
+
const { applyFrontendAssetHeaders, createFrontendAssetRequest } = frontendAssetsModule;
|
|
119
125
|
const { zodDefaultHook } = schemasModule;
|
|
120
126
|
const { executePluginMigrations } = pluginMigrationsModule;
|
|
121
127
|
const { shouldRunPluginMigrationsForRequestPath } = pluginMigrationRoutingModule;
|
|
@@ -163,8 +169,75 @@ async function buildApp() {
|
|
|
163
169
|
app.route('/admin/api', adminRouteModule.adminRoute);
|
|
164
170
|
app.route('/admin/api/backup', backupRouteModule.backupRoute);
|
|
165
171
|
|
|
166
|
-
|
|
172
|
+
function getFrontendConfig(env: Env): FrontendConfigLike | undefined {
|
|
173
|
+
return (doRouterModule.parseConfig(env) as { frontend?: FrontendConfigLike } | undefined)?.frontend;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fetchFrontendAssetResponse(
|
|
177
|
+
assetsBinding: { fetch(request: Request): Promise<Response> },
|
|
178
|
+
assetRequest: Request,
|
|
179
|
+
): Promise<Response> {
|
|
180
|
+
let currentRequest = assetRequest;
|
|
181
|
+
const visitedUrls = new Set<string>();
|
|
182
|
+
|
|
183
|
+
for (let attempt = 0; attempt <= FRONTEND_ASSET_REDIRECT_LIMIT; attempt += 1) {
|
|
184
|
+
visitedUrls.add(currentRequest.url);
|
|
185
|
+
const assetResponse = await assetsBinding.fetch(currentRequest);
|
|
186
|
+
if (!FRONTEND_ASSET_REDIRECT_STATUSES.has(assetResponse.status)) {
|
|
187
|
+
return assetResponse;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const location = assetResponse.headers.get('location');
|
|
191
|
+
if (!location) {
|
|
192
|
+
return assetResponse;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const nextUrl = new URL(location, currentRequest.url);
|
|
196
|
+
if (nextUrl.origin !== new URL(currentRequest.url).origin) {
|
|
197
|
+
return assetResponse;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (visitedUrls.has(nextUrl.toString())) {
|
|
201
|
+
return assetResponse;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
currentRequest = new Request(nextUrl.toString(), currentRequest);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return assetsBinding.fetch(currentRequest);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function serveFrontendAsset(c: { env: Env; req: { raw: Request } }): Promise<Response | null> {
|
|
211
|
+
const frontend = getFrontendConfig(c.env);
|
|
212
|
+
if (!frontend) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!c.env.ASSETS) {
|
|
217
|
+
return new Response(
|
|
218
|
+
JSON.stringify({ code: 404, message: assetUnavailableMessage('frontend bundle') }),
|
|
219
|
+
{
|
|
220
|
+
status: 404,
|
|
221
|
+
headers: { 'content-type': 'application/json; charset=UTF-8' },
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const assetRequest = createFrontendAssetRequest(c.req.raw, frontend);
|
|
227
|
+
if (!assetRequest) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const assetResponse = await fetchFrontendAssetResponse(c.env.ASSETS, assetRequest);
|
|
232
|
+
return applyFrontendAssetHeaders(assetResponse, new URL(assetRequest.url).pathname);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
app.get('/', async (c) => {
|
|
167
236
|
const env = c.env as Env;
|
|
237
|
+
const frontendResponse = await serveFrontendAsset({ env, req: c.req });
|
|
238
|
+
if (frontendResponse) {
|
|
239
|
+
return frontendResponse;
|
|
240
|
+
}
|
|
168
241
|
const externalAdminUrl = resolveAdminRedirectTarget(c.req.url, env.ADMIN_ORIGIN);
|
|
169
242
|
if (externalAdminUrl) {
|
|
170
243
|
return c.redirect(externalAdminUrl, 302);
|
|
@@ -181,6 +254,10 @@ async function buildApp() {
|
|
|
181
254
|
|
|
182
255
|
app.get('/favicon.ico', async (c) => {
|
|
183
256
|
const env = c.env as Env;
|
|
257
|
+
const frontendResponse = await serveFrontendAsset({ env, req: c.req });
|
|
258
|
+
if (frontendResponse) {
|
|
259
|
+
return frontendResponse;
|
|
260
|
+
}
|
|
184
261
|
const externalFaviconUrl = resolveAdminFaviconTarget(env.ADMIN_ORIGIN);
|
|
185
262
|
if (externalFaviconUrl) {
|
|
186
263
|
return c.redirect(externalFaviconUrl, 302);
|
|
@@ -197,6 +274,10 @@ async function buildApp() {
|
|
|
197
274
|
|
|
198
275
|
app.get('/favicon.svg', async (c) => {
|
|
199
276
|
const env = c.env as Env;
|
|
277
|
+
const frontendResponse = await serveFrontendAsset({ env, req: c.req });
|
|
278
|
+
if (frontendResponse) {
|
|
279
|
+
return frontendResponse;
|
|
280
|
+
}
|
|
200
281
|
const externalFaviconUrl = resolveAdminFaviconTarget(env.ADMIN_ORIGIN);
|
|
201
282
|
if (externalFaviconUrl) {
|
|
202
283
|
return c.redirect(externalFaviconUrl, 302);
|
|
@@ -275,6 +356,19 @@ async function buildApp() {
|
|
|
275
356
|
return c.json(normalizeOpenApiDocument(spec as OpenApiSpec, new URL(c.req.url).origin));
|
|
276
357
|
});
|
|
277
358
|
|
|
359
|
+
app.on(['GET', 'HEAD'], '*', async (c) => {
|
|
360
|
+
const env = c.env as Env;
|
|
361
|
+
const frontendResponse = await serveFrontendAsset({ env, req: c.req });
|
|
362
|
+
if (frontendResponse) {
|
|
363
|
+
return frontendResponse;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return c.json({
|
|
367
|
+
code: 404,
|
|
368
|
+
message: `Path '${new URL(c.req.url).pathname}' was not found on this EdgeBase server.`,
|
|
369
|
+
}, 404);
|
|
370
|
+
});
|
|
371
|
+
|
|
278
372
|
app.notFound((c) => {
|
|
279
373
|
return c.json({
|
|
280
374
|
code: 404,
|
package/src/lib/admin-assets.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function resolveAdminAssetPath(pathname: string): string {
|
|
2
2
|
if (pathname === '/admin' || pathname === '/admin/') {
|
|
3
|
-
return '/';
|
|
3
|
+
return '/admin/index.html';
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
if (!pathname.startsWith('/admin/')) {
|
|
@@ -9,19 +9,19 @@ export function resolveAdminAssetPath(pathname: string): string {
|
|
|
9
9
|
|
|
10
10
|
const assetPath = pathname.slice('/admin'.length) || '/';
|
|
11
11
|
if (assetPath === '/' || assetPath === '') {
|
|
12
|
-
return '/';
|
|
12
|
+
return '/admin/index.html';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
if (assetPath.startsWith('/_app/')) {
|
|
16
|
-
return assetPath
|
|
16
|
+
return `/admin${assetPath}`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const lastSegment = assetPath.split('/').pop() ?? '';
|
|
20
20
|
if (lastSegment.includes('.')) {
|
|
21
|
-
return assetPath
|
|
21
|
+
return `/admin${assetPath}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
return '/';
|
|
24
|
+
return '/admin/index.html';
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function createAdminAssetRequest(request: Request): Request {
|