@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.
Files changed (65) hide show
  1. package/admin-build/_app/immutable/chunks/{DnpbvAPi.js → B9efkx2V.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{BhCO1Fpt.js → BMXWUTG-.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{CIOC1v_q.js → Bt4AyT3o.js} +3 -3
  4. package/admin-build/_app/immutable/chunks/CKVjMXZi.js +1 -0
  5. package/admin-build/_app/immutable/chunks/{BfpUQYr3.js → CMYgGhZR.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{DaXO-sFP.js → CTRjWhGs.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{BDYewzou.js → CwyE59Yt.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{D1u3u7xu.js → D8aeTKry.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{BYyykAbh.js → DGAHkap7.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{ejoEf2I5.js → DPgR4-0v.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{Dz9cUCuv.js → DYtrHeVQ.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{CvczjTXx.js → DcVb45Ds.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/Djnkhy-S.js +1 -0
  14. package/admin-build/_app/immutable/chunks/{Tea2dBJ8.js → fPy6xmgG.js} +1 -1
  15. package/admin-build/_app/immutable/chunks/{iEyeblJR.js → j4jxnAKj.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{BEM1BeVF.js → zl2AUKMP.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.DoUaxnew.js → app.Cmz0WjMl.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.JE7dcbK1.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.Dsxi8s7i.js → 0.y6D_QyUb.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.Cp2l-hol.js → 1.CndRxhbH.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.4oY6m8Nz.js → 10.CdA5FmXy.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.DfcozD4J.js → 11.DG8SzMp_.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.uJgZdCIA.js → 12.CvmQqpFa.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.CaN1kRev.js → 13.BbGNdswT.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.DQ5xIi3s.js → 14.CZKsN7-O.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.B_EkebTJ.js → 15.A7-CYgkG.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.Tko1ZX8-.js → 16.hgJT9H-x.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.BCmWMJX9.js → 17.DkWZbcN2.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.hmGhl1O2.js → 18.sX3Fb5gh.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.D-1infOo.js → 19.VAZUW-1K.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.CY4KKcBL.js → 20.DkIKxacG.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.DOjJlQKc.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.14Vd7bnt.js → 22.BDaHvtaw.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.Be6jK77o.js → 23.BVRzw_pD.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CSTFkr6R.js → 24.CVhSJyG0.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.DRTg8fHc.js → 25.Bme-9bZn.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.DKt-9lwQ.js → 26.Dsx7RIIs.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.D5caPu0F.js → 27.DMGQnzFM.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.hJhlnlyY.js → 28.GGwFmEhZ.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.CDYBzFyT.js → 29.Dnghr0nk.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.DMyKwkGn.js → 3.Cg7zZJP1.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.BaHNeEmc.js → 30.C0J24z3I.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.C6PV5L-2.js → 31.MdxFI8v6.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.9E118Ftm.js → 4.DCAOVzGE.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.D8guAl3v.js → 5.DzUQ-cTc.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.D1u__DtT.js → 6.CptBYTVj.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.DWXHnRFf.js → 7.DfeeQ0Rg.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.Dojd8krc.js → 8.CIcvctW7.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.CLtrr0K_.js → 9.QKrvq4RA.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/admin-assets.test.ts +7 -7
  54. package/src/__tests__/frontend-assets.test.ts +75 -0
  55. package/src/__tests__/frontend-config.test.ts +16 -0
  56. package/src/__tests__/frontend-routing.test.ts +200 -0
  57. package/src/durable-objects/room-runtime-base.ts +2 -0
  58. package/src/index.ts +97 -3
  59. package/src/lib/admin-assets.ts +5 -5
  60. package/src/lib/frontend-assets.ts +129 -0
  61. package/src/lib/frontend-config.ts +11 -0
  62. package/admin-build/_app/immutable/chunks/BaUG2TJ-.js +0 -1
  63. package/admin-build/_app/immutable/chunks/qKdzaeX3.js +0 -1
  64. package/admin-build/_app/immutable/entry/start.MmZh8oBH.js +0 -1
  65. 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/CvczjTXx.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/DaXO-sFP.js";import{D as rt,T as nt}from"../chunks/BfpUQYr3.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
+ 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":"1774791730603"}
1
+ {"version":"1775284872870"}
@@ -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.MmZh8oBH.js" rel="modulepreload">
9
- <link href="/admin/_app/immutable/chunks/BaUG2TJ-.js" rel="modulepreload">
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/qKdzaeX3.js" rel="modulepreload">
13
- <link href="/admin/_app/immutable/entry/app.DoUaxnew.js" rel="modulepreload">
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
- __sveltekit_jgts8u = {
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.MmZh8oBH.js"),
38
- import("/admin/_app/immutable/entry/app.DoUaxnew.js")
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.7",
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.7",
38
- "@edge-base/shared": "0.2.7"
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 === 'admin dashboard' ? 'is' : 'are';
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
- app.get('/', (c) => {
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,
@@ -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 {