routerino 2.3.4 → 2.4.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/README.md +17 -2
- package/dist/routerino.js +304 -246
- package/dist/routerino.umd.cjs +1 -1
- package/package.json +2 -1
- package/routerino-forge.js +176 -51
- package/routerino-image.jsx +139 -56
- package/types/routerino.d.ts +0 -1
package/dist/routerino.umd.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(p,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],l):(p=typeof globalThis<"u"?globalThis:p||self,l(p.routerino={},p["react/jsx-runtime"],p.React,p.PropTypes))})(this,function(p,l,$,e){"use strict";const V=[480,800,1200,1920],G="(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px";function K(r,t=""){const i=r.toLowerCase(),n=t.toLowerCase();return!!(i.includes("hero")||i.includes("banner")||n.includes("hero")||n.includes("banner")||n.includes("h-screen")||n.includes("h-full"))}function M(r,t,i=null){const n=r.replace(/\.(jpe?g|png|webp)$/i,""),g=i?`.${i}`:r.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg";return t.map(u=>`${n}-${u}w${g} ${u}w`).join(", ")}function F(r){const{src:t="",alt:i="",priority:n,widths:g=V,sizes:u=G,className:w="",style:q={},loading:E,decoding:v="async",fetchpriority:k,...U}=r||{};if(typeof window<"u"&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"))return l.jsx("img",{src:t,alt:i,loading:E||"lazy",decoding:v,fetchPriority:k,className:w,style:q,...U});const[L,A]=$.useState(null),x=t&&typeof window<"u";$.useEffect(()=>{if(!x)return;const S=new F;S.onload=()=>{A({width:S.naturalWidth,height:S.naturalHeight})},S.src=t},[t,x]);const R=L?g.filter(S=>L.width>=S):g,[c,D]=$.useState(R);$.useEffect(()=>{(async()=>{if(typeof window>"u"){D(R);return}const j=t.replace(/\.(jpe?g|png|webp)$/i,""),o=t.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg",s=[];for(const b of R){const h=`${j}-${b}w${o}`;try{(await fetch(h,{method:"HEAD"})).ok&&s.push(b)}catch{}}s.length===0&&R.length>0&&s.push(Math.min(...R)),D(s)})()},[t,R]);const W=n??K(t,w),C=E||(W?"eager":"lazy"),a=k||(W?"high":void 0),I=M(t,c,"webp"),O=M(t,c);return l.jsxs("picture",{"data-routerino-image":"true","data-original-src":t,children:[l.jsx("source",{srcSet:I,type:"image/webp",sizes:u}),l.jsx("img",{src:t,alt:i,srcSet:O,sizes:u,loading:C,decoding:v,fetchPriority:a,className:w,style:q,...U})]})}F.propTypes={src:e.string.isRequired,alt:e.string.isRequired,priority:e.bool,widths:e.arrayOf(e.number),sizes:e.string,className:e.string,style:e.object,loading:e.oneOf(["lazy","eager"]),decoding:e.oneOf(["sync","async","auto"]),fetchpriority:e.oneOf(["high","low","auto"])};const _=$.createContext(null);function Z(){const r=$.useContext(_);if(!r)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return r}function d({tag:r="meta",soft:t=!1,...i}){const n=Object.keys(i);if(n.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${r} tag`);let g=null;for(let u=0;u<n.length&&(n[u]!=="content"&&(g=document.querySelector(`${r}[${n[u]}='${i[n[u]]}']`)),!g);u++);g&&t||(g||(g=document.createElement(r)),n.forEach(u=>g.setAttribute(u,i[u])),document.querySelector("head").appendChild(g))}function J({routePattern:r,currentRoute:t}){let i={},n=r.split("/"),g=t.split("/");return n.forEach((u,w)=>{u.startsWith(":")&&(i[u.slice(1)]=g[w])}),i}class H extends $.Component{constructor(t){super(t),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(t,i){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",t),console.error("[Routerino] Component Stack:",i.componentStack),this.props.routePath&&console.error("[Routerino] Failed Route:",this.props.routePath),console.error("[Routerino] Error occurred at:",new Date().toISOString()),console.groupEnd()),document.title=this.props.errorTitleString,this.props.usePrerenderTags&&d({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}H.propTypes={children:e.node,fallback:e.node,errorTitleString:e.string.isRequired,usePrerenderTags:e.bool,routePath:e.string,debug:e.bool};function B({routes:r=[{path:"/",element:l.jsx("p",{children:"This is the default route. Pass an array of routes to the Routerino component in order to configure your own pages. Each route is a dictionary with at least `path` and `element` defined."}),title:"Routerino default route example",description:"The default route example description.",tags:[{property:"og:locale",content:"en_US"}]}],notFoundTemplate:t=l.jsxs(l.Fragment,{children:[l.jsx("p",{children:"No page found for this URL. [404]"}),l.jsx("p",{children:l.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:i="Page not found [404]",errorTemplate:n=l.jsxs(l.Fragment,{children:[l.jsx("p",{children:"Page failed to load. [500]"}),l.jsx("p",{children:l.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:g="Page error [500]",useTrailingSlash:u=!0,usePrerenderTags:w=!1,baseUrl:q=null,title:E="",separator:v=" | ",imageUrl:k=null,touchIconUrl:U=null,debug:f=!1}){const L=`${g}${v}${E}`,A=`${i}${v}${E}`;try{if(f){const o=r.map(b=>b.path),s=o.filter((b,h)=>o.indexOf(b)!==h);s.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(s)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[x,R]=$.useState(window?.location?.href??"/");$.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const o=b=>{f&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let h=b.target;for(;h.tagName!=="A"&&h.parentElement;)h=h.parentElement;if(h.tagName!=="A"){f&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const m=h.getAttribute("href")||h.href;if(!m){f&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(m)){f&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",m);return}f&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",m);let y;try{y=new URL(m,window.location.href)}catch(z){f&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",m,z);return}f&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",y,"current:",window.location),y&&window.location.origin===y.origin?(f&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),b.preventDefault(),h.href!==window.location.href&&(R(h.href),window.history.pushState({},"",h.href)),window.scrollTo({top:0,behavior:"auto"})):f&&console.debug("%c[Routerino]%c target link does not share an origin, standard browser link handling applies","color: #6b7280; font-weight: bold","")};document.addEventListener("click",o);const s=()=>{f&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),R(window.location.href)};return window.addEventListener("popstate",s),()=>{document.removeEventListener("click",o),window.removeEventListener("popstate",s)}},[x]);let c=window?.location?.pathname??"/";(c==="/index.html"||c==="")&&(c="/");const D=r.find(o=>o.path===c),W=r.find(o=>`${o.path}/`===c||o.path===`${c}/`),C=r.find(o=>{const s=o.path.endsWith("/")?o.path.slice(0,-1):o.path,b=c.endsWith("/")?c.slice(0,-1):c,h=s.split("/").filter(Boolean),m=b.split("/").filter(Boolean);return h.length!==m.length?!1:h.every((y,z)=>y.startsWith(":")?!0:y===m[z])}),a=D??W??C;if(f&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:a,exactMatch:D,addSlashMatch:W,paramsMatch:C}),!a)return f&&(console.group("%c[Routerino]%c 404 - No matching route","color: #f59e0b; font-weight: bold",""),console.warn("%c[Routerino]%c Requested path:","color: #f59e0b; font-weight: bold","",c),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",r.map(o=>o.path)),console.groupEnd()),document.title=A,w&&d({name:"prerender-status-code",content:"404"}),t;if(w){const o=document.querySelector('meta[name="prerender-status-code"]');o&&o.remove();const s=document.querySelector('meta[name="prerender-header"]');s&&s.remove()}const I=u&&!c.endsWith("/")&&c!=="/",O=!u&&c.endsWith("/")&&c!=="/",S=I?`${c}/`:O?c.slice(0,-1):c,j=`${q??window?.location?.origin??""}${S}`;if(a.title){const o=`${a.title}${v}${E}`;document.title=o,d({tag:"link",rel:"canonical",href:j}),a.tags?.find(({property:s})=>s==="og:title")||d({property:"og:title",content:o}),a.tags?.find(({property:s})=>s==="og:url")||d({property:"og:url",content:j})}if(a.description&&(d({name:"description",content:a.description}),a.tags?.find(({property:o})=>o==="og:description")||d({property:"og:description",content:a.description})),(k||a.imageUrl)&&d({property:"og:image",content:a.imageUrl??k}),a.tags?.find(({property:o})=>o==="twitter:card")||d({name:"twitter:card",content:"summary_large_image"}),U&&d({tag:"link",rel:"apple-touch-icon",href:U}),w&&(I||O)&&(d({name:"prerender-status-code",content:"301"}),d({name:"prerender-header",content:`Location: ${j}`})),a.tags&&a.tags.length?(a.tags.find(({property:o})=>o==="og:type")||d({property:"og:type",content:"website"}),a.tags.forEach(o=>d(o))):d({property:"og:type",content:"website"}),a.element){const o=J({routePattern:a.path,currentRoute:c}),s={currentRoute:c,params:o,routePattern:a.path,updateHeadTag:d};return l.jsx(_.Provider,{value:s,children:l.jsx(H,{fallback:n,errorTitleString:L,usePrerenderTags:w,routePath:c,debug:f,children:a.element})})}return f&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",c),document.title=A,w&&d({name:"prerender-status-code",content:"404"}),t}catch(x){return f&&(console.group("%c[Routerino]%c Fatal Error","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c An error occurred in the router itself (not in a route component)","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c Error:","color: #ff6b6b; font-weight: bold","",x),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),w&&d({name:"prerender-status-code",content:"500"}),document.title=L,n}}const N=e.exact({path:(r,t,i)=>{const n=r[t];return n==null?new Error(`The prop \`${t}\` is marked as required in \`${i}\`, but its value is \`${n}\`.`):typeof n!="string"?new Error(`Invalid prop \`${t}\` of type \`${typeof n}\` supplied to \`${i}\`, expected \`string\`.`):n.startsWith("/")?null:new Error(`Invalid prop \`${t}\` value \`${n}\` supplied to \`${i}\`. Route paths must start with a forward slash (/).`)},element:e.element.isRequired,title:e.string,description:e.string,tags:e.arrayOf(e.object),imageUrl:e.string});B.propTypes={routes:e.arrayOf(N),title:e.string,separator:e.string,notFoundTemplate:e.element,notFoundTitle:e.string,errorTemplate:e.element,errorTitle:e.string,useTrailingSlash:e.bool,usePrerenderTags:e.bool,baseUrl:(r,t,i)=>{const n=r[t];if(n!=null){if(typeof n!="string")return new Error(`Invalid prop \`${t}\` of type \`${typeof n}\` supplied to \`${i}\`, expected \`string\`.`);if(n.endsWith("/"))return new Error(`Invalid prop \`${t}\` supplied to \`${i}\`. The baseUrl should not end with a slash. Got: "${n}"`)}return null},imageUrl:e.string,touchIconUrl:e.string,debug:e.bool},p.ErrorBoundary=H,p.Image=F,p.Routerino=B,p.default=B,p.updateHeadTag=d,p.useRouterino=Z,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
|
1
|
+
(function(w,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],c):(w=typeof globalThis<"u"?globalThis:w||self,c(w.routerino={},w["react/jsx-runtime"],w.React,w.PropTypes))})(this,(function(w,c,b,o){"use strict";const Q=[480,800,1200,1920],X="(max-width: 480px) 100vw, (max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px",A=new Map;function Y(i,e=""){const r=i.toLowerCase(),n=e.toLowerCase();return!!(r.includes("hero")||r.includes("banner")||n.includes("hero")||n.includes("banner")||n.includes("h-screen")||n.includes("h-full"))}function D(i,e,r=null){const n=i.replace(/\.(jpe?g|png|webp)$/i,""),f=r?`.${r}`:i.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg";return e.map(s=>`${n}-${s}w${f} ${s}w`).join(", ")}function Z(i){const{src:e="",alt:r="",priority:n,widths:f=Q,sizes:s=X,className:m="",style:V={},width:R,height:x,loading:B,decoding:C="async",fetchpriority:p,...L}=i||{},q=typeof window>"u",S=!q&&typeof document<"u"&&typeof HTMLElement<"u"&&document.createElement("div")instanceof HTMLElement,y=S&&(window.location.hostname==="localhost"||window.location.hostname==="127.0.0.1"),a=n??Y(e,m),W=B||(a?"eager":"lazy"),I=p||(a?"high":void 0),[E,l]=b.useState(null);b.useEffect(()=>{if(!S||y||!e)return;const g=new window.Image;g.onload=()=>{l({width:g.naturalWidth,height:g.naturalHeight})},g.src=e},[e,S,y]);const k=b.useMemo(()=>E?f.filter(g=>E.width>=g):f,[E,f]),[O,G]=b.useState(null);b.useEffect(()=>{if(!S||y)return;let g=!1;return(async()=>{const U=e.replace(/\.(jpe?g|png|webp)$/i,""),F=e.match(/\.(jpe?g|png|webp)$/i)?.[0]||".jpg",M=[];for(const K of k){const H=`${U}-${K}w${F}`;if(A.has(H)){A.get(H)&&M.push(K);continue}try{const N=await fetch(H,{method:"HEAD"});A.set(H,N.ok),N.ok&&M.push(K)}catch{A.set(H,!1)}}g||G(M.length>0?M:k)})(),()=>{g=!0}},[e,k,S,y]);const j=O??k,t={};R!=null&&(t.width=R),x!=null&&(t.height=x),E&&R==null&&x==null&&(t.width=E.width,t.height=E.height);const u={maxWidth:"100%",height:"auto",...V};if(y)return c.jsx("img",{src:e,alt:r,loading:W,decoding:C,fetchPriority:I,className:m,style:u,...t,...L});if(q)return c.jsxs("picture",{"data-routerino-image":"true","data-original-src":e,children:[c.jsx("source",{srcSet:D(e,f,"webp"),type:"image/webp",sizes:s}),c.jsx("img",{src:e,alt:r,srcSet:D(e,f),sizes:s,loading:W,decoding:"async",fetchPriority:I,className:m,style:u,...t,...L})]});const $=D(e,j,"webp"),h=D(e,j);return c.jsxs("picture",{"data-routerino-image":"true","data-original-src":e,children:[c.jsx("source",{srcSet:$,type:"image/webp",sizes:s}),c.jsx("img",{src:e,alt:r,srcSet:h,sizes:s,loading:W,decoding:C,fetchPriority:I,className:m,style:u,...t,...L})]})}Z.propTypes={src:o.string.isRequired,alt:o.string.isRequired,priority:o.bool,widths:o.arrayOf(o.number),sizes:o.string,className:o.string,style:o.object,width:o.number,height:o.number,loading:o.oneOf(["lazy","eager"]),decoding:o.oneOf(["sync","async","auto"]),fetchpriority:o.oneOf(["high","low","auto"])};const J=b.createContext(null);function T(){const i=b.useContext(J);if(!i)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return i}function d({tag:i="meta",soft:e=!1,...r}){const n=Object.keys(r);if(n.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${i} tag`);let f=null;for(let s=0;s<n.length&&(n[s]!=="content"&&(f=document.querySelector(`${i}[${n[s]}='${r[n[s]]}']`)),!f);s++);f&&e||(f||(f=document.createElement(i)),n.forEach(s=>f.setAttribute(s,r[s])),document.querySelector("head").appendChild(f))}function P({routePattern:i,currentRoute:e}){let r={},n=i.split("/"),f=e.split("/");return n.forEach((s,m)=>{s.startsWith(":")&&(r[s.slice(1)]=f[m])}),r}class _ extends b.Component{constructor(e){super(e),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e,r){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",e),console.error("[Routerino] Component Stack:",r.componentStack),this.props.routePath&&console.error("[Routerino] Failed Route:",this.props.routePath),console.error("[Routerino] Error occurred at:",new Date().toISOString()),console.groupEnd()),document.title=this.props.errorTitleString,this.props.usePrerenderTags&&d({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}_.propTypes={children:o.node,fallback:o.node,errorTitleString:o.string.isRequired,usePrerenderTags:o.bool,routePath:o.string,debug:o.bool};function z({routes:i=[{path:"/",element:c.jsx("p",{children:"This is the default route. Pass an array of routes to the Routerino component in order to configure your own pages. Each route is a dictionary with at least `path` and `element` defined."}),title:"Routerino default route example",description:"The default route example description.",tags:[{property:"og:locale",content:"en_US"}]}],notFoundTemplate:e=c.jsxs(c.Fragment,{children:[c.jsx("p",{children:"No page found for this URL. [404]"}),c.jsx("p",{children:c.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:r="Page not found [404]",errorTemplate:n=c.jsxs(c.Fragment,{children:[c.jsx("p",{children:"Page failed to load. [500]"}),c.jsx("p",{children:c.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:f="Page error [500]",useTrailingSlash:s=!0,usePrerenderTags:m=!1,baseUrl:V=null,title:R="",separator:x=" | ",imageUrl:B=null,touchIconUrl:C=null,debug:p=!1}){const L=`${f}${x}${R}`,q=`${r}${x}${R}`;try{if(p){const t=i.map($=>$.path),u=t.filter(($,h)=>t.indexOf($)!==h);u.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(u)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[S,y]=b.useState(window?.location?.href??"/");b.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const t=$=>{p&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let h=$.target;for(;h.tagName!=="A"&&h.parentElement;)h=h.parentElement;if(h.tagName!=="A"){p&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const g=h.getAttribute("href")||h.href;if(!g){p&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(g)){p&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",g);return}p&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",g);let v;try{v=new URL(g,window.location.href)}catch(U){p&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",g,U);return}if(p&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",v,"current:",window.location),v&&window.location.origin===v.origin)if(p&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),$.preventDefault(),h.href!==window.location.href&&(y(h.href),window.history.pushState({},"",h.href)),h.hash){const U=decodeURIComponent(h.hash.slice(1));setTimeout(()=>{const F=document.getElementById(U);F?F.scrollIntoView({behavior:"auto"}):window.scrollTo({top:0,behavior:"auto"})},0)}else window.scrollTo({top:0,behavior:"auto"});else p&&console.debug("%c[Routerino]%c target link does not share an origin, standard browser link handling applies","color: #6b7280; font-weight: bold","")};document.addEventListener("click",t);const u=()=>{p&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),y(window.location.href)};return window.addEventListener("popstate",u),()=>{document.removeEventListener("click",t),window.removeEventListener("popstate",u)}},[S]);let a=window?.location?.pathname??"/";(a==="/index.html"||a==="")&&(a="/");const W=i.find(t=>t.path===a),I=i.find(t=>`${t.path}/`===a||t.path===`${a}/`),E=i.find(t=>{const u=t.path.endsWith("/")?t.path.slice(0,-1):t.path,$=a.endsWith("/")?a.slice(0,-1):a,h=u.split("/").filter(Boolean),g=$.split("/").filter(Boolean);return h.length!==g.length?!1:h.every((v,U)=>v.startsWith(":")?!0:v===g[U])}),l=W??I??E;if(p&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:l,exactMatch:W,addSlashMatch:I,paramsMatch:E}),!l)return p&&(console.group("%c[Routerino]%c 404 - No matching route","color: #f59e0b; font-weight: bold",""),console.warn("%c[Routerino]%c Requested path:","color: #f59e0b; font-weight: bold","",a),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",i.map(t=>t.path)),console.groupEnd()),document.title=q,m&&d({name:"prerender-status-code",content:"404"}),e;if(m){const t=document.querySelector('meta[name="prerender-status-code"]');t&&t.remove();const u=document.querySelector('meta[name="prerender-header"]');u&&u.remove()}const k=s&&!a.endsWith("/")&&a!=="/",O=!s&&a.endsWith("/")&&a!=="/",G=k?`${a}/`:O?a.slice(0,-1):a,j=`${V??window?.location?.origin??""}${G}`;if(l.title){const t=`${l.title}${x}${R}`;document.title=t,d({tag:"link",rel:"canonical",href:j}),l.tags?.find(({property:u})=>u==="og:title")||d({property:"og:title",content:t}),l.tags?.find(({property:u})=>u==="og:url")||d({property:"og:url",content:j})}if(l.description&&(d({name:"description",content:l.description}),l.tags?.find(({property:t})=>t==="og:description")||d({property:"og:description",content:l.description})),(B||l.imageUrl)&&d({property:"og:image",content:l.imageUrl??B}),l.tags?.find(({property:t})=>t==="twitter:card")||d({name:"twitter:card",content:"summary_large_image"}),C&&d({tag:"link",rel:"apple-touch-icon",href:C}),m&&(k||O)&&(d({name:"prerender-status-code",content:"301"}),d({name:"prerender-header",content:`Location: ${j}`})),l.tags&&l.tags.length?(l.tags.find(({property:t})=>t==="og:type")||d({property:"og:type",content:"website"}),l.tags.forEach(t=>d(t))):d({property:"og:type",content:"website"}),l.element){const t=P({routePattern:l.path,currentRoute:a}),u={currentRoute:a,params:t,routePattern:l.path,updateHeadTag:d};return c.jsx(J.Provider,{value:u,children:c.jsx(_,{fallback:n,errorTitleString:L,usePrerenderTags:m,routePath:a,debug:p,children:l.element})})}return p&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",a),document.title=q,m&&d({name:"prerender-status-code",content:"404"}),e}catch(S){return p&&(console.group("%c[Routerino]%c Fatal Error","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c An error occurred in the router itself (not in a route component)","color: #ff6b6b; font-weight: bold",""),console.error("%c[Routerino]%c Error:","color: #ff6b6b; font-weight: bold","",S),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),m&&d({name:"prerender-status-code",content:"500"}),document.title=L,n}}const ee=o.exact({path:(i,e,r)=>{const n=i[e];return n==null?new Error(`The prop \`${e}\` is marked as required in \`${r}\`, but its value is \`${n}\`.`):typeof n!="string"?new Error(`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${r}\`, expected \`string\`.`):n.startsWith("/")?null:new Error(`Invalid prop \`${e}\` value \`${n}\` supplied to \`${r}\`. Route paths must start with a forward slash (/).`)},element:o.element.isRequired,title:o.string,description:o.string,tags:o.arrayOf(o.object),imageUrl:o.string});z.propTypes={routes:o.arrayOf(ee),title:o.string,separator:o.string,notFoundTemplate:o.element,notFoundTitle:o.string,errorTemplate:o.element,errorTitle:o.string,useTrailingSlash:o.bool,usePrerenderTags:o.bool,baseUrl:(i,e,r)=>{const n=i[e];if(n!=null){if(typeof n!="string")return new Error(`Invalid prop \`${e}\` of type \`${typeof n}\` supplied to \`${r}\`, expected \`string\`.`);if(n.endsWith("/"))return new Error(`Invalid prop \`${e}\` supplied to \`${r}\`. The baseUrl should not end with a slash. Got: "${n}"`)}return null},imageUrl:o.string,touchIconUrl:o.string,debug:o.bool},w.ErrorBoundary=_,w.Image=Z,w.Routerino=z,w.default=z,w.updateHeadTag=d,w.useRouterino=T,Object.defineProperties(w,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routerino",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "A lightweight, SEO-optimized React router for modern web applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"@vitejs/plugin-react": "^5.0.4",
|
|
67
67
|
"eslint": "^9.38.0",
|
|
68
68
|
"eslint-plugin-react": "^7.37.5",
|
|
69
|
+
"eslint-plugin-react-refresh": "^0.4.26",
|
|
69
70
|
"express": "^5.1.0",
|
|
70
71
|
"globals": "^16.4.0",
|
|
71
72
|
"husky": "^9.1.7",
|
package/routerino-forge.js
CHANGED
|
@@ -24,7 +24,7 @@ const shouldProcessImage = (src) =>
|
|
|
24
24
|
// Default configuration for Image component processing
|
|
25
25
|
const DEFAULT_IMAGE_CONFIG = {
|
|
26
26
|
widths: [480, 800, 1200, 1920],
|
|
27
|
-
formats: ["webp"],
|
|
27
|
+
formats: ["webp"],
|
|
28
28
|
placeholderSize: 20,
|
|
29
29
|
blur: 4,
|
|
30
30
|
maxSize: 10485760, // 10MB
|
|
@@ -33,7 +33,7 @@ const DEFAULT_IMAGE_CONFIG = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// Get image dimensions using ffprobe
|
|
36
|
-
function getImageDimensionsWithFfprobe(inputPath) {
|
|
36
|
+
function getImageDimensionsWithFfprobe(inputPath, config) {
|
|
37
37
|
return new Promise((resolve, reject) => {
|
|
38
38
|
const args = [
|
|
39
39
|
"-v",
|
|
@@ -47,7 +47,7 @@ function getImageDimensionsWithFfprobe(inputPath) {
|
|
|
47
47
|
inputPath,
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
-
const ffprobe = spawn(
|
|
50
|
+
const ffprobe = spawn(config.binaryPaths.ffprobe, args);
|
|
51
51
|
|
|
52
52
|
let output = "";
|
|
53
53
|
let errorOutput = "";
|
|
@@ -103,33 +103,68 @@ async function generateResponsiveImages(
|
|
|
103
103
|
inputFileName.match(/\.(jpe?g|png)$/i)?.[0] || ".jpg";
|
|
104
104
|
|
|
105
105
|
// Get dimensions using ffprobe
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
let dimensions = null;
|
|
107
|
+
try {
|
|
108
|
+
dimensions = await getImageDimensionsWithFfprobe(inputPath, config);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (config.verbose) {
|
|
111
|
+
console.warn(
|
|
112
|
+
`[Routerino Image] Failed to get dimensions for ${inputPath}: ${error.message}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
109
116
|
|
|
110
117
|
// Generate placeholder (LQIP)
|
|
111
118
|
const placeholder = await generatePlaceholder(
|
|
112
119
|
inputPath,
|
|
113
|
-
config.placeholderSize
|
|
120
|
+
config.placeholderSize,
|
|
121
|
+
config
|
|
114
122
|
);
|
|
115
123
|
|
|
124
|
+
// Validate dimensions and log issues
|
|
125
|
+
if (dimensions) {
|
|
126
|
+
if (dimensions.width <= 0 || dimensions.height <= 0) {
|
|
127
|
+
if (config.verbose) {
|
|
128
|
+
console.warn(
|
|
129
|
+
`[Routerino Image] Invalid dimensions for ${inputPath}: ${dimensions.width}x${dimensions.height}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
dimensions = null;
|
|
133
|
+
} else if (config.verbose) {
|
|
134
|
+
console.log(
|
|
135
|
+
`[Routerino Image] Detected dimensions for ${inputPath}: ${dimensions.width}x${dimensions.height}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
116
140
|
const results = {
|
|
117
141
|
placeholder,
|
|
118
|
-
width: dimensions?.width
|
|
119
|
-
height: dimensions?.height
|
|
142
|
+
width: dimensions?.width ?? null,
|
|
143
|
+
height: dimensions?.height ?? null,
|
|
120
144
|
variants: {},
|
|
121
145
|
};
|
|
122
146
|
|
|
123
147
|
// Filter widths to only include those applicable to this image
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
// Prevent upscaling by only generating widths that don't exceed the original
|
|
149
|
+
const applicableWidths = dimensions
|
|
150
|
+
? config.widths.filter((width) => width <= dimensions.width)
|
|
151
|
+
: config.widths; // Fallback: generate all widths if dimensions unknown
|
|
152
|
+
|
|
153
|
+
if (dimensions && applicableWidths.length === 0) {
|
|
154
|
+
if (config.verbose) {
|
|
155
|
+
console.warn(
|
|
156
|
+
`[Routerino Image] Original image ${inputPath} (${dimensions.width}px) is smaller than the smallest configured width (${config.widths[0]}px)`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
// Add the original width as a variant to prevent complete failure
|
|
160
|
+
applicableWidths.push(dimensions.width);
|
|
161
|
+
}
|
|
127
162
|
|
|
128
163
|
// Generate responsive variants for applicable widths only
|
|
129
164
|
for (const width of applicableWidths) {
|
|
130
165
|
// Generate WebP version in output directory
|
|
131
166
|
const webpPath = path.join(outputDir, `${base}-${width}w.webp`);
|
|
132
|
-
await generateImageVariant(inputPath, webpPath, width, "webp");
|
|
167
|
+
await generateImageVariant(inputPath, webpPath, width, "webp", config);
|
|
133
168
|
|
|
134
169
|
// Generate original format version in output directory
|
|
135
170
|
const originalPath = path.join(
|
|
@@ -140,7 +175,8 @@ async function generateResponsiveImages(
|
|
|
140
175
|
inputPath,
|
|
141
176
|
originalPath,
|
|
142
177
|
width,
|
|
143
|
-
originalExtension.slice(1)
|
|
178
|
+
originalExtension.slice(1),
|
|
179
|
+
config
|
|
144
180
|
);
|
|
145
181
|
|
|
146
182
|
results.variants[width] = {
|
|
@@ -162,7 +198,13 @@ async function generateResponsiveImages(
|
|
|
162
198
|
/**
|
|
163
199
|
* Generate a single image variant at specified width
|
|
164
200
|
*/
|
|
165
|
-
async function generateImageVariant(
|
|
201
|
+
async function generateImageVariant(
|
|
202
|
+
inputPath,
|
|
203
|
+
outputPath,
|
|
204
|
+
width,
|
|
205
|
+
format,
|
|
206
|
+
config
|
|
207
|
+
) {
|
|
166
208
|
return new Promise((resolve, reject) => {
|
|
167
209
|
// Build ffmpeg command based on format
|
|
168
210
|
const args = ["-i", inputPath, "-vf", `scale=${width}:-2`];
|
|
@@ -175,7 +217,7 @@ async function generateImageVariant(inputPath, outputPath, width, format) {
|
|
|
175
217
|
|
|
176
218
|
args.push("-y", outputPath); // Overwrite existing files
|
|
177
219
|
|
|
178
|
-
const ffmpeg = spawn(
|
|
220
|
+
const ffmpeg = spawn(config.binaryPaths.ffmpeg, args);
|
|
179
221
|
let errorOutput = "";
|
|
180
222
|
|
|
181
223
|
ffmpeg.stderr.on("data", (data) => {
|
|
@@ -200,7 +242,7 @@ async function generateImageVariant(inputPath, outputPath, width, format) {
|
|
|
200
242
|
/**
|
|
201
243
|
* Generate LQIP placeholder using ffmpeg
|
|
202
244
|
*/
|
|
203
|
-
async function generatePlaceholder(inputPath, targetHeight = 20) {
|
|
245
|
+
async function generatePlaceholder(inputPath, targetHeight = 20, config) {
|
|
204
246
|
return new Promise((resolve, reject) => {
|
|
205
247
|
const args = [
|
|
206
248
|
"-i",
|
|
@@ -216,7 +258,7 @@ async function generatePlaceholder(inputPath, targetHeight = 20) {
|
|
|
216
258
|
"pipe:1",
|
|
217
259
|
];
|
|
218
260
|
|
|
219
|
-
const ffmpeg = spawn(
|
|
261
|
+
const ffmpeg = spawn(config.binaryPaths.ffmpeg, args);
|
|
220
262
|
let chunks = [];
|
|
221
263
|
let errorOutput = "";
|
|
222
264
|
|
|
@@ -260,7 +302,7 @@ async function getImageCacheKey(imagePath, config = DEFAULT_IMAGE_CONFIG) {
|
|
|
260
302
|
* Process <Image> components in HTML to add responsive images + LQIP
|
|
261
303
|
* Only processes elements with data-routerino-image="true"
|
|
262
304
|
*/
|
|
263
|
-
async function processRouterInoImages(html, outputDir) {
|
|
305
|
+
async function processRouterInoImages(html, outputDir, config) {
|
|
264
306
|
const stats = {
|
|
265
307
|
processed: 0,
|
|
266
308
|
skipped: 0,
|
|
@@ -269,6 +311,28 @@ async function processRouterInoImages(html, outputDir) {
|
|
|
269
311
|
errors: [],
|
|
270
312
|
};
|
|
271
313
|
|
|
314
|
+
// Lazy binary setup: check if HTML contains Image components and setup binaries if needed
|
|
315
|
+
const hasImagesInHtml = html.includes('data-routerino-image="true"');
|
|
316
|
+
if (hasImagesInHtml && !config.binaryPaths) {
|
|
317
|
+
try {
|
|
318
|
+
const binaryInfo = await ensureBinariesAvailable();
|
|
319
|
+
config.binaryPaths = {
|
|
320
|
+
ffmpeg: binaryInfo.ffmpegPath,
|
|
321
|
+
ffprobe: binaryInfo.ffprobePath,
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
// Warn once and return HTML unmodified — images will still work
|
|
325
|
+
// using their original src, just without responsive variants or LQIP
|
|
326
|
+
if (!config._binaryWarningShown) {
|
|
327
|
+
console.warn(
|
|
328
|
+
`[Routerino Image] ffmpeg/ffprobe not available, skipping image optimization. Install ffmpeg to enable: https://ffmpeg.org/download.html`
|
|
329
|
+
);
|
|
330
|
+
config._binaryWarningShown = true;
|
|
331
|
+
}
|
|
332
|
+
return { html, stats };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
272
336
|
// Ensure cache directory exists
|
|
273
337
|
const cacheDir = path.resolve(DEFAULT_IMAGE_CONFIG.cacheDir);
|
|
274
338
|
await fs.mkdir(cacheDir, { recursive: true }).catch(() => {});
|
|
@@ -304,6 +368,11 @@ async function processRouterInoImages(html, outputDir) {
|
|
|
304
368
|
fileStats.size < DEFAULT_IMAGE_CONFIG.minSize ||
|
|
305
369
|
fileStats.size > DEFAULT_IMAGE_CONFIG.maxSize
|
|
306
370
|
) {
|
|
371
|
+
if (config.verbose) {
|
|
372
|
+
console.warn(
|
|
373
|
+
`[Routerino Image] Skipping ${originalSrc}: size ${fileStats.size} bytes (min: ${DEFAULT_IMAGE_CONFIG.minSize}, max: ${DEFAULT_IMAGE_CONFIG.maxSize})`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
307
376
|
stats.skipped++;
|
|
308
377
|
continue;
|
|
309
378
|
}
|
|
@@ -326,9 +395,17 @@ async function processRouterInoImages(html, outputDir) {
|
|
|
326
395
|
imageData = cached;
|
|
327
396
|
} catch {
|
|
328
397
|
// Generate responsive images + placeholder
|
|
329
|
-
imageData = await generateResponsiveImages(imagePath, outputDir
|
|
398
|
+
imageData = await generateResponsiveImages(imagePath, outputDir, {
|
|
399
|
+
...DEFAULT_IMAGE_CONFIG,
|
|
400
|
+
...config,
|
|
401
|
+
});
|
|
330
402
|
|
|
331
403
|
if (!imageData) {
|
|
404
|
+
if (config.verbose) {
|
|
405
|
+
console.warn(
|
|
406
|
+
`[Routerino Image] Failed to process ${originalSrc}: no image data returned`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
332
409
|
stats.skipped++;
|
|
333
410
|
continue;
|
|
334
411
|
}
|
|
@@ -360,7 +437,13 @@ async function processRouterInoImages(html, outputDir) {
|
|
|
360
437
|
}
|
|
361
438
|
|
|
362
439
|
/**
|
|
363
|
-
* Transform a picture element to include LQIP background and update srcsets
|
|
440
|
+
* Transform a picture element to include LQIP background and update srcsets.
|
|
441
|
+
*
|
|
442
|
+
* The LQIP placeholder is applied as a background-image on the <picture>
|
|
443
|
+
* element itself, with the blurred placeholder visible behind the <img>.
|
|
444
|
+
* No wrapper div or <style> block is injected — only inline styles on the
|
|
445
|
+
* existing <picture> and <img> elements — so the DOM structure matches what
|
|
446
|
+
* the React Image component renders on the client, avoiding hydration mismatches.
|
|
364
447
|
*/
|
|
365
448
|
async function transformPictureWithLQIP(
|
|
366
449
|
pictureHTML,
|
|
@@ -368,31 +451,33 @@ async function transformPictureWithLQIP(
|
|
|
368
451
|
imageData,
|
|
369
452
|
config
|
|
370
453
|
) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
454
|
+
let updatedPicture = pictureHTML;
|
|
455
|
+
|
|
456
|
+
// Build LQIP inline styles for the <picture> element
|
|
457
|
+
const pictureStyles = [
|
|
458
|
+
"display:block",
|
|
459
|
+
`background-image:url('${imageData.placeholder}')`,
|
|
460
|
+
"background-size:cover",
|
|
461
|
+
"background-position:center",
|
|
462
|
+
`filter:blur(${config.blur}px)`,
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
// Add LQIP styles to the <picture> element
|
|
466
|
+
updatedPicture = updatedPicture.replace(
|
|
467
|
+
/(<picture)([^>]*>)/i,
|
|
468
|
+
(match, tag, rest) => {
|
|
469
|
+
if (rest.includes("style=")) {
|
|
470
|
+
// Append to existing style
|
|
471
|
+
return match.replace(
|
|
472
|
+
/style=["']([^"']*)/i,
|
|
473
|
+
`style="${pictureStyles.join(";")};$1`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return `${tag} style="${pictureStyles.join(";")}"${rest}`;
|
|
390
477
|
}
|
|
391
|
-
|
|
478
|
+
);
|
|
392
479
|
|
|
393
480
|
// Update srcsets to point to generated responsive images
|
|
394
|
-
let updatedPicture = pictureHTML;
|
|
395
|
-
|
|
396
481
|
// Update WebP source srcset using actual generated paths
|
|
397
482
|
const webpSrcSet = Object.entries(imageData.variants)
|
|
398
483
|
.map(([width, paths]) => `${paths.webp} ${width}w`)
|
|
@@ -422,21 +507,33 @@ async function transformPictureWithLQIP(
|
|
|
422
507
|
);
|
|
423
508
|
}
|
|
424
509
|
|
|
425
|
-
// Add
|
|
510
|
+
// Add width/height attributes and ensure protective styles on <img>
|
|
426
511
|
updatedPicture = updatedPicture.replace(
|
|
427
512
|
/<img([^>]*?)\/?>/i,
|
|
428
513
|
(match, attributes) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
514
|
+
let newMatch = match;
|
|
515
|
+
|
|
516
|
+
// Add width/height attributes if dimensions are available and not already present
|
|
517
|
+
if (imageData.width && imageData.height) {
|
|
518
|
+
if (!attributes.includes("width=")) {
|
|
519
|
+
newMatch = newMatch.replace(
|
|
520
|
+
"<img",
|
|
521
|
+
`<img width="${imageData.width}"`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (!attributes.includes("height=")) {
|
|
525
|
+
newMatch = newMatch.replace(
|
|
526
|
+
"<img",
|
|
527
|
+
`<img height="${imageData.height}"`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
434
530
|
}
|
|
531
|
+
|
|
532
|
+
return newMatch;
|
|
435
533
|
}
|
|
436
534
|
);
|
|
437
535
|
|
|
438
|
-
|
|
439
|
-
return `<style>${lqipStyle}</style><div class="${uniqueClass}">${updatedPicture}</div>`;
|
|
536
|
+
return updatedPicture;
|
|
440
537
|
}
|
|
441
538
|
|
|
442
539
|
export function routerinoForge(options = {}) {
|
|
@@ -692,6 +789,7 @@ export function render(url, baseUrl) {
|
|
|
692
789
|
console.log("[Routerino Forge] Forging SSG bundle...");
|
|
693
790
|
await build({
|
|
694
791
|
root: viteConfig.root,
|
|
792
|
+
configFile: viteConfig.configFile,
|
|
695
793
|
build: {
|
|
696
794
|
ssr: ssgEntryPath,
|
|
697
795
|
outDir: ssgOutDir,
|
|
@@ -699,6 +797,7 @@ export function render(url, baseUrl) {
|
|
|
699
797
|
output: {
|
|
700
798
|
format: "es",
|
|
701
799
|
entryFileNames: "entry-server.mjs",
|
|
800
|
+
manualChunks: () => null,
|
|
702
801
|
},
|
|
703
802
|
},
|
|
704
803
|
},
|
|
@@ -1281,4 +1380,30 @@ Sitemap: ${config.baseUrl}/sitemap.xml`;
|
|
|
1281
1380
|
}
|
|
1282
1381
|
}
|
|
1283
1382
|
|
|
1383
|
+
// Binary availability check for ffmpeg/ffprobe
|
|
1384
|
+
async function testBinary(binaryPath) {
|
|
1385
|
+
return new Promise((resolve) => {
|
|
1386
|
+
const test = spawn(binaryPath, ["-version"], { stdio: "ignore" });
|
|
1387
|
+
test.on("close", (code) => resolve(code === 0));
|
|
1388
|
+
test.on("error", () => resolve(false));
|
|
1389
|
+
|
|
1390
|
+
// Timeout after 5 seconds
|
|
1391
|
+
setTimeout(() => {
|
|
1392
|
+
test.kill();
|
|
1393
|
+
resolve(false);
|
|
1394
|
+
}, 5000);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async function ensureBinariesAvailable() {
|
|
1399
|
+
if ((await testBinary("ffmpeg")) && (await testBinary("ffprobe"))) {
|
|
1400
|
+
return {
|
|
1401
|
+
ffmpegPath: "ffmpeg",
|
|
1402
|
+
ffprobePath: "ffprobe",
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
throw new Error("ffmpeg/ffprobe not found in PATH");
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1284
1409
|
export default routerinoForge;
|