@finesoft/front 0.4.3 → 0.5.0
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/browser-BYZq9Jp7.mjs +2 -0
- package/dist/{browser-DMw76sEo.d.mts → browser-JTs2jqVY.d.mts} +99 -19
- package/dist/browser.d.mts +2 -2
- package/dist/browser.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +22 -22
- package/docs/02-routing-and-controllers.md +127 -16
- package/docs/zh/02-routing-and-controllers.md +119 -8
- package/package.json +3 -3
- package/dist/browser-BIetJHp_.mjs +0 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{$ as e,$t as t,A as n,An as r,At as i,B as a,Bn as o,Bt as s,C as c,Cn as l,Ct as u,D as d,Dn as f,Dt as p,E as m,En as h,Et as g,F as _,Fn as v,Ft as y,G as b,Gn as x,Gt as S,H as C,Hn as w,Ht as T,I as E,In as D,It as ee,J as te,Jn as ne,Jt as re,K as ie,Kn as ae,Kt as O,L as k,Ln as oe,Lt as se,M as ce,Mn as le,Mt as ue,N as de,Nn as fe,Nt as pe,O as me,On as A,Ot as he,P as ge,Pn as _e,Pt as ve,Q as ye,Qt as be,R as xe,Rn as Se,Rt as Ce,S as we,Sn as Te,St as Ee,T as De,Tn as Oe,Tt as ke,U as Ae,Un as je,Ut as Me,V as Ne,Vn as Pe,Vt as j,W as Fe,Wn as Ie,Wt as Le,X as Re,Xt as ze,Y as Be,Yt as Ve,Z as He,Zt as Ue,_ as We,_n as Ge,_t as Ke,a as qe,an as Je,at as Ye,b as Xe,bn as Ze,bt as Qe,c as $e,cn as et,ct as M,d as tt,dn as nt,dt as rt,en as it,et as at,f as ot,fn as st,ft as ct,g as lt,gn as ut,gt as dt,h as ft,hn as pt,ht as mt,i as ht,in as gt,it as _t,j as vt,jn as yt,jt as bt,k as xt,kn as St,kt as N,l as Ct,ln as wt,lt as Tt,m as Et,mn as Dt,mt as Ot,n as kt,nn as At,nt as jt,o as Mt,on as Nt,ot as Pt,p as Ft,pn as It,pt as Lt,q as Rt,qn as zt,qt as Bt,r as Vt,rn as Ht,rt as Ut,s as Wt,sn as Gt,st as Kt,t as qt,tn as Jt,tt as Yt,u as Xt,un as Zt,ut as Qt,v as $t,vn as en,vt as tn,w as nn,wn as rn,wt as an,x as on,xn as sn,xt as cn,y as ln,yn as un,yt as dn,z as fn,zn as pn,zt as mn}from"./browser-BIetJHp_.mjs";import{Hono as P}from"hono";async function hn(e){return F(e,0)}async function F(e,t){if(t>=5)throw Error(`[SSR] Rewrite recursion depth exceeded (max 5) at "${e.url}"`);let{url:n,frameworkConfig:r,bootstrap:i,getErrorPage:a,renderApp:o,ssrContext:s,resolveLocale:c,loadMessages:l}=e,u=new URL(n,`http://localhost`),f=u.pathname+u.search,p=c?.(n,s?.request),m=p?{...r,locale:p.lang}:r,g=await h({locale:m.locale,loadMessages:l,context:m.locale?{runtime:`server`,fetch:gn(s?.fetch??m.fetch),url:f,request:s?.request}:void 0}),_={...m,fetch:s?.fetch??m.fetch},v=O.create({..._,_resolvedMessages:g});i(v);try{let n=await v.routeUrl(f);if(n?.renderMode===`csr`)return{html:``,head:``,css:``,serverData:[],renderMode:`csr`};let r,i=[],c;if(n){let l=d({url:f,intent:n.intent,container:v.container,request:s?.request}),u=await v.runBeforeLoad(l,n.beforeGuards);if(u.kind===`rewrite`)return F({...e,url:u.url},t+1);if(u.kind!==`next`){let e=await I(u,a,o,v);if(e)return e}try{r=await v.dispatch(n.intent),i=[{intent:n.intent,data:r}]}catch(e){v.container.resolve(wt.LOGGER).error(`[SSR] dispatch failed for intent "${n.intent.id}":`,e),r=a(500,`Internal error`)}let p={...l,page:r},m=await v.runAfterLoad(p,n.afterGuards);if(m.kind===`rewrite`)c=m.url;else if(m.kind!==`next`){let e=await I(m,a,o,v);if(e)return e}}else r=a(404,`Page not found`);let l=await o(r,v),u=p??v.getLocale();return{html:l.html,head:l.head,css:l.css,serverData:i,renderMode:n?.renderMode,slots:l.slots,locale:u,rewriteUrl:c}}finally{v.dispose()}}function gn(e){return(e??globalThis.fetch?.bind(globalThis))||(()=>{throw Error(`[ssrRender] loadMessages requires a fetch implementation.`)})}async function I(e,t,n,r){switch(e.kind){case`next`:case`rewrite`:return null;case`redirect`:return{html:``,head:``,css:``,serverData:[],redirect:{url:e.url,status:e.status}};case`deny`:{let i=await n(t(e.status,e.message),r);return{html:i.html,head:i.head,css:i.css,serverData:[],slots:i.slots,status:e.status}}}}function _n(e){let{bootstrap:t,getErrorPage:n,renderApp:r,frameworkConfig:i,resolveLocale:a,loadMessages:o}=e;return(e,s)=>hn({url:e,frameworkConfig:i??{},bootstrap:t,getErrorPage:n,renderApp:(e,t)=>r(e,t),ssrContext:s,resolveLocale:a,loadMessages:o})}const vn={HEAD:`<!--ssr-head-->`,BODY:`<!--ssr-body-->`,DATA:`<!--ssr-data-->`},L=/<!--ssr-([a-z][a-z0-9-]*)-->/g;function R(e){let{template:t,head:n,css:r,html:i,serializedData:a,slots:o,locale:s}=e,c={head:`${n}\n${r?`<style>${r}</style>`:``}`,body:i,data:`<script id="serialized-server-data" type="application/json">${a}<\/script>`,...o},l=t.replace(L,(e,t)=>c[t]??``);return s&&(l=B(l,s)),l}function z(e,t){let n=e.replace(L,()=>``);return t&&(n=B(n,t)),n}const yn=/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi,bn=/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi;function B(e,t){return e.replace(/(<html)([^>]*)(>)/i,(e,n,r,i)=>`${n}${r.replace(yn,``).replace(bn,``)} lang="${t.lang}" dir="${t.dir}"${i}`)}const V=`@finesoft/navigation-tree`;async function H(e){let{url:t,frameworkConfig:n,bootstrap:r,getErrorPage:i,renderApp:a,navigation:o,ssrContext:s,resolveLocale:c,loadMessages:l}=e,u=new URL(t,`http://localhost`),d=u.pathname+u.search,f=c?.(t,s?.request),p=f?{...n,locale:f.lang}:n,m=await h({locale:p.locale,loadMessages:l,context:p.locale?{runtime:`server`,fetch:Mn(s?.fetch??p.fetch),url:d,request:s?.request}:void 0}),g={...p,fetch:s?.fetch??p.fetch},_=O.create({...g,_resolvedMessages:m});r(_);try{let e=await xn(_,o,d);if(e===void 0)return jn({framework:_,resolvedLocale:f,renderApp:a,page:i(404,`Page not found`),snapshot:{tree:N(`@finesoft/not-found`),destinations:[]},serverData:[],renderMode:void 0,status:void 0});let{tree:t,renderMode:n}=e;if(n===`csr`)return{html:``,head:``,css:``,serverData:[],renderMode:`csr`,snapshot:{tree:t,destinations:[]}};let r,c=await Sn({framework:_,navigation:o,initialTree:t,fullPath:d,request:s?.request,getErrorPage:i,onRedirect:e=>{r??=e}}).resolve();if(r!==void 0)return{html:``,head:``,css:``,serverData:[],redirect:r,snapshot:c};let l=Cn(c,i);return jn({framework:_,resolvedLocale:f,renderApp:a,page:l.page,snapshot:c,serverData:En(c),renderMode:n,status:l.status})}finally{_.dispose()}}async function xn(e,t,n){let r=t.codec.decode(n,e.router);if(r!==void 0)return{tree:r,renderMode:void 0};let i=t.initial?.(n);if(i!==void 0)return{tree:i,renderMode:void 0};let a=await e.routeUrl(n);if(a!==null)return{tree:N(a.intent.id,a.intent.params??{}),renderMode:a.renderMode}}function Sn(e){let{framework:t,navigation:n,initialTree:r,fullPath:i,request:a,getErrorPage:o,onRedirect:s}=e;return at({intentDispatcher:t.intentDispatcher,router:t.router,initial:r,beforeLoad:n.beforeLoad,afterLoad:n.afterLoad,getErrorPage:o,onRedirect:s,createContext:({intent:e,params:n})=>({container:t.container,url:i,navigation:d({url:i,intent:{id:e,params:n},container:t.container,request:a})})})}function Cn(e,t){let n=wn(e.tree);if(n!==void 0){let t=e.destinations.find(e=>e.intent===n.intent&&Tn(e.params,n.params));if(t!==void 0)return t}return e.destinations.length>0?e.destinations[0]:{intent:`@finesoft/empty`,params:{},page:t(404,`Page not found`),status:404}}function wn(e){let t=e;for(;;)switch(t.kind){case`leaf`:return{intent:t.intent,params:t.params};case`stack`:if(t.entries.length===0)return;t=t.entries[t.entries.length-1];break;case`tabs`:{let e=t.branches[t.active];if(e===void 0)return;t=e;break}case`split`:{let e;for(let n of t.columns)n.content!==void 0&&(e=n.content);if(e===void 0)return;t=e;break}}}function Tn(e,t){let n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(let r of n)if(e[r]!==t[r])return!1;return!0}function En(e){let t=[];for(let n of e.destinations)t.push({intent:{id:n.intent,params:n.params},data:n.page});return t.push(Dn(e.tree)),t}function Dn(e){let t={__finesoftNavigationTree:!0,tree:M(e)};return{intent:{id:V},data:S(t,!0)}}function On(e){for(let t of e)if(An(t))return Kt(t.data.tree)}function kn(e){return e.filter(e=>!An(e))}function An(e){if(e.intent.id!==`@finesoft/navigation-tree`)return!1;let t=e.data;return typeof t==`object`&&!!t&&t.__finesoftNavigationTree===!0}async function jn(e){let{framework:t,resolvedLocale:n,renderApp:r,page:i,snapshot:a,serverData:o,renderMode:s,status:c}=e,l=await r(i,t,a),u=n??t.getLocale();return{html:l.html,head:l.head,css:l.css,serverData:o,renderMode:s,slots:l.slots,locale:u,status:c,snapshot:a}}function Mn(e){return(e??globalThis.fetch?.bind(globalThis))||(()=>{throw Error(`[ssrRenderNavigation] loadMessages requires a fetch implementation.`)})}function Nn(e){let{bootstrap:t,getErrorPage:n,renderApp:r,navigation:i,frameworkConfig:a,resolveLocale:o,loadMessages:s}=e;return(e,c)=>H({url:e,frameworkConfig:a??{},bootstrap:t,getErrorPage:n,renderApp:r,navigation:i,ssrContext:c,resolveLocale:o,loadMessages:s})}const Pn={"<":`\\u003C`,">":`\\u003E`,"/":`\\u002F`,"\u2028":`\\u2028`,"\u2029":`\\u2029`},Fn=RegExp(`[<>/\\u2028\\u2029]`,`g`);let In=!1;function Ln(e,t={}){let n=t.onUnmarkedPage??`all`,r=e.map(e=>Rn(e,n));return JSON.stringify(r).replace(Fn,e=>Pn[e]??e)}function Rn(e,t){let n=e.data,r=Me(n);if(r===!0)return e;if(Array.isArray(r))return{...e,data:zn(n,r)};if(t===`strict`)throw Error(`[serializeServerData] page for intent "${e.intent.id}" has no markPublic marker; wrap it with markPublic(page, ["field", ...]) to declare client-visible fields.`);return t===`base-fields`?{...e,data:zn(n,j)}:(Bn(e.intent.id),e)}function zn(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Bn(e){In||(In=!0,Vn()&&console.warn(`[finesoft/ssr] Page for intent "${e}" was serialized without markPublic(); all fields (including any sensitive data) are exposed in the SSR HTML. Wrap your page with markPublic(page, ["field", ...]) to declare client-visible fields explicitly. The next major version will default to BasePage-only fields when no marker is present.`))}function Vn(){try{return globalThis.process?.env?.NODE_ENV!==`production`}catch{return!1}}function Hn(e){return Object.entries(e).map(([e,t])=>t===``?e:`${e}="${Un(t)}"`).join(` `)}function Un(e){return e.replace(/&/g,`&`).replace(/"/g,`"`).replace(/</g,`<`)}async function Wn(e,t){let n=[];for(let r of e.destinations){let e=Ae(r.intent,r.params),i=await t({intent:r.intent,params:r.params,entryKey:e,page:r.page}),a=Hn(an(r.intent,e));n.push(`<div ${a}>${i}</div>`)}return n.join(``)}const Gn=new Map,Kn=e=>import(e),qn=typeof process<`u`&&process.env?.FINESOFT_DEBUG===`1`;function Jn(e){qn&&console.debug(`[finesoft:dynamic-import] ${e}`)}async function U(e){let t=!e.startsWith(`file:`)&&!e.startsWith(`/`);if(t){let t=Gn.get(e);if(t)return Jn(`cache hit → ${e}`),t}Jn(`importing → ${e}`);let n=await Kn(e);return t&&Gn.set(e,n),n}const W=10*1024*1024;function Yn(e){if(e.length>2048)return null;try{if(decodeURIComponent(e)!==e)return null}catch{return null}return e.startsWith(`//`)||!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(e)?null:e.startsWith(`/`)?e:`/${e}`}function Xn(e){if(!e.prefix.startsWith(`/`))throw Error(`[proxy] prefix must start with "/": "${e.prefix}"`);let t=e.target.startsWith(`https://`),n=e.target.startsWith(`http://`);if(!t&&!n)throw Error(`[proxy] target must start with "https://" or "http://": "${e.target}"`);n&&console.warn(`[proxy] ⚠ target "${e.target}" uses plain HTTP — traffic will not be encrypted. Use HTTPS in production to prevent data interception.`)}function G(e,t){for(let n of t){Xn(n);let t=n.methods??[`all`],r=`${n.prefix}/*`,i=async e=>{let t=Yn(e.req.path.replace(n.prefix,``));if(!t)return e.text(`Invalid path`,400);let r=new URL(t,n.target),i=new URL(n.target).origin;if(r.origin!==i)return e.text(`Invalid proxy target`,400);new URL(e.req.url).searchParams.forEach((e,t)=>r.searchParams.set(t,e));let a={...n.headers};if(n.auth){let e=process.env[n.auth.envKey];e?a.Authorization=n.auth.type===`bearer`?`Bearer ${e}`:`Basic ${e}`:console.warn(`[Proxy ${n.prefix}] Auth env var "${n.auth.envKey}" is not set`)}try{let t=await fetch(r.toString(),{headers:a,redirect:n.followRedirects?`follow`:`manual`}),i=t.headers.get(`Content-Length`);if(i&&parseInt(i,10)>W)return e.text(`Proxy response too large`,502);let o=await t.arrayBuffer();if(o.byteLength>W)return e.text(`Proxy response too large`,502);let s={"Content-Type":t.headers.get(`Content-Type`)??`application/json`};return n.cache&&(s[`Cache-Control`]=n.cache),e.newResponse(o,t.status,s)}catch(t){return console.error(`[Proxy ${n.prefix}]`,t),e.json({error:`Proxy request failed`},502)}};for(let n of t)e[n](r,i)}}function Zn(e){if(!e||e.length===0)return``;for(let t of e)Xn(t);let t=[];t.push(`
|
|
1
|
+
import{$ as e,$t as t,A as n,An as r,At as i,B as a,Bn as o,Bt as s,C as c,Cn as l,Ct as u,D as d,Dn as f,Dt as p,E as m,En as h,Et as g,F as _,Fn as v,Ft as y,G as b,Gn as x,Gt as S,H as ee,Hn as C,Ht as w,I as T,In as E,It as te,J as ne,Jn as re,Jt as ie,K as ae,Kn as oe,Kt as se,L as D,Ln as ce,Lt as le,M as ue,Mn as de,Mt as fe,N as pe,Nn as me,Nt as he,O as ge,On as _e,Ot as ve,P as ye,Pn as be,Pt as xe,Q as Se,Qt as Ce,R as we,Rn as Te,Rt as Ee,S as De,Sn as Oe,St as ke,T as Ae,Tn as je,Tt as O,U as k,Un as Me,Ut as Ne,V as Pe,Vn as Fe,Vt as Ie,W as Le,Wn as Re,Wt as A,X as ze,Xn as Be,Xt as Ve,Y as He,Yn as Ue,Yt as We,Z as Ge,Zn as Ke,Zt as qe,_ as Je,_n as Ye,_t as Xe,a as Ze,an as Qe,at as $e,b as et,bn as tt,bt as nt,c as rt,cn as it,ct as j,d as at,dn as ot,dt as st,en as ct,et as lt,f as ut,fn as M,ft as dt,g as ft,gn as pt,gt as mt,h as ht,hn as gt,ht as _t,i as vt,in as yt,it as bt,j as xt,jn as N,jt as St,k as Ct,kn as P,kt as wt,l as Tt,ln as Et,lt as F,m as Dt,mn as Ot,mt as kt,n as At,nn as jt,nt as Mt,o as Nt,on as Pt,ot as Ft,p as It,pn as Lt,pt as Rt,q as zt,qn as Bt,qt as I,r as Vt,rn as Ht,rt as Ut,s as Wt,sn as Gt,st as Kt,t as qt,tn as Jt,tt as L,u as Yt,un as Xt,ut as Zt,v as Qt,vn as $t,vt as en,w as tn,wn as nn,wt as rn,x as an,xn as on,xt as sn,y as cn,yn as ln,yt as un,z as dn,zn as fn,zt as pn}from"./browser-BYZq9Jp7.mjs";import{Hono as R}from"hono";async function z(e){return mn(e,0)}async function mn(e,t){if(t>=5)throw Error(`[SSR] Rewrite recursion depth exceeded (max 5) at "${e.url}"`);let{url:n,frameworkConfig:r,bootstrap:i,getErrorPage:a,renderApp:o,ssrContext:s,resolveLocale:c,loadMessages:l}=e,u=new URL(n,`http://localhost`),f=u.pathname+u.search,p=c?.(n,s?.request),m=p?{...r,locale:p.lang}:r,h=await P({locale:m.locale,loadMessages:l,context:m.locale?{runtime:`server`,fetch:hn(s?.fetch??m.fetch),url:f,request:s?.request}:void 0}),g={...m,fetch:s?.fetch??m.fetch},_=I.create({...g,_resolvedMessages:h});i(_);try{let n=await _.routeUrl(f);if(n?.renderMode===`csr`)return{html:``,head:``,css:``,serverData:[],renderMode:`csr`};let r,i=[],c;if(n){let l=d({url:f,intent:n.intent,container:_.container,request:s?.request}),u=await _.runBeforeLoad(l,n.beforeGuards);if(u.kind===`rewrite`)return mn({...e,url:u.url},t+1);if(u.kind!==`next`){let e=await gn(u,a,o,_);if(e)return e}try{r=await _.dispatch(n.intent),i=[{intent:n.intent,data:r}]}catch(e){_.container.resolve(M.LOGGER).error(`[SSR] dispatch failed for intent "${n.intent.id}":`,e),r=a(500,`Internal error`)}let p={...l,page:r},m=await _.runAfterLoad(p,n.afterGuards);if(m.kind===`rewrite`)c=m.url;else if(m.kind!==`next`){let e=await gn(m,a,o,_);if(e)return e}}else r=a(404,`Page not found`);let l=await o(r,_),u=p??_.getLocale();return{html:l.html,head:l.head,css:l.css,serverData:i,renderMode:n?.renderMode,slots:l.slots,locale:u,rewriteUrl:c}}finally{_.dispose()}}function hn(e){return(e??globalThis.fetch?.bind(globalThis))||(()=>{throw Error(`[ssrRender] loadMessages requires a fetch implementation.`)})}async function gn(e,t,n,r){switch(e.kind){case`next`:case`rewrite`:return null;case`redirect`:return{html:``,head:``,css:``,serverData:[],redirect:{url:e.url,status:e.status}};case`deny`:{let i=await n(t(e.status,e.message),r);return{html:i.html,head:i.head,css:i.css,serverData:[],slots:i.slots,status:e.status}}}}function _n(e){let{bootstrap:t,getErrorPage:n,renderApp:r,frameworkConfig:i,resolveLocale:a,loadMessages:o}=e;return(e,s)=>z({url:e,frameworkConfig:i??{},bootstrap:t,getErrorPage:n,renderApp:(e,t)=>r(e,t),ssrContext:s,resolveLocale:a,loadMessages:o})}const vn={HEAD:`<!--ssr-head-->`,BODY:`<!--ssr-body-->`,DATA:`<!--ssr-data-->`},yn=/<!--ssr-([a-z][a-z0-9-]*)-->/g;function B(e){let{template:t,head:n,css:r,html:i,serializedData:a,slots:o,locale:s}=e,c={head:`${n}\n${r?`<style>${r}</style>`:``}`,body:i,data:`<script id="serialized-server-data" type="application/json">${a}<\/script>`,...o},l=t.replace(yn,(e,t)=>c[t]??``);return s&&(l=Sn(l,s)),l}function V(e,t){let n=e.replace(yn,()=>``);return t&&(n=Sn(n,t)),n}const bn=/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi,xn=/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi;function Sn(e,t){return e.replace(/(<html)([^>]*)(>)/i,(e,n,r,i)=>`${n}${r.replace(bn,``).replace(xn,``)} lang="${t.lang}" dir="${t.dir}"${i}`)}const Cn=`@finesoft/navigation-tree`;async function wn(e){let{url:t,frameworkConfig:n,bootstrap:r,getErrorPage:a,renderApp:o,navigation:s,ssrContext:c,resolveLocale:l,loadMessages:u}=e,d=new URL(t,`http://localhost`),f=d.pathname+d.search,p=l?.(t,c?.request),m=p?{...n,locale:p.lang}:n,h=await P({locale:m.locale,loadMessages:u,context:m.locale?{runtime:`server`,fetch:In(c?.fetch??m.fetch),url:f,request:c?.request}:void 0}),g={...m,fetch:c?.fetch??m.fetch},_=I.create({...g,_resolvedMessages:h});r(_);try{let e=await Tn(_,s,f);if(e===void 0)return Fn({framework:_,resolvedLocale:p,renderApp:o,page:a(404,`Page not found`),snapshot:{tree:i(`@finesoft/not-found`),destinations:[]},serverData:[],renderMode:void 0,status:void 0});let{tree:t,renderMode:n}=e;if(n===`csr`)return{html:``,head:``,css:``,serverData:[],renderMode:`csr`,snapshot:{tree:t,destinations:[]}};let r,l=await En({framework:_,navigation:s,initialTree:t,fullPath:f,request:c?.request,getErrorPage:a,onRedirect:e=>{r??=e}}).resolve();if(r!==void 0)return{html:``,head:``,css:``,serverData:[],redirect:r,snapshot:l};let u=Dn(l,a);return Fn({framework:_,resolvedLocale:p,renderApp:o,page:u.page,snapshot:l,serverData:An(l),renderMode:n,status:u.status})}finally{_.dispose()}}async function Tn(e,t,n){let r=t.codec.decode(n,e.router);if(r!==void 0)return{tree:r,renderMode:void 0};let a=t.initial?.(n);if(a!==void 0)return{tree:a,renderMode:void 0};let o=await e.routeUrl(n);if(o!==null)return{tree:i(o.intent.id,o.intent.params??{}),renderMode:o.renderMode}}function En(e){let{framework:t,navigation:n,initialTree:r,fullPath:i,request:a,getErrorPage:o,onRedirect:s}=e;return L({intentDispatcher:t.intentDispatcher,router:t.router,initial:r,beforeLoad:n.beforeLoad,afterLoad:n.afterLoad,getErrorPage:o,onRedirect:s,createContext:({intent:e,params:n})=>({container:t.container,url:i,navigation:d({url:i,intent:{id:e,params:n},container:t.container,request:a})})})}function Dn(e,t){let n=On(e.tree);if(n!==void 0){let t=e.destinations.find(e=>e.intent===n.intent&&kn(e.params,n.params));if(t!==void 0)return t}return e.destinations.length>0?e.destinations[0]:{intent:`@finesoft/empty`,params:{},page:t(404,`Page not found`),status:404}}function On(e){let t=e;for(;;)switch(t.kind){case`leaf`:return{intent:t.intent,params:t.params};case`stack`:if(t.entries.length===0)return;t=t.entries[t.entries.length-1];break;case`tabs`:{let e=t.branches[t.active];if(e===void 0)return;t=e;break}case`split`:{let e;for(let n of t.columns)n.content!==void 0&&(e=n.content);if(e===void 0)return;t=e;break}}}function kn(e,t){let n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(let r of n)if(e[r]!==t[r])return!1;return!0}function An(e){let t=[];for(let n of e.destinations)t.push({intent:{id:n.intent,params:n.params},data:n.page});return t.push(jn(e.tree)),t}function jn(e){let t={__finesoftNavigationTree:!0,tree:F(e)};return{intent:{id:Cn},data:se(t,!0)}}function Mn(e){for(let t of e)if(Pn(t))return j(t.data.tree)}function Nn(e){return e.filter(e=>!Pn(e))}function Pn(e){if(e.intent.id!==`@finesoft/navigation-tree`)return!1;let t=e.data;return typeof t==`object`&&!!t&&t.__finesoftNavigationTree===!0}async function Fn(e){let{framework:t,resolvedLocale:n,renderApp:r,page:i,snapshot:a,serverData:o,renderMode:s,status:c}=e,l=await r(i,t,a),u=n??t.getLocale();return{html:l.html,head:l.head,css:l.css,serverData:o,renderMode:s,slots:l.slots,locale:u,status:c,snapshot:a}}function In(e){return(e??globalThis.fetch?.bind(globalThis))||(()=>{throw Error(`[ssrRenderNavigation] loadMessages requires a fetch implementation.`)})}function Ln(e){let{bootstrap:t,getErrorPage:n,renderApp:r,navigation:i,frameworkConfig:a,resolveLocale:o,loadMessages:s}=e;return(e,c)=>wn({url:e,frameworkConfig:a??{},bootstrap:t,getErrorPage:n,renderApp:r,navigation:i,ssrContext:c,resolveLocale:o,loadMessages:s})}const Rn={"<":`\\u003C`,">":`\\u003E`,"/":`\\u002F`,"\u2028":`\\u2028`,"\u2029":`\\u2029`},zn=RegExp(`[<>/\\u2028\\u2029]`,`g`);let Bn=!1;function Vn(e,t={}){let n=t.onUnmarkedPage??`all`,r=e.map(e=>Hn(e,n));return JSON.stringify(r).replace(zn,e=>Rn[e]??e)}function Hn(e,t){let n=e.data,r=A(n);if(r===!0)return e;if(Array.isArray(r))return{...e,data:Un(n,r)};if(t===`strict`)throw Error(`[serializeServerData] page for intent "${e.intent.id}" has no markPublic marker; wrap it with markPublic(page, ["field", ...]) to declare client-visible fields.`);return t===`base-fields`?{...e,data:Un(n,w)}:(Wn(e.intent.id),e)}function Un(e,t){let n={};for(let r of t)r in e&&(n[r]=e[r]);return n}function Wn(e){Bn||(Bn=!0,Gn()&&console.warn(`[finesoft/ssr] Page for intent "${e}" was serialized without markPublic(); all fields (including any sensitive data) are exposed in the SSR HTML. Wrap your page with markPublic(page, ["field", ...]) to declare client-visible fields explicitly. The next major version will default to BasePage-only fields when no marker is present.`))}function Gn(){try{return globalThis.process?.env?.NODE_ENV!==`production`}catch{return!1}}function Kn(e){return Object.entries(e).map(([e,t])=>t===``?e:`${e}="${qn(t)}"`).join(` `)}function qn(e){return e.replace(/&/g,`&`).replace(/"/g,`"`).replace(/</g,`<`)}async function Jn(e,t){let n=[];for(let r of e.destinations){let e=k(r.intent,r.params),i=await t({intent:r.intent,params:r.params,entryKey:e,page:r.page}),a=Kn(O(r.intent,e));n.push(`<div ${a}>${i}</div>`)}return n.join(``)}const Yn=new Map,Xn=e=>import(e),Zn=typeof process<`u`&&process.env?.FINESOFT_DEBUG===`1`;function Qn(e){Zn&&console.debug(`[finesoft:dynamic-import] ${e}`)}async function H(e){let t=!e.startsWith(`file:`)&&!e.startsWith(`/`);if(t){let t=Yn.get(e);if(t)return Qn(`cache hit → ${e}`),t}Qn(`importing → ${e}`);let n=await Xn(e);return t&&Yn.set(e,n),n}const U=10*1024*1024;function $n(e){if(e.length>2048)return null;try{if(decodeURIComponent(e)!==e)return null}catch{return null}return e.startsWith(`//`)||!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(e)?null:e.startsWith(`/`)?e:`/${e}`}function er(e){if(!e.prefix.startsWith(`/`))throw Error(`[proxy] prefix must start with "/": "${e.prefix}"`);let t=e.target.startsWith(`https://`),n=e.target.startsWith(`http://`);if(!t&&!n)throw Error(`[proxy] target must start with "https://" or "http://": "${e.target}"`);n&&console.warn(`[proxy] ⚠ target "${e.target}" uses plain HTTP — traffic will not be encrypted. Use HTTPS in production to prevent data interception.`)}function W(e,t){for(let n of t){er(n);let t=n.methods??[`all`],r=`${n.prefix}/*`,i=async e=>{let t=$n(e.req.path.replace(n.prefix,``));if(!t)return e.text(`Invalid path`,400);let r=new URL(t,n.target),i=new URL(n.target).origin;if(r.origin!==i)return e.text(`Invalid proxy target`,400);new URL(e.req.url).searchParams.forEach((e,t)=>r.searchParams.set(t,e));let a={...n.headers};if(n.auth){let e=process.env[n.auth.envKey];e?a.Authorization=n.auth.type===`bearer`?`Bearer ${e}`:`Basic ${e}`:console.warn(`[Proxy ${n.prefix}] Auth env var "${n.auth.envKey}" is not set`)}try{let t=await fetch(r.toString(),{headers:a,redirect:n.followRedirects?`follow`:`manual`}),i=t.headers.get(`Content-Length`);if(i&&parseInt(i,10)>U)return e.text(`Proxy response too large`,502);let o=await t.arrayBuffer();if(o.byteLength>U)return e.text(`Proxy response too large`,502);let s={"Content-Type":t.headers.get(`Content-Type`)??`application/json`};return n.cache&&(s[`Cache-Control`]=n.cache),e.newResponse(o,t.status,s)}catch(t){return console.error(`[Proxy ${n.prefix}]`,t),e.json({error:`Proxy request failed`},502)}};for(let n of t)e[n](r,i)}}function tr(e){if(!e||e.length===0)return``;for(let t of e)er(t);let t=[];t.push(`
|
|
2
2
|
// ─── 框架声明式代理路由 ───
|
|
3
3
|
function _sanitizeProxyPath(raw) {
|
|
4
4
|
if (raw.length > 2048) return null;
|
|
@@ -21,12 +21,12 @@ function _sanitizeProxyPath(raw) {
|
|
|
21
21
|
const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${o} });
|
|
22
22
|
// Content-Length 快速拒绝,防止 serverless/edge 加载超大响应到内存
|
|
23
23
|
const _cl = _resp.headers.get("Content-Length");
|
|
24
|
-
if (_cl && parseInt(_cl, 10) > ${
|
|
24
|
+
if (_cl && parseInt(_cl, 10) > ${U}) {
|
|
25
25
|
return c.text("Proxy response too large", 502);
|
|
26
26
|
}
|
|
27
27
|
// arrayBuffer 保留二进制完整性
|
|
28
28
|
const _body = await _resp.arrayBuffer();
|
|
29
|
-
if (_body.byteLength > ${
|
|
29
|
+
if (_body.byteLength > ${U}) {
|
|
30
30
|
return c.text("Proxy response too large", 502);
|
|
31
31
|
}
|
|
32
32
|
const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
|
|
@@ -37,7 +37,7 @@ function _sanitizeProxyPath(raw) {
|
|
|
37
37
|
return c.json({ error: "Proxy request failed" }, 502);
|
|
38
38
|
}
|
|
39
39
|
}`;for(let n of e)t.push(`app.${n}(${r}, ${c});`)}return t.join(`
|
|
40
|
-
`)}const
|
|
40
|
+
`)}const nr=[`vite`,`esbuild`,`rollup`,`fsevents`,`lightningcss`],rr=[`node:async_hooks`,`node:buffer`,`node:crypto`,`node:fs`,`node:http`,`node:http2`,`node:module`,`node:net`,`node:os`,`node:path`,`node:stream`,`node:url`,`node:util`,`node:zlib`,`crypto`,`http`,`http2`,`stream`];function G(e,t){let n=e.setupPath?`import _setupDefault from "./${e.setupPath}";`:``,r=e.setupPath?`if (typeof _setupDefault === "function") await _setupDefault(app);`:``,i=JSON.stringify(e.renderModes??{}),a=t.platformCache?t.platformCache:`
|
|
41
41
|
const ISR_CACHE_MAX = 1000;
|
|
42
42
|
const _isrMap = new Map();
|
|
43
43
|
async function platformCacheGet(url) {
|
|
@@ -109,7 +109,7 @@ function matchRenderMode(url) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
const app = new Hono();
|
|
112
|
-
${
|
|
112
|
+
${tr(e.proxies??[])}
|
|
113
113
|
${r}
|
|
114
114
|
${t.platformMiddleware??``}
|
|
115
115
|
|
|
@@ -173,8 +173,8 @@ app.get("*", async (c) => {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
${t.platformExport}
|
|
176
|
-
`}async function
|
|
177
|
-
<style>`+i+`</style>`,body:n,data:`<script id="serialized-server-data" type="application/json">`+s+`<\/script>`})[t]??``);if(o){let{getLocaleAttributes:e}=await
|
|
176
|
+
`}async function K(e,t){await e.vite.build({root:e.root,build:{ssr:t.entry,outDir:t.outDir,emptyOutDir:t.emptyOutDir??!0,target:t.target??`node18`,rollupOptions:{output:{entryFileNames:t.fileName??`index.mjs`}}},ssr:{noExternal:t.noExternal!==!1,external:[...t.external??nr,...rr]},resolve:e.resolvedResolve,css:e.resolvedCss})}function q(e,t,n){let{fs:r,path:i}=e;r.cpSync(i.resolve(e.root,`dist/client`),t,{recursive:!0}),n?.excludeHtml!==!1&&r.rmSync(i.join(t,`index.html`),{force:!0})}async function J(e){let{fs:t,path:n,root:r,vite:i}=e,{pathToFileURL:a}=await H(`node:url`),o=e.bootstrapEntry??`src/lib/bootstrap.ts`,s=[];if(t.existsSync(n.resolve(r,o))){await i.build({root:r,build:{ssr:o,outDir:n.resolve(r,`dist/server`),emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`_routes_prerender.mjs`}}},resolve:e.resolvedResolve});let c=a(n.resolve(r,`dist/server/_routes_prerender.mjs`)).href,l=await H(c);s=l.routes??l.default??[],t.rmSync(n.resolve(r,`dist/server/_routes_prerender.mjs`),{force:!0})}let c=new Set;for(let e of s)e.renderMode===`prerender`&&e.path&&!e.path.includes(`:`)&&c.add(e.path);if(e.renderModes)for(let[t,n]of Object.entries(e.renderModes))n===`prerender`&&!t.includes(`*`)&&!t.includes(`:`)&&c.add(t);if(e.locales?.length){let t=[...c];for(let n of e.locales)for(let e of t){let t=e===`/`?`/${n}`:`/${n}${e}`;c.add(t)}}if(c.size===0)return[];let l=a(n.resolve(r,`dist/server/ssr.js`)).href,u=await H(l),d=[];for(let t of c)try{let{html:n,head:r,css:i,serverData:a,locale:o}=await u.render(t),s=u.serializeServerData(a),c=e.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g,(e,t)=>({head:r+`
|
|
177
|
+
<style>`+i+`</style>`,body:n,data:`<script id="serialized-server-data" type="application/json">`+s+`<\/script>`})[t]??``);if(o){let{getLocaleAttributes:e}=await H(`@finesoft/core`),t=e(o);c=c.replace(/<html([^>]*)>/i,(e,n)=>`<html${n.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi,``).replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi,``)} lang="${t.lang}" dir="${t.dir}">`)}d.push({url:t,html:c})}catch(e){console.warn(` [prerender] Failed to render ${t}:`,e)}return d.length>0&&console.log(` Pre-rendered ${d.length} pages (${c.size} routes)\n`),d}function ir(){return{name:`cloudflare`,async build(e){let{fs:t,path:n,root:r}=e,i=n.resolve(r,`dist/cloudflare`);t.rmSync(i,{recursive:!0,force:!0});let a=G(e,{platformImport:``,platformExport:`export default app;`,platformCache:`
|
|
178
178
|
const ISR_CACHE_TTL = 3600; // 1 hour
|
|
179
179
|
async function platformCacheGet(url) {
|
|
180
180
|
try {
|
|
@@ -194,8 +194,8 @@ async function platformCacheSet(url, html) {
|
|
|
194
194
|
});
|
|
195
195
|
await cache.put(cacheKey, resp);
|
|
196
196
|
} catch {}
|
|
197
|
-
}`}),o=n.resolve(r,`.cf-entry.tmp.mjs`);t.writeFileSync(o,a);try{await
|
|
198
|
-
`)}}}function
|
|
197
|
+
}`}),o=n.resolve(r,`.cf-entry.tmp.mjs`);t.writeFileSync(o,a);try{await K(e,{entry:`.cf-entry.tmp.mjs`,outDir:i,target:`es2022`,fileName:`_worker.js`}),q(e,n.resolve(i,`assets`));let r=await J(e);for(let{url:e,html:a}of r){let r=e===`/`?n.join(i,`assets`,`index.html`):n.join(i,`assets`,e,`index.html`);t.mkdirSync(n.resolve(r,`..`),{recursive:!0}),t.writeFileSync(r,a)}}finally{t.rmSync(o,{force:!0})}console.log(` Cloudflare output → dist/cloudflare/
|
|
198
|
+
`)}}}function ar(){return{name:`netlify`,async build(e){let{fs:t,path:n,root:r}=e,i=n.resolve(r,`.netlify/functions-internal/ssr`);t.rmSync(n.resolve(r,`.netlify`),{recursive:!0,force:!0});let a=G(e,{platformImport:`import { handle } from "hono/netlify";`,platformExport:`export default handle(app);
|
|
199
199
|
export const config = { path: "/*", preferStatic: true };`,platformCache:`
|
|
200
200
|
const ISR_SWR_TTL = 3600;
|
|
201
201
|
const ISR_CACHE_MAX = 1000;
|
|
@@ -210,10 +210,10 @@ async function platformCacheSet(url, html) {
|
|
|
210
210
|
}
|
|
211
211
|
_isrMap.set(url, html);
|
|
212
212
|
}`,platformPrerenderResponseHook:`c.header("Cache-Control", "public, max-age=0, must-revalidate");
|
|
213
|
-
c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`}),o=n.resolve(r,`.netlify-entry.tmp.mjs`);t.writeFileSync(o,a);try{await
|
|
214
|
-
`);let s=await
|
|
213
|
+
c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`}),o=n.resolve(r,`.netlify-entry.tmp.mjs`);t.writeFileSync(o,a);try{await K(e,{entry:`.netlify-entry.tmp.mjs`,outDir:i,target:`node18`})}finally{t.rmSync(o,{force:!0})}t.writeFileSync(n.resolve(r,`dist/client/_redirects`),`/* /.netlify/functions/ssr 200
|
|
214
|
+
`);let s=await J(e),c=n.resolve(r,`dist/client`);for(let{url:e,html:r}of s){let i=e===`/`?n.join(c,`index.html`):n.join(c,e,`index.html`);t.mkdirSync(n.resolve(i,`..`),{recursive:!0}),t.writeFileSync(i,r)}console.log(` Netlify output → .netlify/functions-internal/ssr/
|
|
215
215
|
Publish dir: dist/client/
|
|
216
|
-
`)}}}function
|
|
216
|
+
`)}}}function or(){return{name:`node`,async build(e){let{fs:t,path:n,root:r}=e,i=G(e,{platformImport:`import { serve } from "@hono/node-server";
|
|
217
217
|
import { readFileSync, existsSync } from "node:fs";
|
|
218
218
|
import { resolve, dirname } from "node:path";
|
|
219
219
|
import { fileURLToPath } from "node:url";`,platformMiddleware:`
|
|
@@ -241,14 +241,14 @@ const port = +(process.env.PORT || 3000);
|
|
|
241
241
|
serve({ fetch: app.fetch, port }, (info) => {
|
|
242
242
|
console.log(\`Server running at http://localhost:\${info.port}\`);
|
|
243
243
|
});
|
|
244
|
-
`}),a=n.resolve(r,`.node-entry.tmp.mjs`);t.writeFileSync(a,i);try{await
|
|
244
|
+
`}),a=n.resolve(r,`.node-entry.tmp.mjs`);t.writeFileSync(a,i);try{await K(e,{entry:`.node-entry.tmp.mjs`,outDir:n.resolve(r,`dist/server`),target:`node18`,emptyOutDir:!1})}finally{t.rmSync(a,{force:!0})}let o=await J(e);if(o.length>0){let e=n.resolve(r,`dist/prerender`);t.mkdirSync(e,{recursive:!0});for(let{url:r,html:i}of o){let a=r===`/`?n.join(e,`index.html`):n.join(e,r,`index.html`);t.mkdirSync(n.resolve(a,`..`),{recursive:!0}),t.writeFileSync(a,i)}}console.log(` Node output → dist/server/index.mjs
|
|
245
245
|
Run: node dist/server/index.mjs
|
|
246
|
-
`)}}}function
|
|
247
|
-
`)}}}async function
|
|
248
|
-
<style>`+n+`</style>`,body:r,data:`<script id="serialized-server-data" type="application/json">`+i+`<\/script>`};return e.replace(a,(e,t)=>o[t]??``)}function
|
|
249
|
-
`)}),o=n.resolve(r,`.vercel-entry.tmp.mjs`);t.writeFileSync(o,a);try{let i=n.resolve(r,`.vercel/output/functions/ssr.func`);await
|
|
250
|
-
`)}}}function
|
|
251
|
-
`))}if(u)return{vite:a};if(!i){let e=a;if(!e){let{createServer:t}=await
|
|
246
|
+
`)}}}function sr(e={}){return{name:`static`,async build(t){let{fs:n,path:r,root:i}=t,a=r.resolve(i,`dist/static`);n.rmSync(a,{recursive:!0,force:!0}),n.mkdirSync(a,{recursive:!0});let{pathToFileURL:o}=await H(`node:url`),s=o(r.resolve(i,`dist/server/ssr.js`)).href,c=await H(s);t.copyStaticAssets(a,{excludeHtml:!0});let{paths:l,defs:u}=await cr(t,e);console.log(` Pre-rendering ${l.length} pages...\n`);for(let e of l)try{let i=dr(e,u.find(t=>t.path===e)?.renderMode,t.renderModes),o;if(i===`csr`)o=ur(t.templateHtml);else{let{html:n,head:r,css:i,serverData:a}=await c.render(e),s=c.serializeServerData(a);o=lr(t.templateHtml,r,i,n,s)}let s=e===`/`?r.join(a,`index.html`):r.join(a,e,`index.html`);n.mkdirSync(r.resolve(s,`..`),{recursive:!0}),n.writeFileSync(s,o)}catch(t){console.warn(` [static] Failed to render ${e}:`,t)}console.log(` Static output → dist/static/
|
|
247
|
+
`)}}}async function cr(e,t){let n=t.routesExport??`src/lib/bootstrap.ts`,r=[],i=[];try{let{pathToFileURL:t}=await H(`node:url`);await e.vite.build({root:e.root,build:{ssr:n,outDir:e.path.resolve(e.root,`dist/server`),emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`_routes.mjs`}}},resolve:e.resolvedResolve});let a=t(e.path.resolve(e.root,`dist/server/_routes.mjs`)).href,o=await H(a),s=o.routes??o.default;if(Array.isArray(s))for(let e of s)e.path&&!e.path.includes(`:`)&&(r.push(e.path),i.push({path:e.path,renderMode:e.renderMode}));e.fs.rmSync(e.path.resolve(e.root,`dist/server/_routes.mjs`),{force:!0})}catch(e){console.warn(` [static] Could not load routes from "${n}". Using "/" only.`,e),r.length===0&&r.push(`/`)}if(t.dynamicRoutes)for(let e of t.dynamicRoutes)r.includes(e)||r.push(e);return r.length===0&&r.push(`/`),{paths:r,defs:i}}function lr(e,t,n,r,i){let a=/<!--ssr-([a-z][a-z0-9-]*)-->/g,o={head:t+`
|
|
248
|
+
<style>`+n+`</style>`,body:r,data:`<script id="serialized-server-data" type="application/json">`+i+`<\/script>`};return e.replace(a,(e,t)=>o[t]??``)}function ur(e){return e.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g,()=>``)}function dr(e,t,n){if(n){if(n[e])return n[e];for(let[t,r]of Object.entries(n))if(t.includes(`*`)){let n=t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);if(RegExp(`^`+n.replace(/\*/g,`.*`)+`$`).test(e))return r}}return t??`ssr`}function fr(){return{name:`vercel`,async build(e){let{fs:t,path:n,root:r}=e,i=n.resolve(r,`.vercel/output`);t.rmSync(i,{recursive:!0,force:!0});let a=G(e,{platformImport:`import { getRequestListener } from "@hono/node-server";`,platformExport:[`const _listener = getRequestListener(app.fetch);`,`export default (req, res) => {`,` const m = req.headers["x-now-route-matches"];`,` if (typeof m === "string") {`,` try {`,` const p = new URLSearchParams(m);`,` const c = p.get("1");`,` if (c != null) {`,` const qi = (req.url || "").indexOf("?");`,` const qs = qi !== -1 ? req.url.slice(qi) : "";`,` req.url = "/" + decodeURIComponent(c) + qs;`,` }`,` } catch {}`,` }`,` return _listener(req, res);`,`};`].join(`
|
|
249
|
+
`)}),o=n.resolve(r,`.vercel-entry.tmp.mjs`);t.writeFileSync(o,a);try{let i=n.resolve(r,`.vercel/output/functions/ssr.func`);await K(e,{entry:`.vercel-entry.tmp.mjs`,outDir:i,target:`node18`}),t.writeFileSync(n.resolve(i,`.vc-config.json`),JSON.stringify({runtime:`nodejs20.x`,handler:`index.mjs`,launcherType:`Nodejs`},null,2)),q(e,n.resolve(r,`.vercel/output/static`)),t.writeFileSync(n.resolve(r,`.vercel/output/config.json`),JSON.stringify({version:3,routes:[{handle:`filesystem`},{src:`/(.*)`,dest:`/ssr`}]},null,2))}finally{t.rmSync(o,{force:!0})}let s=await J(e),c=n.resolve(r,`.vercel/output/static`);for(let{url:e,html:r}of s){let i=e===`/`?n.join(c,`index.html`):n.join(c,e,`index.html`);t.mkdirSync(n.resolve(i,`..`),{recursive:!0}),t.writeFileSync(i,r)}if(s.length>0){let e=n.resolve(r,`.vercel/output/config.json`),i=JSON.parse(t.readFileSync(e,`utf-8`));i.overrides=i.overrides??{};for(let{url:e}of s){let t=e===`/`?`index.html`:`${e.replace(/^\//,``)}/index.html`;i.overrides[t]={path:e===`/`?`/`:e,contentType:`text/html; charset=utf-8`}}t.writeFileSync(e,JSON.stringify(i,null,2))}console.log(` Vercel output → .vercel/output/
|
|
250
|
+
`)}}}function Y(e){if(typeof e!=`string`)return e;switch(e){case`vercel`:return fr();case`cloudflare`:return ir();case`netlify`:return ar();case`node`:return or();case`static`:return sr();case`auto`:return pr();default:throw Error(`[finesoft] Unknown adapter: "${e}". Available: vercel, cloudflare, netlify, node, static, auto`)}}function pr(){return{name:`auto`,async build(e){let t=mr();return console.log(` [auto] Detected platform: ${t}\n`),Y(t).build(e)}}}function mr(){return process.env.VERCEL?`vercel`:process.env.CF_PAGES?`cloudflare`:process.env.NETLIFY?`netlify`:`node`}function hr(e,t=1){return((n,r)=>{if(typeof n==`string`&&n.startsWith(`/`)){let i=new Request(`http://localhost${n}`,r);return i.headers.set(`x-ssr-depth`,String(t)),Promise.resolve(e(i))}return globalThis.fetch(n,r)})}function gr(e,t){if(!t)return null;let n=e.split(`?`)[0];if(t[n])return t[n];for(let[e,r]of Object.entries(t))if(e.includes(`*`)){let t=e.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);if(RegExp(`^`+t.replace(/\*/g,`.*`)+`$`).test(n))return r}return null}function X(e){let{root:t,vite:n,isProduction:r,ssrEntryPath:i=`/src/ssr.ts`,ssrProductionModule:a,parentFetch:o,renderModes:s,defaultLocale:c}=e,l=new R,u=new D(1e3),d;async function f(e){if(!r&&n){let{readFileSync:r}=await H(`node:fs`),i=r((await H(`node:path`)).resolve(t,`index.html`),`utf-8`);return n.transformIndexHtml(e,i)}if(d)return d;if(globalThis.Deno!==void 0){let e=import.meta.url;return d=globalThis.Deno.readTextFileSync(new URL(`../dist/client/index.html`,e)),d}let{readFileSync:i}=await H(`node:fs`);return d=i((await H(`node:path`)).resolve(t,`dist/client/index.html`),`utf-8`),d}async function p(){if(!r&&n)return await n.ssrLoadModule(i);if(a)return H(a);let e=await H(`node:path`),{pathToFileURL:o}=await H(`node:url`),s=o(e.resolve(t,`dist/server/ssr.js`)).href;return H(s)}return l.get(`*`,async e=>{let t=parseInt(e.req.header(`x-ssr-depth`)??`0`,10);if(t>=5)return e.text(`SSR recursion loop detected`,508);let i=e.req.path+(e.req.url.includes(`?`)?`?`+e.req.url.split(`?`)[1]:``);try{let n=await f(i),r=await p();if(typeof r.render!=`function`||typeof r.serializeServerData!=`function`)throw Error(`[SSR] Module missing required exports: render, serializeServerData`);let{render:a,serializeServerData:l}=r,d=gr(i,s);if(d===`csr`)return e.html(V(n,c?N(c):void 0));let m=u.get(i);if(m)return e.html(m);let h=o?hr(o,t+1):void 0,g={request:e.req.raw};h&&(g.fetch=h);let{html:_,head:v,css:y,serverData:b,renderMode:x,redirect:S,slots:ee,locale:C,status:w,rewriteUrl:T}=await a(i,g);if(S)return e.redirect(S.url,S.status);if(x===`csr`)return e.html(V(n,C));let E=B({template:n,head:v,css:y,html:_,serializedData:l(b),slots:ee,locale:C});return(x===`prerender`||d===`prerender`)&&!w&&!T&&u.set(i,E),T&&e.header(`Content-Location`,T),w&&w>=400?e.html(E,w):e.html(E)}catch(t){return!r&&n&&n.ssrFixStacktrace(t),console.error(`[SSR Error]`,t),e.text(`Internal Server Error`,500)}}),l}function Z(){return{isDeno:globalThis.Deno!==void 0,isBun:globalThis.Bun!==void 0,isVercel:!!process.env.VERCEL,isProduction:process.env.NODE_ENV===`production`}}async function _r(e,t=0){if(globalThis.Deno!==void 0){let n=new URL(e);for(let e=0;e<t;e++)n=new URL(`..`,n);return n.pathname}let n=await H(`node:path`),{fileURLToPath:r}=await H(`node:url`),i=n.normalize(n.dirname(r(e)));for(let e=0;e<t;e++)i=n.resolve(i,`..`);return i}async function Q(e){let{app:t,root:n,port:r=3e3,isProduction:i,vite:a,routes:o,ssrEntryPath:s}=e,{isDeno:c,isBun:l,isVercel:u}=e.runtime??Z();function d(){let e=[`\n Server running at http://localhost:${r}\n`];if(o&&o.length>0){e.push(` Routes:`);for(let t of o)e.push(` ${t}`);e.push(``)}s&&e.push(` SSR Entry: ${s}`),s&&e.push(``),console.log(e.join(`
|
|
251
|
+
`))}if(u)return{vite:a};if(!i){let e=a;if(!e){let{createServer:t}=await H(`vite`);e=await t({root:n,server:{middlewareMode:!0},appType:`custom`})}let{getRequestListener:i}=await H(`@hono/node-server`),{createServer:o}=await H(`node:http`),s=i(t.fetch);return o((t,n)=>{e.middlewares(t,n,()=>s(t,n))}).listen(r,()=>{d()}),{vite:e}}if(c)globalThis.Deno.serve({port:r},t.fetch);else if(!l){let{serveStatic:e}=await H(`@hono/node-server/serve-static`),i=await H(`node:path`),a=new R,o=i.resolve(n,`dist/client`);a.use(`/*`,e({root:o,rewriteRequestPath:e=>e.endsWith(`/`)?`/__nosuchfile__`:e})),a.route(`/`,t);let{serve:s}=await H(`@hono/node-server`);s({fetch:a.fetch,port:r},()=>{d()})}return{vite:a}}async function vr(e={}){let{root:t,port:n=Number(process.env.PORT)||3e3,setup:r,proxies:i,ssr:a}=e,o=t??process.cwd(),{existsSync:s}=await H(`node:fs`),c=(await H(`node:path`)).resolve(o,`.env`);if(s(c))try{let{config:e}=await H(`dotenv`);e({path:c})}catch(e){console.warn(`[Server] Failed to load .env: ${e.message}`)}let l=Z(),u;if(!l.isProduction&&!l.isVercel){let{createServer:e}=await H(`vite`);u=await e({root:o,server:{middlewareMode:!0},appType:`custom`})}let d=new R;i?.length&&W(d,i),r&&await r(d);let f=X({root:o,vite:u,isProduction:l.isProduction,parentFetch:d.fetch.bind(d),...a});return d.route(`/`,f),await Q({app:d,root:o,port:n,isProduction:l.isProduction,vite:u,runtime:l,ssrEntryPath:a?.ssrEntryPath}),{app:d,vite:u,runtime:l}}function yr(e,t,n){let r=t??[`zh`,`en`],i=n??r[0]??`en`;if(!e||e.length>1024)return i;let a=e.split(`,`);if(a.length>50)return i;let o=a.map(e=>{let[t,n]=e.trim().split(`;q=`),r=n?parseFloat(n):1;return{lang:t.trim().toLowerCase(),q:Number.isFinite(r)&&r>=0&&r<=1?r:0}}).filter(e=>e.q>0).sort((e,t)=>t.q-e.q);for(let{lang:e}of o){let t=e.split(`-`)[0];if(r.includes(t))return t}return i}const $=`virtual:finesoft-front/i18n-loader`,br=`\0${$}`;function xr(e){return typeof e.default==`function`?e.default:typeof e.setup==`function`?e.setup:Object.values(e).find(e=>typeof e==`function`)??null}function Sr(e,t){if(!t)return null;let n=e.split(`?`)[0];if(t[n])return t[n];for(let[e,r]of Object.entries(t))if(e.includes(`*`)){let t=e.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);if(RegExp(`^`+t.replace(/\*/g,`.*`)+`$`).test(n))return r}return null}function Cr(e){return e.replace(/\\/g,`/`)}let wr=!1;function Tr(){wr||(wr=!0,console.log(`\n${(e=>`\x1b[33m${e}\x1b[0m`)(`[finesoft]`)} dev server is exposing source via /src/* and /@fs/* (HMR).\n`+(e=>`\x1b[2m${e}\x1b[0m`)(" Do NOT expose this server to untrusted networks; run `vp build && vp preview` for production.\n")))}async function Er(e,t){let{existsSync:n}=await H(`node:fs`),r=await H(`node:path`),i=r.isAbsolute(t)?t:r.resolve(e,t);if(!n(i))throw Error(`[finesoftFrontViteConfig] i18n.messagesDir not found: ${t}`);let a=Cr(r.relative(e,i));if(!a||a.startsWith(`..`))throw Error(`[finesoftFrontViteConfig] i18n.messagesDir must stay inside the project root: ${t}`);return a.replace(/^\.\/+/,``)}function Dr(e={}){let t=e.ssr?.entry??`src/ssr.ts`,n=process.cwd(),r,i,a,o=/\.(css|scss|less|sass|styl|stylus|pcss|postcss)($|\?)/;return{name:`finesoft-front`,config(t){let n={appType:`custom`,define:{__FINESOFT_I18N_LOADER_SPECIFIER__:e.i18n?.messagesDir?JSON.stringify($):`undefined`}};return process.env.__FINESOFT_SUB_BUILD__||(n.build={outDir:t.build?.outDir??`dist/client`}),n},configResolved(e){r=e.command,i=e.resolve,a=e.css,n=e.root},resolveId(t){return t===$&&e.i18n?.messagesDir?br:null},async load(t){if(t!==br||!e.i18n?.messagesDir)return null;let r=`/${await Er(n,e.i18n.messagesDir)}`,i=`${r}/*.json`,a=`${r}/`;return`
|
|
252
252
|
const localeModules = import.meta.glob(${JSON.stringify(i)}, { import: "default" });
|
|
253
253
|
|
|
254
254
|
export async function loadMessages(locale) {
|
|
@@ -257,8 +257,8 @@ export async function loadMessages(locale) {
|
|
|
257
257
|
const messages = await loader();
|
|
258
258
|
return messages ?? undefined;
|
|
259
259
|
}
|
|
260
|
-
`},transformIndexHtml:{order:`pre`,async handler(e,t){let n=t.server;if(!n)return;let r=(t.originalUrl||t.path||``).split(`?`)[0];if(/\.\w+$/.test(r)&&!r.endsWith(`.html`))return;let i=[...e.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g)].find(e=>!e[1].startsWith(`/@`));if(!i)return;let a=i[1];try{await n.transformRequest(a)}catch{return}let s=[],c=new Set;function l(e,t=0){if(!(t>100)&&!(!e?.url||c.has(e.url))&&(c.add(e.url),o.test(e.url)&&!e.url.includes(`.svelte`)&&s.push(e.url),e.importedModules))for(let n of e.importedModules)l(n,t+1)}let u=await n.moduleGraph.getModuleByUrl(a);if(u&&l(u),s.length===0)return;let d=[];for(let e of s)try{let t=(await n.ssrLoadModule(e))?.default;typeof t==`string`&&t.length>0&&d.push({tag:`style`,attrs:{"data-vite-dev-id":e},children:t,injectTo:`head`})}catch{}return d}},configureServer(r){return async()=>{let{Hono:i}=await
|
|
260
|
+
`},transformIndexHtml:{order:`pre`,async handler(e,t){let n=t.server;if(!n)return;let r=(t.originalUrl||t.path||``).split(`?`)[0];if(/\.\w+$/.test(r)&&!r.endsWith(`.html`))return;let i=[...e.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g)].find(e=>!e[1].startsWith(`/@`));if(!i)return;let a=i[1];try{await n.transformRequest(a)}catch{return}let s=[],c=new Set;function l(e,t=0){if(!(t>100)&&!(!e?.url||c.has(e.url))&&(c.add(e.url),o.test(e.url)&&!e.url.includes(`.svelte`)&&s.push(e.url),e.importedModules))for(let n of e.importedModules)l(n,t+1)}let u=await n.moduleGraph.getModuleByUrl(a);if(u&&l(u),s.length===0)return;let d=[];for(let e of s)try{let t=(await n.ssrLoadModule(e))?.default;typeof t==`string`&&t.length>0&&d.push({tag:`style`,attrs:{"data-vite-dev-id":e},children:t,injectTo:`head`})}catch{}return d}},configureServer(r){return async()=>{let{Hono:i}=await H(`hono`),{getRequestListener:a}=await H(`@hono/node-server`);Tr();let o=new i;if(e.proxies?.length&&W(o,e.proxies),typeof e.setup==`function`)await e.setup(o);else if(typeof e.setup==`string`){let t=xr(await r.ssrLoadModule(`/`+e.setup));t&&await t(o)}let s=X({root:n,vite:r,isProduction:!1,ssrEntryPath:`/`+t,parentFetch:o.fetch.bind(o),renderModes:e.renderModes,defaultLocale:e.defaultLocale});o.route(`/`,s);let c=a(o.fetch);r.middlewares.use((e,t)=>{c(e,t)})}},configurePreviewServer(t){return async()=>{let{readFileSync:r}=await H(`node:fs`),i=await H(`node:path`),{pathToFileURL:a}=await H(`node:url`),{Hono:o}=await H(`hono`),{getRequestListener:s}=await H(`@hono/node-server`),c=new o,l=new D(1e3);if(e.proxies?.length&&W(c,e.proxies),typeof e.setup==`function`)await e.setup(c);else if(typeof e.setup==`string`)try{let e=a(i.resolve(n,`dist/server/setup.mjs`)).href,t=xr(await H(e));t&&await t(c)}catch{console.warn(`[finesoft] Could not load setup module for preview. API routes disabled.`)}let u=r(i.resolve(n,`dist/client/index.html`),`utf-8`),d=a(i.resolve(n,`dist/server/ssr.js`)).href,f=await H(d);c.get(`*`,async t=>{let n=parseInt(t.req.header(`x-ssr-depth`)??`0`,10);if(n>=5)return t.text(`SSR recursion loop detected`,508);let r=t.req.path+(t.req.url.includes(`?`)?`?`+t.req.url.split(`?`)[1]:``);try{let i=Sr(r,e.renderModes);if(i===`csr`)return t.html(V(u,e.defaultLocale?N(e.defaultLocale):void 0));let a=l.get(r);if(a)return t.html(a);let{html:o,head:s,css:d,serverData:p,renderMode:m,redirect:h,slots:g,locale:_,status:v,rewriteUrl:y}=await f.render(r,{fetch:hr(c.fetch.bind(c),n+1)});if(h)return t.redirect(h.url,h.status);if(m===`csr`)return t.html(V(u,_));let b=B({template:u,head:s,css:d,html:o,serializedData:f.serializeServerData(p),slots:g,locale:_});return(m===`prerender`||i===`prerender`)&&!v&&!y&&l.set(r,b),y&&t.header(`Content-Location`,y),v&&v>=400?t.html(b,v):t.html(b)}catch(e){return console.error(`[SSR Preview Error]`,e),t.text(`Internal Server Error`,500)}});let p=s(c.fetch);t.middlewares.use((e,t)=>{p(e,t)})}},async closeBundle(){if(!process.env.__FINESOFT_SUB_BUILD__&&r===`build`){process.env.__FINESOFT_SUB_BUILD__=`1`;try{let r=await H(`vite`),o=await H(`node:fs`),s=await H(`node:path`);if(console.log(`
|
|
261
261
|
Building SSR bundle...
|
|
262
|
-
`),await r.build({root:n,build:{ssr:t,outDir:`dist/server`},ssr:{external
|
|
263
|
-
`),await r.build({root:n,build:{ssr:e.setup,outDir:`dist/server`,emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`setup.mjs`}}},resolve:i})),e.adapter){let c=
|
|
262
|
+
`),await r.build({root:n,build:{ssr:t,outDir:`dist/server`},ssr:{external:rr},resolve:i,css:a}),typeof e.setup==`string`&&(console.log(` Building setup module...
|
|
263
|
+
`),await r.build({root:n,build:{ssr:e.setup,outDir:`dist/server`,emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`setup.mjs`}}},resolve:i})),e.adapter){let c=Y(e.adapter),l=o.readFileSync(s.resolve(n,`dist/client/index.html`),`utf-8`),u={root:n,ssrEntry:t,setupPath:typeof e.setup==`string`?e.setup:void 0,bootstrapEntry:e.bootstrapEntry,templateHtml:l,renderModes:e.renderModes,proxies:e.proxies,locales:e.locales,defaultLocale:e.defaultLocale,resolvedResolve:i,resolvedCss:a,vite:r,fs:o,path:s,generateSSREntry(e){return G(u,e)},buildBundle(e){return K(u,e)},copyStaticAssets(e,t){return q(u,e,t)}};console.log(` Running adapter: ${c.name}...\n`),await c.build(u)}}finally{delete process.env.__FINESOFT_SUB_BUILD__}}}}}export{oe as ACTION_KINDS,x as ActionDispatcher,w as BASE_PAGE_FIELDS,s as BaseController,Oe as BaseLogger,an as CompositeEventRecorder,l as CompositeLogger,nn as CompositeLoggerFactory,gt as ConsoleEventRecorder,$t as ConsoleLogger,ln as ConsoleLoggerFactory,Me as Container,Mt as DEFAULT_NAV_PARAM,M as DEP_KEYS,Ne as FINESOFT_PUBLIC,I as Framework,ht as History,Te as HostGuardError,fn as HttpClient,o as HttpError,Re as IntentDispatcher,et as IntersectionImpressionObserver,D as LruMap,xe as NAVIGATION_NODE_KINDS,lt as NAVIGATION_OP_KINDS,Cn as NAVIGATION_TREE_INTENT_ID,y as NavigationError,ie as PrefetchedIntents,pt as ReportingLogger,Ye as ReportingLoggerFactory,Pt as Router,vt as SESSION_DEFAULT_DEBOUNCE_MS,ae as SESSION_DEFAULT_KEY,zt as SESSION_DEFAULT_VERSION,te as SPLIT_VISIBILITIES,vn as SSR_PLACEHOLDERS,ne as SessionError,je as SimpleTranslator,cn as VoidEventRecorder,Qt as WithFieldsRecorder,pr as autoAdapter,ct as bool,Ct as buildUrl,Fe as classifyHost,C as classifyUrl,ir as cloudflareAdapter,st as collectAllLeaves,Pe as collectLeafKeys,dt as collectVisibleDestinations,Ut as createActiveLeafCodec,ut as createAppHandle,m as createBrowserContext,at as createDomRestore,bt as createFlatStackCodec,$e as createFullStateCodec,Yt as createIslandOrchestrator,Tt as createNavigationBridge,L as createNavigationController,ee as createNavigationScopedState,we as createNavigationSessionAdapter,Wt as createPrefetchedIntentsFromDom,X as createSSRApp,Ln as createSSRNavigationRender,_n as createSSRRender,vr as createServer,d as createServerContext,Ze as createSessionBridge,a as createSessionStore,dn as createUrlSessionAdapter,Vt as createWebStorage,Ft as decodeNavigationTreeParam,Le as decodeSnapshot,Nt as defaultShouldRestore,e as defineNavigation,ot as defineRequestScopedKey,ze as defineRoute,Ge as defineRoutes,De as deny,j as deserializeNavigation,rt as deserializeServerData,Ot as detectPlatform,Z as detectRuntime,Kt as encodeNavigationTreeParam,b as encodeSnapshot,h as englishPlural,Mn as extractNavigationTree,Rt as findNearestStack,kt as findNode,Dr as finesoftFrontViteConfig,tr as generateProxyCode,ge as generateUuid,n as getBaseUrl,N as getLocaleAttributes,ye as getPWADisplayMode,A as getPublicFields,de as getTextDirection,V as injectCSRShell,B as injectSSRContent,Jt as int,f as interpolate,Bt as isCompoundAction,re as isExternalUrlAction,Ue as isFlowAction,g as isLeafNode,Et as isMultiValueSchema,_ as isNone,S as isPublicMarked,me as isRtl,T as isSome,p as isSplitNode,ve as isStackNode,wt as isTabsNode,He as isUrlLocation,O as islandContainerAttributes,i as leaf,Xt as list,Lt as makeDependencies,Be as makeExternalUrlAction,Ke as makeFlowAction,be as makeLocaleInfo,Gt as makeSchema,le as mapEach,se as markPublic,ar as netlifyAdapter,c as next,or as nodeAdapter,jt as num,Ht as oneOf,Ce as optional,yr as parseAcceptLanguage,Ee as pipe,pn as pipeAsync,_t as pop,mt as popTo,Xe as popToRoot,en as push,tn as redirect,It as registerActionHandlers,Je as registerExternalUrlHandler,Dt as registerFlowActionHandler,W as registerProxyRoutes,xt as removeHost,ue as removeQueryParams,pe as removeScheme,Jn as renderIslandsHtml,un as replaceTop,tt as resetFilterCache,nt as resolveActivePath,Y as resolveAdapter,P as resolveConfiguredMessages,qt as resolveIslandsShell,v as resolveLocaleFromUrl,r as resolveMessages,_e as resolvePluralKey,_r as resolveRoot,Ae as rewrite,Se as route,Ve as runAfterLoadGuards,qe as runBeforeLoadGuards,it as runStandard,Ie as safeErrorPage,ce as secureFetch,sn as selectColumn,ke as selectTab,F as serializeNavigation,Zt as serializeNavigationStable,Vn as serializeServerData,k as sessionEntryKey,E as setHtmlLocaleAttributes,u as setVisibility,on as shouldLog,St as split,z as ssrRender,wn as ssrRenderNavigation,We as stableStringify,fe as stack,At as startBrowserApp,Q as startServer,sr as staticAdapter,yt as str,Nn as stripNavigationTree,he as tabs,ft as tryScroll,Qe as uuid,fr as vercelAdapter,rn as visibleSplitColumns,t as withDefault};
|
|
264
264
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -19,6 +19,7 @@ A route definition combines:
|
|
|
19
19
|
- A **path pattern** (`/products/:id`)
|
|
20
20
|
- An **intent id** (logical name for the operation; one intent can have multiple routes)
|
|
21
21
|
- A **controller instance** (where the page data is produced)
|
|
22
|
+
- Optional **param codecs** (`params` / `query`) that validate and type the URL params
|
|
22
23
|
- An optional **render mode** (`ssr` / `csr` / `prerender`)
|
|
23
24
|
- Optional **guards** (`beforeLoad` / `afterLoad`)
|
|
24
25
|
|
|
@@ -68,23 +69,132 @@ export function bootstrap(framework: Framework): void {
|
|
|
68
69
|
|
|
69
70
|
### Route options
|
|
70
71
|
|
|
71
|
-
| Field | Type | Notes
|
|
72
|
-
| ------------ | -------------------------------- |
|
|
73
|
-
| `path` | `string` | Path pattern with `:param` placeholders. Trailing `/` is normalized.
|
|
74
|
-
| `intentId` | `string` | Logical operation name. Used to register the controller.
|
|
75
|
-
| `controller` | `BaseController<TParams, TPage>` | Optional if the intent is already registered.
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
72
|
+
| Field | Type | Notes |
|
|
73
|
+
| ------------ | -------------------------------- | ------------------------------------------------------------------------ |
|
|
74
|
+
| `path` | `string` | Path pattern with `:param` placeholders. Trailing `/` is normalized. |
|
|
75
|
+
| `intentId` | `string` | Logical operation name. Used to register the controller. |
|
|
76
|
+
| `controller` | `BaseController<TParams, TPage>` | Optional if the intent is already registered. |
|
|
77
|
+
| `params` | codec map | Validate/convert **path** params. Keys must appear in `path`. See below. |
|
|
78
|
+
| `query` | codec map | Validate/convert **query** params. Keys are open. See below. |
|
|
79
|
+
| `renderMode` | `"ssr" \| "csr" \| "prerender"` | Default `"ssr"`. See [chapter 4](./04-rendering-and-hydration.md). |
|
|
80
|
+
| `beforeLoad` | `BeforeLoadGuard[]` | Run before the controller. See [chapter 3](./03-middleware.md). |
|
|
81
|
+
| `afterLoad` | `AfterLoadGuard[]` | Run after the page is produced. |
|
|
79
82
|
|
|
80
83
|
### Path patterns
|
|
81
84
|
|
|
82
85
|
- Static: `/about`
|
|
83
86
|
- Parameterized: `/products/:id`, `/users/:userId/posts/:postId`
|
|
84
|
-
-
|
|
85
|
-
- Optional segment is **not** supported — write two routes instead.
|
|
87
|
+
- Optional param: `/blog/:slug?` matches both `/blog` and `/blog/hello`.
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
By default, path and query params arrive in `controller.execute(params, container)` as a **string-keyed object**. Attach codecs (next section) to validate and convert them.
|
|
90
|
+
|
|
91
|
+
## Typed route params
|
|
92
|
+
|
|
93
|
+
Codecs turn raw string params into validated, converted, **compile-time-typed** values. Built-ins cover the common cases with zero dependencies; any [Standard Schema](https://standardschema.dev) (zod, valibot, arktype, …) works too.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { defineRoutes, int, list, oneOf, optional, str, withDefault } from "@finesoft/front";
|
|
97
|
+
|
|
98
|
+
defineRoutes(framework, [
|
|
99
|
+
{
|
|
100
|
+
path: "/products/:id",
|
|
101
|
+
intentId: "product",
|
|
102
|
+
controller: new ProductController(),
|
|
103
|
+
params: { id: int({ min: 1 }) }, // :id validated as a positive integer, converted to number
|
|
104
|
+
query: {
|
|
105
|
+
page: withDefault(int({ min: 1 }), 1), // ?page= → number, defaults to 1 when absent
|
|
106
|
+
sort: optional(oneOf(["asc", "desc"] as const)), // optional "asc" | "desc"
|
|
107
|
+
tags: list(str()), // ?tags=a&tags=b → string[]
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Built-in codecs
|
|
114
|
+
|
|
115
|
+
| Codec | Output | Validates |
|
|
116
|
+
| ----------------------- | ------------- | ----------------------------------------------------------- |
|
|
117
|
+
| `str(opts?)` | `string` | `minLength` / `maxLength` / `pattern` (`RegExp`) |
|
|
118
|
+
| `int(opts?)` | `number` | integer + `min` / `max` |
|
|
119
|
+
| `num(opts?)` | `number` | finite number + `min` / `max` |
|
|
120
|
+
| `bool()` | `boolean` | `"true" \| "1" \| "false" \| "0"` |
|
|
121
|
+
| `oneOf([...] as const)` | literal union | membership |
|
|
122
|
+
| `uuid()` | `string` | UUID v1–v5 |
|
|
123
|
+
| `list(item, opts?)` | `T[]` | multi-value query; each item via `item` + `min`/`max` count |
|
|
124
|
+
|
|
125
|
+
Modifiers wrap a codec (codecs stay plain serializable data — no chained `.optional()`):
|
|
126
|
+
|
|
127
|
+
- `optional(codec)` — missing input → `undefined`; renders the key as **optional** (`page?: T`).
|
|
128
|
+
- `withDefault(codec, fallback)` — missing input → `fallback`; key stays required.
|
|
129
|
+
|
|
130
|
+
### Validation = fall-through to 404
|
|
131
|
+
|
|
132
|
+
A failed codec means the route **doesn't match** — the router continues to the next route, falling through to your existing 404 if nothing else catches it. There is no separate `400` channel. This lets overlapping routes disambiguate by type:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
defineRoutes(framework, [
|
|
136
|
+
{ path: "/item/:id", intentId: "item-by-id", controller, params: { id: int() } },
|
|
137
|
+
{ path: "/item/:slug", intentId: "item-by-slug", controller, params: { slug: str() } },
|
|
138
|
+
]);
|
|
139
|
+
// /item/42 → item-by-id (int matches)
|
|
140
|
+
// /item/hello → item-by-slug (int rejects → falls through to str)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Compile-time param types
|
|
144
|
+
|
|
145
|
+
`InferParams` / `InferQuery` derive the controller's param type straight from the codec objects — no hand-written generics to keep in sync:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import {
|
|
149
|
+
BaseController,
|
|
150
|
+
type InferParams,
|
|
151
|
+
type InferQuery,
|
|
152
|
+
int,
|
|
153
|
+
oneOf,
|
|
154
|
+
optional,
|
|
155
|
+
} from "@finesoft/front";
|
|
156
|
+
|
|
157
|
+
const params = { id: int() };
|
|
158
|
+
const query = { sort: optional(oneOf(["asc", "desc"] as const)) };
|
|
159
|
+
|
|
160
|
+
class ProductController extends BaseController<
|
|
161
|
+
InferParams<typeof params> & InferQuery<typeof query>, // { id: number; sort?: "asc" | "desc" }
|
|
162
|
+
ProductPage
|
|
163
|
+
> {
|
|
164
|
+
readonly intentId = "product";
|
|
165
|
+
execute(params) {
|
|
166
|
+
// params.id: number, params.sort: "asc" | "desc" | undefined
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `route()` — param-key safety
|
|
172
|
+
|
|
173
|
+
The plain array-object form already checks that every `params` key appears in the `path`. The `route(path, def)` helper gives the same check as a standalone, composable entry:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
route("/products/:id", { intentId: "product", params: { id: int() } }); // ✓
|
|
177
|
+
route("/products/:id", { intentId: "product", params: { slug: str() } }); // ✗ compile error: "slug" is not in the path
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `defineRoute()` — auto-typed handlers
|
|
181
|
+
|
|
182
|
+
`defineRoute(path, def)` takes a **handler** function instead of a controller class, and infers its params type from the codecs automatically — no `InferParams` needed. It mirrors `BaseController`'s `try/catch → fallback`:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
defineRoute("/products/:id", {
|
|
186
|
+
intentId: "product",
|
|
187
|
+
params: { id: int() },
|
|
188
|
+
query: { page: withDefault(int(), 1) },
|
|
189
|
+
handler: (params, container) => {
|
|
190
|
+
// params: { id: number; page: number } — inferred from the codecs
|
|
191
|
+
return loadProduct(params.id, params.page);
|
|
192
|
+
},
|
|
193
|
+
fallback: (params, error) => errorPage(error), // optional
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Routes without `params` / `query` behave exactly as before — params stay strings, runtime is unchanged.
|
|
88
198
|
|
|
89
199
|
## Writing a controller
|
|
90
200
|
|
|
@@ -177,19 +287,20 @@ This avoids duplicating controller instances when only the URL surface differs.
|
|
|
177
287
|
|
|
178
288
|
## Inspecting the resolved match
|
|
179
289
|
|
|
180
|
-
For diagnostics or custom routing, call `Router.resolve()` directly:
|
|
290
|
+
For diagnostics or custom routing, call `Router.resolve()` directly. It is **async** (codecs may validate asynchronously), so `await` it:
|
|
181
291
|
|
|
182
292
|
```ts
|
|
183
|
-
const match = framework.router.resolve("/products/42");
|
|
293
|
+
const match = await framework.router.resolve("/products/42");
|
|
184
294
|
// {
|
|
185
|
-
// intent: {
|
|
295
|
+
// intent: { id: "product", params: { id: "42" } },
|
|
186
296
|
// action: { kind: "flow", url: "/products/42" },
|
|
187
297
|
// renderMode: "ssr",
|
|
188
|
-
//
|
|
298
|
+
// beforeGuards: [...],
|
|
299
|
+
// afterGuards: [...],
|
|
189
300
|
// }
|
|
190
301
|
```
|
|
191
302
|
|
|
192
|
-
`router.resolve()`
|
|
303
|
+
`router.resolve()` resolves to `null` for unmatched URLs — handle this in your server-side 404 logic.
|
|
193
304
|
|
|
194
305
|
## Try it
|
|
195
306
|
|
|
@@ -19,6 +19,7 @@ URL ──Router.resolve()──▶ RouteMatch { intent, renderMode, guards }
|
|
|
19
19
|
- **路径模式**(`/products/:id`)
|
|
20
20
|
- **intent id**(操作的逻辑名;一个 intent 可以对应多条路由)
|
|
21
21
|
- **Controller 实例**(产出页面数据)
|
|
22
|
+
- 可选的**参数 codec**(`params` / `query`)—— 校验并类型化 URL 参数
|
|
22
23
|
- 可选的**渲染模式**(`ssr` / `csr` / `prerender`)
|
|
23
24
|
- 可选的**守卫**(`beforeLoad` / `afterLoad`)
|
|
24
25
|
|
|
@@ -73,6 +74,8 @@ export function bootstrap(framework: Framework): void {
|
|
|
73
74
|
| `path` | `string` | 路径模式,含 `:param` 占位。结尾的 `/` 会被归一化。 |
|
|
74
75
|
| `intentId` | `string` | 操作的逻辑名。用于注册 Controller。 |
|
|
75
76
|
| `controller` | `BaseController<TParams, TPage>` | 若 intent 已注册可省略。 |
|
|
77
|
+
| `params` | codec map | 校验/转换 **path** 参数。key 必须出现在 `path` 中。见下文。 |
|
|
78
|
+
| `query` | codec map | 校验/转换 **query** 参数。key 开放。见下文。 |
|
|
76
79
|
| `renderMode` | `"ssr" \| "csr" \| "prerender"` | 默认 `"ssr"`。详见[第 4 章](./04-rendering-and-hydration.md)。 |
|
|
77
80
|
| `beforeLoad` | `BeforeLoadGuard[]` | Controller 之前执行。详见[第 3 章](./03-middleware.md)。 |
|
|
78
81
|
| `afterLoad` | `AfterLoadGuard[]` | Page 产出之后执行。 |
|
|
@@ -81,10 +84,117 @@ export function bootstrap(framework: Framework): void {
|
|
|
81
84
|
|
|
82
85
|
- 静态:`/about`
|
|
83
86
|
- 带参数:`/products/:id`、`/users/:userId/posts/:postId`
|
|
84
|
-
-
|
|
85
|
-
- **不**支持可选段 —— 写两条路由代替。
|
|
87
|
+
- 可选参数:`/blog/:slug?` 同时匹配 `/blog` 和 `/blog/hello`。
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
默认情况下,path 和 query 参数以**字符串键对象**的形式进入 `controller.execute(params, container)`。挂上 codec(下一节)即可校验并转换它们。
|
|
90
|
+
|
|
91
|
+
## 路由参数类型化
|
|
92
|
+
|
|
93
|
+
codec 把原始字符串参数变成经校验、已转换、**编译期类型化**的值。内置原语零依赖覆盖常见场景;任意 [Standard Schema](https://standardschema.dev)(zod、valibot、arktype……)也能用。
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { defineRoutes, int, list, oneOf, optional, str, withDefault } from "@finesoft/front";
|
|
97
|
+
|
|
98
|
+
defineRoutes(framework, [
|
|
99
|
+
{
|
|
100
|
+
path: "/products/:id",
|
|
101
|
+
intentId: "product",
|
|
102
|
+
controller: new ProductController(),
|
|
103
|
+
params: { id: int({ min: 1 }) }, // :id 校验为正整数,转换为 number
|
|
104
|
+
query: {
|
|
105
|
+
page: withDefault(int({ min: 1 }), 1), // ?page= → number,缺失时默认 1
|
|
106
|
+
sort: optional(oneOf(["asc", "desc"] as const)), // 可选的 "asc" | "desc"
|
|
107
|
+
tags: list(str()), // ?tags=a&tags=b → string[]
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 内置 codec
|
|
114
|
+
|
|
115
|
+
| codec | 输出 | 校验 |
|
|
116
|
+
| ----------------------- | ---------- | ------------------------------------------------- |
|
|
117
|
+
| `str(opts?)` | `string` | `minLength` / `maxLength` / `pattern`(`RegExp`) |
|
|
118
|
+
| `int(opts?)` | `number` | 整数 + `min` / `max` |
|
|
119
|
+
| `num(opts?)` | `number` | 有限数 + `min` / `max` |
|
|
120
|
+
| `bool()` | `boolean` | `"true" \| "1" \| "false" \| "0"` |
|
|
121
|
+
| `oneOf([...] as const)` | 字面量联合 | 成员检查 |
|
|
122
|
+
| `uuid()` | `string` | UUID v1–v5 |
|
|
123
|
+
| `list(item, opts?)` | `T[]` | 多值 query;每项过 `item` + `min`/`max` 个数 |
|
|
124
|
+
|
|
125
|
+
修饰器包裹 codec(codec 保持可序列化纯数据,不用链式 `.optional()`):
|
|
126
|
+
|
|
127
|
+
- `optional(codec)` —— 输入缺失 → `undefined`;把该 key 渲染为**可选属性**(`page?: T`)。
|
|
128
|
+
- `withDefault(codec, fallback)` —— 输入缺失 → `fallback`;key 保持必选。
|
|
129
|
+
|
|
130
|
+
### 校验失败 = fall-through 到 404
|
|
131
|
+
|
|
132
|
+
codec 校验失败意味着该路由**不匹配** —— 路由器继续尝试下一条,全不中时落到既有 404。**没有**单独的 `400` 通道。这让重叠路由可以按类型消歧:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
defineRoutes(framework, [
|
|
136
|
+
{ path: "/item/:id", intentId: "item-by-id", controller, params: { id: int() } },
|
|
137
|
+
{ path: "/item/:slug", intentId: "item-by-slug", controller, params: { slug: str() } },
|
|
138
|
+
]);
|
|
139
|
+
// /item/42 → item-by-id (int 匹配)
|
|
140
|
+
// /item/hello → item-by-slug (int 拒绝 → fall-through 到 str)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 编译期参数类型
|
|
144
|
+
|
|
145
|
+
`InferParams` / `InferQuery` 直接从 codec 对象推导 Controller 的参数类型 —— 无需手写、也不必和路由保持同步:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import {
|
|
149
|
+
BaseController,
|
|
150
|
+
type InferParams,
|
|
151
|
+
type InferQuery,
|
|
152
|
+
int,
|
|
153
|
+
oneOf,
|
|
154
|
+
optional,
|
|
155
|
+
} from "@finesoft/front";
|
|
156
|
+
|
|
157
|
+
const params = { id: int() };
|
|
158
|
+
const query = { sort: optional(oneOf(["asc", "desc"] as const)) };
|
|
159
|
+
|
|
160
|
+
class ProductController extends BaseController<
|
|
161
|
+
InferParams<typeof params> & InferQuery<typeof query>, // { id: number; sort?: "asc" | "desc" }
|
|
162
|
+
ProductPage
|
|
163
|
+
> {
|
|
164
|
+
readonly intentId = "product";
|
|
165
|
+
execute(params) {
|
|
166
|
+
// params.id: number, params.sort: "asc" | "desc" | undefined
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `route()` —— 参数 key 安全
|
|
172
|
+
|
|
173
|
+
数组对象形态本身已会检查 `params` 的每个 key 都出现在 `path` 中。`route(path, def)` helper 把同样的检查做成独立、可组合的条目:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
route("/products/:id", { intentId: "product", params: { id: int() } }); // ✓
|
|
177
|
+
route("/products/:id", { intentId: "product", params: { slug: str() } }); // ✗ 编译报错:"slug" 不在 path 中
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `defineRoute()` —— 自动类型化的 handler
|
|
181
|
+
|
|
182
|
+
`defineRoute(path, def)` 接受一个 **handler** 函数而非 Controller 类,并从 codec 自动推导其入参类型 —— 无需 `InferParams`。它复刻 `BaseController` 的 `try/catch → fallback`:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
defineRoute("/products/:id", {
|
|
186
|
+
intentId: "product",
|
|
187
|
+
params: { id: int() },
|
|
188
|
+
query: { page: withDefault(int(), 1) },
|
|
189
|
+
handler: (params, container) => {
|
|
190
|
+
// params: { id: number; page: number } —— 从 codec 推导
|
|
191
|
+
return loadProduct(params.id, params.page);
|
|
192
|
+
},
|
|
193
|
+
fallback: (params, error) => errorPage(error), // 可选
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
没有 `params` / `query` 的路由行为完全不变 —— 参数保持字符串,运行时无变化。
|
|
88
198
|
|
|
89
199
|
## 写一个 Controller
|
|
90
200
|
|
|
@@ -177,19 +287,20 @@ defineRoutes(framework, [
|
|
|
177
287
|
|
|
178
288
|
## 检查解析后的 match
|
|
179
289
|
|
|
180
|
-
诊断或自定义路由时,可以直接调 `Router.resolve()`:
|
|
290
|
+
诊断或自定义路由时,可以直接调 `Router.resolve()`。它是**异步**的(codec 可能异步校验),需 `await`:
|
|
181
291
|
|
|
182
292
|
```ts
|
|
183
|
-
const match = framework.router.resolve("/products/42");
|
|
293
|
+
const match = await framework.router.resolve("/products/42");
|
|
184
294
|
// {
|
|
185
|
-
// intent: {
|
|
295
|
+
// intent: { id: "product", params: { id: "42" } },
|
|
186
296
|
// action: { kind: "flow", url: "/products/42" },
|
|
187
297
|
// renderMode: "ssr",
|
|
188
|
-
//
|
|
298
|
+
// beforeGuards: [...],
|
|
299
|
+
// afterGuards: [...],
|
|
189
300
|
// }
|
|
190
301
|
```
|
|
191
302
|
|
|
192
|
-
|
|
303
|
+
未命中时 resolve 出 `null` —— 在服务端 404 逻辑里处理它。
|
|
193
304
|
|
|
194
305
|
## 实时演示
|
|
195
306
|
|