routerino 2.2.2 → 2.2.3

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/routerino.js CHANGED
@@ -1,9 +1,9 @@
1
- import { jsx as g, jsxs as j, Fragment as M } from "react/jsx-runtime";
2
- import { createContext as J, Component as Q, useContext as V, useState as X, useEffect as Y } from "react";
1
+ import { jsx as g, jsxs as W, Fragment as A } from "react/jsx-runtime";
2
+ import { createContext as O, Component as j, useContext as M, useState as _, useEffect as z } from "react";
3
3
  import t from "prop-types";
4
- const _ = J(null);
5
- function ne() {
6
- const i = V(_);
4
+ const F = O(null);
5
+ function Y() {
6
+ const i = M(F);
7
7
  if (!i)
8
8
  throw new Error(
9
9
  "useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component."
@@ -23,13 +23,13 @@ function l({ tag: i = "meta", soft: c = !1, ...a }) {
23
23
  ;
24
24
  h && c || (h || (h = document.createElement(i)), o.forEach((f) => h.setAttribute(f, a[f])), document.querySelector("head").appendChild(h));
25
25
  }
26
- function Z({ routePattern: i, currentRoute: c }) {
26
+ function G({ routePattern: i, currentRoute: c }) {
27
27
  let a = {}, o = i.split("/"), h = c.split("/");
28
28
  return o.forEach((f, m) => {
29
29
  f.startsWith(":") && (a[f.slice(1)] = h[m]);
30
30
  }), a;
31
31
  }
32
- class z extends Q {
32
+ class H extends j {
33
33
  constructor(c) {
34
34
  super(c), this.state = { hasError: !1 };
35
35
  }
@@ -48,7 +48,7 @@ class z extends Q {
48
48
  return this.state.hasError ? this.props.fallback : this.props.children;
49
49
  }
50
50
  }
51
- z.propTypes = {
51
+ H.propTypes = {
52
52
  /** The child components to render when there's no error */
53
53
  children: t.node,
54
54
  /** The fallback UI to display when an error is caught */
@@ -62,7 +62,7 @@ z.propTypes = {
62
62
  /** Whether to log debug messages to console (optional) */
63
63
  debug: t.bool
64
64
  };
65
- function N({
65
+ function K({
66
66
  routes: i = [
67
67
  {
68
68
  path: "/",
@@ -72,26 +72,25 @@ function N({
72
72
  tags: [{ property: "og:locale", content: "en_US" }]
73
73
  }
74
74
  ],
75
- notFoundTemplate: c = /* @__PURE__ */ j(M, { children: [
75
+ notFoundTemplate: c = /* @__PURE__ */ W(A, { children: [
76
76
  /* @__PURE__ */ g("p", { children: "No page found for this URL. [404]" }),
77
77
  /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
78
78
  ] }),
79
79
  notFoundTitle: a = "Page not found [404]",
80
- errorTemplate: o = /* @__PURE__ */ j(M, { children: [
80
+ errorTemplate: o = /* @__PURE__ */ W(A, { children: [
81
81
  /* @__PURE__ */ g("p", { children: "Page failed to load. [500]" }),
82
82
  /* @__PURE__ */ g("p", { children: /* @__PURE__ */ g("a", { href: "/", children: "Home" }) })
83
83
  ] }),
84
84
  errorTitle: h = "Page error [500]",
85
85
  useTrailingSlash: f = !0,
86
86
  usePrerenderTags: m = !1,
87
- baseUrl: G = null,
87
+ baseUrl: I = null,
88
88
  title: R = "",
89
89
  separator: y = " | ",
90
90
  imageUrl: v = null,
91
91
  touchIconUrl: T = null,
92
92
  debug: s = !1
93
93
  }) {
94
- var P, U, C, L, q, B, W;
95
94
  const k = `${h}${y}${R}`, x = `${a}${y}${R}`;
96
95
  try {
97
96
  if (s) {
@@ -109,10 +108,8 @@ function N({
109
108
  ""
110
109
  ));
111
110
  }
112
- const [$, A] = X(((P = window == null ? void 0 : window.location) == null ? void 0 : P.href) ?? "/");
113
- Y(() => {
114
- if (typeof window > "u" || typeof document > "u")
115
- return;
111
+ const [$, P] = _(window.location.href);
112
+ z(() => {
116
113
  const e = (w) => {
117
114
  s && console.debug(
118
115
  "%c[Routerino]%c click occurred",
@@ -178,7 +175,7 @@ function N({
178
175
  "%c[Routerino]%c target link is same origin, will use push-state transitioning",
179
176
  "color: #6b7280; font-weight: bold",
180
177
  ""
181
- ), w.preventDefault(), d.href !== window.location.href && (A(d.href), window.history.pushState({}, "", d.href)), window.scrollTo({
178
+ ), w.preventDefault(), d.href !== window.location.href && (P(d.href), window.history.pushState({}, "", d.href)), window.scrollTo({
182
179
  top: 0,
183
180
  behavior: "auto"
184
181
  })) : s && console.debug(
@@ -194,25 +191,25 @@ function N({
194
191
  "color: #6b7280; font-weight: bold",
195
192
  "",
196
193
  window.location.pathname
197
- ), A(window.location.href);
194
+ ), P(window.location.href);
198
195
  };
199
196
  return window.addEventListener("popstate", u), () => {
200
197
  document.removeEventListener("click", e), window.removeEventListener("popstate", u);
201
198
  };
202
199
  }, [$]);
203
- let r = ((U = window == null ? void 0 : window.location) == null ? void 0 : U.pathname) ?? "/";
200
+ let r = window.location?.pathname ?? "/";
204
201
  (r === "/index.html" || r === "") && (r = "/");
205
- const F = i.find((e) => e.path === r), H = i.find(
202
+ const U = i.find((e) => e.path === r), C = i.find(
206
203
  (e) => `${e.path}/` === r || e.path === `${r}/`
207
- ), I = i.find((e) => {
204
+ ), L = i.find((e) => {
208
205
  const u = e.path.endsWith("/") ? e.path.slice(0, -1) : e.path, w = r.endsWith("/") ? r.slice(0, -1) : r, d = u.split("/").filter(Boolean), p = w.split("/").filter(Boolean);
209
206
  return d.length !== p.length ? !1 : d.every((b, S) => b.startsWith(":") ? !0 : b === p[S]);
210
- }), n = F ?? H ?? I;
207
+ }), n = U ?? C ?? L;
211
208
  if (s && console.debug(
212
209
  "%c[Routerino]%c Route matching:",
213
210
  "color: #6b7280; font-weight: bold",
214
211
  "",
215
- { match: n, exactMatch: F, addSlashMatch: H, paramsMatch: I }
212
+ { match: n, exactMatch: U, addSlashMatch: C, paramsMatch: L }
216
213
  ), !n)
217
214
  return s && (console.group(
218
215
  "%c[Routerino]%c 404 - No matching route",
@@ -239,39 +236,39 @@ function N({
239
236
  );
240
237
  u && u.remove();
241
238
  }
242
- const D = f && !r.endsWith("/") && r !== "/", O = !f && r.endsWith("/") && r !== "/", K = D ? `${r}/` : O ? r.slice(0, -1) : r, E = `${G ?? ((C = window == null ? void 0 : window.location) == null ? void 0 : C.origin) ?? ""}${K}`;
239
+ const q = f && !r.endsWith("/") && r !== "/", B = !f && r.endsWith("/") && r !== "/", D = q ? `${r}/` : B ? r.slice(0, -1) : r, E = `${I || window.location.origin}${D}`;
243
240
  if (n.title) {
244
241
  const e = `${n.title}${y}${R}`;
245
242
  document.title = e, l({
246
243
  tag: "link",
247
244
  rel: "canonical",
248
245
  href: E
249
- }), (L = n.tags) != null && L.find(({ property: u }) => u === "og:title") || l({
246
+ }), n.tags?.find(({ property: u }) => u === "og:title") || l({
250
247
  property: "og:title",
251
248
  content: e
252
- }), (q = n.tags) != null && q.find(({ property: u }) => u === "og:url") || l({
249
+ }), n.tags?.find(({ property: u }) => u === "og:url") || l({
253
250
  property: "og:url",
254
251
  content: E
255
252
  });
256
253
  }
257
- if (n.description && (l({ name: "description", content: n.description }), (B = n.tags) != null && B.find(({ property: e }) => e === "og:description") || l({
254
+ if (n.description && (l({ name: "description", content: n.description }), n.tags?.find(({ property: e }) => e === "og:description") || l({
258
255
  property: "og:description",
259
256
  content: n.description
260
257
  })), (v || n.imageUrl) && l({
261
258
  property: "og:image",
262
259
  content: n.imageUrl ?? v
263
- }), (W = n.tags) != null && W.find(({ property: e }) => e === "twitter:card") || l({
260
+ }), n.tags?.find(({ property: e }) => e === "twitter:card") || l({
264
261
  name: "twitter:card",
265
262
  content: "summary_large_image"
266
263
  }), T && l({
267
264
  tag: "link",
268
265
  rel: "apple-touch-icon",
269
266
  href: T
270
- }), m && (D || O) && (l({ name: "prerender-status-code", content: "301" }), l({
267
+ }), m && (q || B) && (l({ name: "prerender-status-code", content: "301" }), l({
271
268
  name: "prerender-header",
272
269
  content: `Location: ${E}`
273
270
  })), n.tags && n.tags.length ? (n.tags.find(({ property: e }) => e === "og:type") || l({ property: "og:type", content: "website" }), n.tags.forEach((e) => l(e))) : l({ property: "og:type", content: "website" }), n.element) {
274
- const e = Z({
271
+ const e = G({
275
272
  routePattern: n.path,
276
273
  currentRoute: r
277
274
  }), u = {
@@ -280,8 +277,8 @@ function N({
280
277
  routePattern: n.path,
281
278
  updateHeadTag: l
282
279
  };
283
- return /* @__PURE__ */ g(_.Provider, { value: u, children: /* @__PURE__ */ g(
284
- z,
280
+ return /* @__PURE__ */ g(F.Provider, { value: u, children: /* @__PURE__ */ g(
281
+ H,
285
282
  {
286
283
  fallback: o,
287
284
  errorTitleString: k,
@@ -319,7 +316,7 @@ function N({
319
316
  ), console.groupEnd()), m && l({ name: "prerender-status-code", content: "500" }), document.title = k, o;
320
317
  }
321
318
  }
322
- const ee = t.exact({
319
+ const J = t.exact({
323
320
  path: (i, c, a) => {
324
321
  const o = i[c];
325
322
  return o == null ? new Error(
@@ -336,8 +333,8 @@ const ee = t.exact({
336
333
  tags: t.arrayOf(t.object),
337
334
  imageUrl: t.string
338
335
  });
339
- N.propTypes = {
340
- routes: t.arrayOf(ee),
336
+ K.propTypes = {
337
+ routes: t.arrayOf(J),
341
338
  title: t.string,
342
339
  separator: t.string,
343
340
  notFoundTemplate: t.element,
@@ -365,9 +362,9 @@ N.propTypes = {
365
362
  debug: t.bool
366
363
  };
367
364
  export {
368
- z as ErrorBoundary,
369
- N as Routerino,
370
- N as default,
365
+ H as ErrorBoundary,
366
+ K as Routerino,
367
+ K as default,
371
368
  l as updateHeadTag,
372
- ne as useRouterino
369
+ Y as useRouterino
373
370
  };
@@ -1 +1 @@
1
- (function(g,a){typeof exports=="object"&&typeof module<"u"?a(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],a):(g=typeof globalThis<"u"?globalThis:g||self,a(g.routerino={},g["react/jsx-runtime"],g.React,g.PropTypes))})(this,function(g,a,$,t){"use strict";const q=$.createContext(null);function J(){const c=$.useContext(q);if(!c)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return c}function i({tag:c="meta",soft:l=!1,...s}){const o=Object.keys(s);if(o.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${c} tag`);let p=null;for(let h=0;h<o.length&&(o[h]!=="content"&&(p=document.querySelector(`${c}[${o[h]}='${s[o[h]]}']`)),!p);h++);p&&l||(p||(p=document.createElement(c)),o.forEach(h=>p.setAttribute(h,s[h])),document.querySelector("head").appendChild(p))}function Q({routePattern:c,currentRoute:l}){let s={},o=c.split("/"),p=l.split("/");return o.forEach((h,b)=>{h.startsWith(":")&&(s[h.slice(1)]=p[b])}),s}class E extends $.Component{constructor(l){super(l),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(l,s){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",l),console.error("[Routerino] Component Stack:",s.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&&i({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}E.propTypes={children:t.node,fallback:t.node,errorTitleString:t.string.isRequired,usePrerenderTags:t.bool,routePath:t.string,debug:t.bool};function S({routes:c=[{path:"/",element:a.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:l=a.jsxs(a.Fragment,{children:[a.jsx("p",{children:"No page found for this URL. [404]"}),a.jsx("p",{children:a.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:s="Page not found [404]",errorTemplate:o=a.jsxs(a.Fragment,{children:[a.jsx("p",{children:"Page failed to load. [500]"}),a.jsx("p",{children:a.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:p="Page error [500]",useTrailingSlash:h=!0,usePrerenderTags:b=!1,baseUrl:X=null,title:v="",separator:y=" | ",imageUrl:C=null,touchIconUrl:B=null,debug:u=!1}){var F,H,j,A,I,M,O;const L=`${p}${y}${v}`,W=`${s}${y}${v}`;try{if(u){const e=c.map(m=>m.path),d=e.filter((m,f)=>e.indexOf(m)!==f);d.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(d)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[k,D]=$.useState(((F=window==null?void 0:window.location)==null?void 0:F.href)??"/");$.useEffect(()=>{if(typeof window>"u"||typeof document>"u")return;const e=m=>{u&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let f=m.target;for(;f.tagName!=="A"&&f.parentElement;)f=f.parentElement;if(f.tagName!=="A"){u&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const w=f.getAttribute("href")||f.href;if(!w){u&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(w)){u&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",w);return}u&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",w);let R;try{R=new URL(w,window.location.href)}catch(U){u&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",w,U);return}u&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",R,"current:",window.location),R&&window.location.origin===R.origin?(u&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),m.preventDefault(),f.href!==window.location.href&&(D(f.href),window.history.pushState({},"",f.href)),window.scrollTo({top:0,behavior:"auto"})):u&&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",e);const d=()=>{u&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),D(window.location.href)};return window.addEventListener("popstate",d),()=>{document.removeEventListener("click",e),window.removeEventListener("popstate",d)}},[k]);let r=((H=window==null?void 0:window.location)==null?void 0:H.pathname)??"/";(r==="/index.html"||r==="")&&(r="/");const _=c.find(e=>e.path===r),z=c.find(e=>`${e.path}/`===r||e.path===`${r}/`),T=c.find(e=>{const d=e.path.endsWith("/")?e.path.slice(0,-1):e.path,m=r.endsWith("/")?r.slice(0,-1):r,f=d.split("/").filter(Boolean),w=m.split("/").filter(Boolean);return f.length!==w.length?!1:f.every((R,U)=>R.startsWith(":")?!0:R===w[U])}),n=_??z??T;if(u&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:n,exactMatch:_,addSlashMatch:z,paramsMatch:T}),!n)return u&&(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","",r),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",c.map(e=>e.path)),console.groupEnd()),document.title=W,b&&i({name:"prerender-status-code",content:"404"}),l;if(b){const e=document.querySelector('meta[name="prerender-status-code"]');e&&e.remove();const d=document.querySelector('meta[name="prerender-header"]');d&&d.remove()}const G=h&&!r.endsWith("/")&&r!=="/",K=!h&&r.endsWith("/")&&r!=="/",Y=G?`${r}/`:K?r.slice(0,-1):r,x=`${X??((j=window==null?void 0:window.location)==null?void 0:j.origin)??""}${Y}`;if(n.title){const e=`${n.title}${y}${v}`;document.title=e,i({tag:"link",rel:"canonical",href:x}),(A=n.tags)!=null&&A.find(({property:d})=>d==="og:title")||i({property:"og:title",content:e}),(I=n.tags)!=null&&I.find(({property:d})=>d==="og:url")||i({property:"og:url",content:x})}if(n.description&&(i({name:"description",content:n.description}),(M=n.tags)!=null&&M.find(({property:e})=>e==="og:description")||i({property:"og:description",content:n.description})),(C||n.imageUrl)&&i({property:"og:image",content:n.imageUrl??C}),(O=n.tags)!=null&&O.find(({property:e})=>e==="twitter:card")||i({name:"twitter:card",content:"summary_large_image"}),B&&i({tag:"link",rel:"apple-touch-icon",href:B}),b&&(G||K)&&(i({name:"prerender-status-code",content:"301"}),i({name:"prerender-header",content:`Location: ${x}`})),n.tags&&n.tags.length?(n.tags.find(({property:e})=>e==="og:type")||i({property:"og:type",content:"website"}),n.tags.forEach(e=>i(e))):i({property:"og:type",content:"website"}),n.element){const e=Q({routePattern:n.path,currentRoute:r}),d={currentRoute:r,params:e,routePattern:n.path,updateHeadTag:i};return a.jsx(q.Provider,{value:d,children:a.jsx(E,{fallback:o,errorTitleString:L,usePrerenderTags:b,routePath:r,debug:u,children:n.element})})}return u&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",r),document.title=W,b&&i({name:"prerender-status-code",content:"404"}),l}catch(k){return u&&(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","",k),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),b&&i({name:"prerender-status-code",content:"500"}),document.title=L,o}}const V=t.exact({path:(c,l,s)=>{const o=c[l];return o==null?new Error(`The prop \`${l}\` is marked as required in \`${s}\`, but its value is \`${o}\`.`):typeof o!="string"?new Error(`Invalid prop \`${l}\` of type \`${typeof o}\` supplied to \`${s}\`, expected \`string\`.`):o.startsWith("/")?null:new Error(`Invalid prop \`${l}\` value \`${o}\` supplied to \`${s}\`. Route paths must start with a forward slash (/).`)},element:t.element.isRequired,title:t.string,description:t.string,tags:t.arrayOf(t.object),imageUrl:t.string});S.propTypes={routes:t.arrayOf(V),title:t.string,separator:t.string,notFoundTemplate:t.element,notFoundTitle:t.string,errorTemplate:t.element,errorTitle:t.string,useTrailingSlash:t.bool,usePrerenderTags:t.bool,baseUrl:(c,l,s)=>{const o=c[l];if(o!=null){if(typeof o!="string")return new Error(`Invalid prop \`${l}\` of type \`${typeof o}\` supplied to \`${s}\`, expected \`string\`.`);if(o.endsWith("/"))return new Error(`Invalid prop \`${l}\` supplied to \`${s}\`. The baseUrl should not end with a slash. Got: "${o}"`)}return null},imageUrl:t.string,touchIconUrl:t.string,debug:t.bool},g.ErrorBoundary=E,g.Routerino=S,g.default=S,g.updateHeadTag=i,g.useRouterino=J,Object.defineProperties(g,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ (function(g,a){typeof exports=="object"&&typeof module<"u"?a(exports,require("react/jsx-runtime"),require("react"),require("prop-types")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","prop-types"],a):(g=typeof globalThis<"u"?globalThis:g||self,a(g.routerino={},g["react/jsx-runtime"],g.React,g.PropTypes))})(this,function(g,a,$,t){"use strict";const q=$.createContext(null);function O(){const c=$.useContext(q);if(!c)throw new Error("useRouterino must be used within a Routerino router. Make sure your component is rendered inside a <Routerino> component.");return c}function i({tag:c="meta",soft:l=!1,...s}){const o=Object.keys(s);if(o.length<1)return console.error(`[Routerino] updateHeadTag() received no attributes to set for ${c} tag`);let p=null;for(let h=0;h<o.length&&(o[h]!=="content"&&(p=document.querySelector(`${c}[${o[h]}='${s[o[h]]}']`)),!p);h++);p&&l||(p||(p=document.createElement(c)),o.forEach(h=>p.setAttribute(h,s[h])),document.querySelector("head").appendChild(p))}function D({routePattern:c,currentRoute:l}){let s={},o=c.split("/"),p=l.split("/");return o.forEach((h,b)=>{h.startsWith(":")&&(s[h.slice(1)]=p[b])}),s}class E extends $.Component{constructor(l){super(l),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(l,s){this.props.debug&&(console.group("%c[Routerino]%c Error Boundary Caught an Error","color: #ff6b6b; font-weight: bold","",l),console.error("[Routerino] Component Stack:",s.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&&i({name:"prerender-status-code",content:"500"})}render(){return this.state.hasError?this.props.fallback:this.props.children}}E.propTypes={children:t.node,fallback:t.node,errorTitleString:t.string.isRequired,usePrerenderTags:t.bool,routePath:t.string,debug:t.bool};function S({routes:c=[{path:"/",element:a.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:l=a.jsxs(a.Fragment,{children:[a.jsx("p",{children:"No page found for this URL. [404]"}),a.jsx("p",{children:a.jsx("a",{href:"/",children:"Home"})})]}),notFoundTitle:s="Page not found [404]",errorTemplate:o=a.jsxs(a.Fragment,{children:[a.jsx("p",{children:"Page failed to load. [500]"}),a.jsx("p",{children:a.jsx("a",{href:"/",children:"Home"})})]}),errorTitle:p="Page error [500]",useTrailingSlash:h=!0,usePrerenderTags:b=!1,baseUrl:z=null,title:v="",separator:y=" | ",imageUrl:C=null,touchIconUrl:B=null,debug:u=!1}){const L=`${p}${y}${v}`,W=`${s}${y}${v}`;try{if(u){const e=c.map(m=>m.path),d=e.filter((m,f)=>e.indexOf(m)!==f);d.length>0&&(console.warn("%c[Routerino]%c Duplicate route paths detected:","color: #f59e0b; font-weight: bold","",[...new Set(d)]),console.warn("%c[Routerino]%c The first matching route will be used","color: #f59e0b; font-weight: bold",""))}const[k,F]=$.useState(window.location.href);$.useEffect(()=>{const e=m=>{u&&console.debug("%c[Routerino]%c click occurred","color: #6b7280; font-weight: bold","");let f=m.target;for(;f.tagName!=="A"&&f.parentElement;)f=f.parentElement;if(f.tagName!=="A"){u&&console.debug("%c[Routerino]%c no anchor tag found during click","color: #6b7280; font-weight: bold","");return}const w=f.getAttribute("href")||f.href;if(!w){u&&console.debug("%c[Routerino]%c anchor tag has no href","color: #6b7280; font-weight: bold","");return}if(!/^(https?:\/\/|\/|\.\/|\.\.\/|[^:]+$)/i.test(w)){u&&console.debug("%c[Routerino]%c skipping non-http URL:","color: #6b7280; font-weight: bold","",w);return}u&&console.debug("%c[Routerino]%c click target href:","color: #6b7280; font-weight: bold","",w);let R;try{R=new URL(w,window.location.href)}catch(U){u&&console.debug("%c[Routerino]%c Invalid URL:","color: #6b7280; font-weight: bold","",w,U);return}u&&console.debug("%c[Routerino]%c targetUrl:","color: #6b7280; font-weight: bold","",R,"current:",window.location),R&&window.location.origin===R.origin?(u&&console.debug("%c[Routerino]%c target link is same origin, will use push-state transitioning","color: #6b7280; font-weight: bold",""),m.preventDefault(),f.href!==window.location.href&&(F(f.href),window.history.pushState({},"",f.href)),window.scrollTo({top:0,behavior:"auto"})):u&&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",e);const d=()=>{u&&console.debug("%c[Routerino]%c route change ->","color: #6b7280; font-weight: bold","",window.location.pathname),F(window.location.href)};return window.addEventListener("popstate",d),()=>{document.removeEventListener("click",e),window.removeEventListener("popstate",d)}},[k]);let r=window.location?.pathname??"/";(r==="/index.html"||r==="")&&(r="/");const H=c.find(e=>e.path===r),j=c.find(e=>`${e.path}/`===r||e.path===`${r}/`),A=c.find(e=>{const d=e.path.endsWith("/")?e.path.slice(0,-1):e.path,m=r.endsWith("/")?r.slice(0,-1):r,f=d.split("/").filter(Boolean),w=m.split("/").filter(Boolean);return f.length!==w.length?!1:f.every((R,U)=>R.startsWith(":")?!0:R===w[U])}),n=H??j??A;if(u&&console.debug("%c[Routerino]%c Route matching:","color: #6b7280; font-weight: bold","",{match:n,exactMatch:H,addSlashMatch:j,paramsMatch:A}),!n)return u&&(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","",r),console.warn("%c[Routerino]%c Available routes:","color: #f59e0b; font-weight: bold","",c.map(e=>e.path)),console.groupEnd()),document.title=W,b&&i({name:"prerender-status-code",content:"404"}),l;if(b){const e=document.querySelector('meta[name="prerender-status-code"]');e&&e.remove();const d=document.querySelector('meta[name="prerender-header"]');d&&d.remove()}const I=h&&!r.endsWith("/")&&r!=="/",M=!h&&r.endsWith("/")&&r!=="/",T=I?`${r}/`:M?r.slice(0,-1):r,x=`${z||window.location.origin}${T}`;if(n.title){const e=`${n.title}${y}${v}`;document.title=e,i({tag:"link",rel:"canonical",href:x}),n.tags?.find(({property:d})=>d==="og:title")||i({property:"og:title",content:e}),n.tags?.find(({property:d})=>d==="og:url")||i({property:"og:url",content:x})}if(n.description&&(i({name:"description",content:n.description}),n.tags?.find(({property:e})=>e==="og:description")||i({property:"og:description",content:n.description})),(C||n.imageUrl)&&i({property:"og:image",content:n.imageUrl??C}),n.tags?.find(({property:e})=>e==="twitter:card")||i({name:"twitter:card",content:"summary_large_image"}),B&&i({tag:"link",rel:"apple-touch-icon",href:B}),b&&(I||M)&&(i({name:"prerender-status-code",content:"301"}),i({name:"prerender-header",content:`Location: ${x}`})),n.tags&&n.tags.length?(n.tags.find(({property:e})=>e==="og:type")||i({property:"og:type",content:"website"}),n.tags.forEach(e=>i(e))):i({property:"og:type",content:"website"}),n.element){const e=D({routePattern:n.path,currentRoute:r}),d={currentRoute:r,params:e,routePattern:n.path,updateHeadTag:i};return a.jsx(q.Provider,{value:d,children:a.jsx(E,{fallback:o,errorTitleString:L,usePrerenderTags:b,routePath:r,debug:u,children:n.element})})}return u&&console.error("%c[Routerino]%c No route found for","color: #ff6b6b; font-weight: bold","",r),document.title=W,b&&i({name:"prerender-status-code",content:"404"}),l}catch(k){return u&&(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","",k),console.error("%c[Routerino]%c This typically means an issue with route configuration or router setup","color: #ff6b6b; font-weight: bold",""),console.groupEnd()),b&&i({name:"prerender-status-code",content:"500"}),document.title=L,o}}const _=t.exact({path:(c,l,s)=>{const o=c[l];return o==null?new Error(`The prop \`${l}\` is marked as required in \`${s}\`, but its value is \`${o}\`.`):typeof o!="string"?new Error(`Invalid prop \`${l}\` of type \`${typeof o}\` supplied to \`${s}\`, expected \`string\`.`):o.startsWith("/")?null:new Error(`Invalid prop \`${l}\` value \`${o}\` supplied to \`${s}\`. Route paths must start with a forward slash (/).`)},element:t.element.isRequired,title:t.string,description:t.string,tags:t.arrayOf(t.object),imageUrl:t.string});S.propTypes={routes:t.arrayOf(_),title:t.string,separator:t.string,notFoundTemplate:t.element,notFoundTitle:t.string,errorTemplate:t.element,errorTitle:t.string,useTrailingSlash:t.bool,usePrerenderTags:t.bool,baseUrl:(c,l,s)=>{const o=c[l];if(o!=null){if(typeof o!="string")return new Error(`Invalid prop \`${l}\` of type \`${typeof o}\` supplied to \`${s}\`, expected \`string\`.`);if(o.endsWith("/"))return new Error(`Invalid prop \`${l}\` supplied to \`${s}\`. The baseUrl should not end with a slash. Got: "${o}"`)}return null},imageUrl:t.string,touchIconUrl:t.string,debug:t.bool},g.ErrorBoundary=E,g.Routerino=S,g.default=S,g.updateHeadTag=i,g.useRouterino=O,Object.defineProperties(g,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routerino",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "A lightweight, SEO-optimized React router for modern web applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,24 +55,24 @@
55
55
  "prepare": "husky"
56
56
  },
57
57
  "devDependencies": {
58
- "@eslint/js": "^9.27.0",
58
+ "@eslint/js": "^9.38.0",
59
59
  "@testing-library/react": "^16.3.0",
60
60
  "@testing-library/user-event": "^14.6.1",
61
- "@vitejs/plugin-react": "^4.5.0",
62
- "eslint": "^9.27.0",
61
+ "@vitejs/plugin-react": "^5.0.4",
62
+ "eslint": "^9.38.0",
63
63
  "eslint-plugin-react": "^7.37.5",
64
- "express": "^4.18.2",
65
- "globals": "^16.1.0",
64
+ "express": "^5.1.0",
65
+ "globals": "^16.4.0",
66
66
  "husky": "^9.1.7",
67
67
  "jsdom": "^26.1.0",
68
- "lint-staged": "^16.1.2",
68
+ "lint-staged": "^16.2.6",
69
69
  "node-fetch": "^3.3.2",
70
70
  "prettier": "^3.6.2",
71
71
  "prop-types": "^15.8.1",
72
- "react": "^19.1.0",
73
- "react-dom": "^19.1.0",
74
- "vite": "^6.3.5",
75
- "vitest": "^3.2.4"
72
+ "react": "^19.2.0",
73
+ "react-dom": "^19.2.0",
74
+ "vite": "^7.1.12",
75
+ "vitest": "^4.0.2"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "prop-types": "^15.0.0",
@@ -91,8 +91,8 @@
91
91
  "node": ">=18"
92
92
  },
93
93
  "volta": {
94
- "node": "22.16.0",
95
- "npm": "10.9.2"
94
+ "node": "22.21.0",
95
+ "npm": "10.9.4"
96
96
  },
97
97
  "lint-staged": {
98
98
  "*.{js,jsx,mjs,cjs}": [
@@ -474,9 +474,7 @@ const routes = routesModule.routes || routesModule.default;
474
474
  const notFoundTemplate = routesModule.notFoundTemplate;
475
475
 
476
476
  // Check if App component is exported from routes file
477
- // App can be: named export, default export, or App property on default export
478
- const App = routesModule.App ||
479
- (typeof routesModule.default === 'function' ? routesModule.default : routesModule.default?.App);
477
+ const App = routesModule.App || routesModule.default?.App;
480
478
 
481
479
  if (!routes) {
482
480
  throw new Error('Could not find routes export. Expected "export const routes" or "export default" from ${relativePath}');
@@ -484,7 +482,7 @@ if (!routes) {
484
482
 
485
483
  // Helper to check if a route is dynamic (contains :param)
486
484
  const isDynamicRoute = (path) => path.split("/").some(segment => segment.startsWith(":"));
487
- export { routes, App };
485
+ export { routes };
488
486
 
489
487
  // Mock minimal window object for SSG
490
488
  function mockWindow(url, baseUrl) {
@@ -510,66 +508,38 @@ function mockWindow(url, baseUrl) {
510
508
  removeEventListener: () => {},
511
509
  dispatchEvent: () => {}
512
510
  };
513
- // Mock for document with more complete implementation
514
- const mockElements = [];
515
511
  global.document = {
516
- title: '', // Mock title property for SSG
517
512
  addEventListener: () => {},
518
513
  removeEventListener: () => {},
519
- querySelector: (selector) => {
520
- // Return mock head for head selector
521
- if (selector === 'head') {
522
- return {
523
- appendChild: (elem) => {
524
- mockElements.push(elem);
525
- return elem;
526
- }
527
- };
528
- }
529
- // For meta tag queries, return null (tag not found)
530
- return null;
531
- },
532
- createElement: (tagName) => {
533
- const elem = {
534
- tagName,
535
- attributes: {},
536
- setAttribute: function(name, value) {
537
- this.attributes[name] = value;
538
- },
539
- appendChild: () => {}
540
- };
541
- return elem;
542
- },
514
+ querySelector: () => null,
515
+ createElement: () => ({
516
+ setAttribute: () => {},
517
+ appendChild: () => {}
518
+ }),
543
519
  head: {
544
- appendChild: (elem) => {
545
- mockElements.push(elem);
546
- return elem;
547
- },
548
- querySelector: () => null,
549
- querySelectorAll: () => []
520
+ appendChild: () => {}
550
521
  }
551
522
  };
552
523
  }
553
524
 
554
525
  export function render(url, baseUrl) {
555
- // Check if we should render the full App or just the route element
526
+ // Check if we should render the full App or just the route element
556
527
  if (App) {
557
- // Find the route to render
558
- const route = routes.find(r => {
559
- if (r.path === url) return true;
560
- if (r.path === '/' && url === '/') return true;
561
- if (isDynamicRoute(r.path)) return false;
562
- return r.path === url;
563
- });
564
-
565
528
  // Mock window for the current route
566
529
  mockWindow(url, baseUrl);
567
530
 
568
531
  try {
569
-
570
- // Render the App with Routerino SSG-aware
532
+ // Render the full App component (which includes Routerino)
571
533
  const html = ReactDOMServer.renderToString(React.createElement(App));
572
534
 
535
+ // Find the rendered route to get its metadata
536
+ const route = routes.find(r => {
537
+ if (r.path === url) return true;
538
+ if (r.path === '/' && url === '/') return true;
539
+ if (isDynamicRoute(r.path)) return false;
540
+ return r.path === url;
541
+ });
542
+
573
543
  return {
574
544
  html,
575
545
  title: route?.title,
@@ -579,7 +549,6 @@ export function render(url, baseUrl) {
579
549
  };
580
550
  } catch (error) {
581
551
  console.error(\`[Routerino Forge] Failed to render App for route \${url}:\`, error.message);
582
- console.error(\`[Routerino Forge] Stack trace:\`, error.stack);
583
552
  // Fall back to route-only rendering
584
553
  } finally {
585
554
  // Clean up global mocks
@@ -589,17 +558,12 @@ export function render(url, baseUrl) {
589
558
  }
590
559
 
591
560
  // Original behavior: render just the route element
592
- // Need to find the route again if App path wasn't taken
593
- const route = App ? null : routes.find(r => {
561
+ const route = routes.find(r => {
594
562
  if (r.path === url) return true;
595
563
  if (r.path === '/' && url === '/') return true;
596
564
  if (isDynamicRoute(r.path)) return false;
597
565
  return r.path === url;
598
- });
599
-
600
- // If we get here and App was defined, it means the App render failed
601
- // Return early to avoid duplicate rendering
602
- if (App) return;
566
+ });
603
567
 
604
568
  if (!route) {
605
569
  if (notFoundTemplate) {
@@ -777,7 +741,7 @@ export function render(url, baseUrl) {
777
741
  const reductionPercent = (
778
742
  (1 - imageStats.placeholderSize / imageStats.totalSize) *
779
743
  100
780
- ).toFixed(2);
744
+ ).toFixed(0);
781
745
  console.log(
782
746
  `[Routerino Forge] ✓ Optimized ${imageStats.processed} images (${totalSizeMB}MB total, ${placeholderSizeKB}KB placeholders, ${reductionPercent}% reduction)`
783
747
  );
@@ -1104,7 +1068,7 @@ async function generate404Page({ template, outputDir, config, render }) {
1104
1068
  config.baseUrl
1105
1069
  );
1106
1070
 
1107
- // The render function will return the notFoundTemplate HTML (already includes App wrapper if App exists)
1071
+ // The render function will return the notFoundTemplate HTML
1108
1072
  const renderedHTML = renderResult.html || "404 - Page Not Found";
1109
1073
 
1110
1074
  // Generate meta tags for 404 page
@@ -1230,5 +1194,4 @@ Sitemap: ${config.baseUrl}/sitemap.xml`;
1230
1194
  }
1231
1195
  }
1232
1196
 
1233
- // Default export
1234
1197
  export default routerinoForge;