@cruxplug/spa 0.0.8 → 0.0.9
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 +9 -17
- package/dist/index.cjs +26 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -9
- package/dist/index.d.ts +8 -9
- package/dist/index.js +26 -26
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div align="center">
|
|
11
|
-
<img src="https://img.shields.io/badge/v-0.0.
|
|
11
|
+
<img src="https://img.shields.io/badge/v-0.0.9-black"/>
|
|
12
12
|
<a href="https://github.com/cruxjs-org"><img src="https://img.shields.io/badge/🔥-@cruxjs-black"/></a>
|
|
13
13
|
<br>
|
|
14
14
|
<img src="https://img.shields.io/badge/coverage-~%25-brightgreen" alt="Test Coverage" />
|
|
@@ -125,22 +125,15 @@
|
|
|
125
125
|
const pages: SPAPageConfig[] = [
|
|
126
126
|
{
|
|
127
127
|
// Page metadata with translation support
|
|
128
|
-
|
|
129
|
-
title : ['meta.home.title', 'Home'],
|
|
128
|
+
title : 'meta.home.title',
|
|
130
129
|
path : '/',
|
|
131
|
-
description :
|
|
132
|
-
|
|
133
|
-
// Keywords: strings are NOT translated, arrays ARE translated
|
|
134
|
-
keywords: [
|
|
135
|
-
'home', // Direct string - no translation
|
|
136
|
-
['meta.keywords.landing'], // Translate this keyword
|
|
137
|
-
['meta.keywords.welcome', 'Welcome'] // With fallback
|
|
138
|
-
],
|
|
130
|
+
description : 'meta.home.desc',
|
|
131
|
+
keywords : ['home', 'platform', 'app'],
|
|
139
132
|
|
|
140
133
|
// E-E-A-T Signals with translation support
|
|
141
134
|
expertise : 'Full-Stack Web Development',
|
|
142
135
|
experience : '2025+',
|
|
143
|
-
authority :
|
|
136
|
+
authority : 'meta.authority',
|
|
144
137
|
|
|
145
138
|
// Content type for AI indexing
|
|
146
139
|
contentType : 'page',
|
|
@@ -158,18 +151,17 @@
|
|
|
158
151
|
const errorPages: ErrorPageConfig[] = [
|
|
159
152
|
{
|
|
160
153
|
statusCode : 404,
|
|
161
|
-
title :
|
|
154
|
+
title : 'meta.error.404.title',
|
|
162
155
|
path : '/*',
|
|
163
|
-
description :
|
|
164
|
-
keywords : ['error', '404', 'not found'], // Direct strings - no translation
|
|
156
|
+
description : 'meta.error.404.desc',
|
|
165
157
|
robots : 'noindex, nofollow',
|
|
166
158
|
contentType : 'page'
|
|
167
159
|
},
|
|
168
160
|
{
|
|
169
161
|
statusCode : 500,
|
|
170
|
-
title : '500
|
|
162
|
+
title : 'meta.error.500.title',
|
|
171
163
|
path : '/500',
|
|
172
|
-
description : '
|
|
164
|
+
description : 'meta.error.500.desc'
|
|
173
165
|
}
|
|
174
166
|
];
|
|
175
167
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var server=require('@minejs/server'),i18n=require('@minejs/i18n');function h(e,t=""){if(!e)return t;if(e.includes(".")&&/^[a-z0-9._]+$/.test(e))try{return server.t(e)||e}catch{return e}return e}function u(e,t=""){return h(e,t)}function w(e){return !e||e.length===0?"":e.map(n=>h(n)).filter(Boolean).join(", ")}function P(e){if(!e)return "Page";let t=h(e);try{return v(t)}catch{return t}}function v(e){let t=server.t("app.name");return i18n.isRTL()?`${t} - ${e}`:`${e} - ${t}`}function y(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,r=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",a=P(e.title),i=u(e.description,t.defaultDescription||"A modern single-page application"),s=w(e.keywords||t.defaultKeywords),o=u(e.expertise,""),c=u(e.experience,""),l=u(e.authority,""),p=e.contentType==="article"?"article":"website";return `<!-- \u{1F50D} Core SEO Meta Tags -->
|
|
2
2
|
<meta charset="UTF-8" />
|
|
3
3
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4
|
-
<title>${
|
|
5
|
-
<meta name="description" content="${
|
|
6
|
-
${
|
|
7
|
-
<meta name="robots" content="${
|
|
4
|
+
<title>${a}</title>
|
|
5
|
+
<meta name="description" content="${i}" />
|
|
6
|
+
${s?`<meta name="keywords" content="${s}" />`:""}
|
|
7
|
+
<meta name="robots" content="${r}" />
|
|
8
8
|
<meta name="language" content="en" />
|
|
9
9
|
<meta http-equiv="content-language" content="en-us" />
|
|
10
10
|
<!-- \u{1F465} E-E-A-T Signals for AI Search -->
|
|
11
11
|
${t.author?`<meta name="author" content="${t.author}" />`:""}
|
|
12
|
-
${
|
|
13
|
-
${
|
|
14
|
-
${
|
|
12
|
+
${o?`<meta name="expertise" content="${o}" />`:""}
|
|
13
|
+
${c?`<meta name="experience" content="${c}" />`:""}
|
|
14
|
+
${l?`<meta name="authority" content="${l}" />`:""}
|
|
15
15
|
<!-- \u{1F4F1} Mobile & Performance -->
|
|
16
16
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
17
17
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
@@ -19,39 +19,39 @@
|
|
|
19
19
|
<meta name="theme-color" content="#000000" />
|
|
20
20
|
<!-- \u{1F517} Canonical & Prefetch -->
|
|
21
21
|
<link rel="canonical" href="${n}" />
|
|
22
|
-
${(e.clientScriptPath||t.clientScriptPath)?.map(
|
|
22
|
+
${(e.clientScriptPath||t.clientScriptPath)?.map(g=>`<link rel="prefetch" href="${g}" />`).join(`
|
|
23
23
|
`)}
|
|
24
24
|
<!-- \u26A1 Performance & Security -->
|
|
25
25
|
<meta name="format-detection" content="telephone=no" />
|
|
26
26
|
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
|
27
27
|
<!-- \u{1F4D8} Open Graph Protocol (Social Media) -->
|
|
28
|
-
<meta property="og:type" content="${
|
|
29
|
-
<meta property="og:title" content="${
|
|
30
|
-
<meta property="og:description" content="${
|
|
28
|
+
<meta property="og:type" content="${p}" />
|
|
29
|
+
<meta property="og:title" content="${a}" />
|
|
30
|
+
<meta property="og:description" content="${i}" />
|
|
31
31
|
<meta property="og:url" content="${n}" />
|
|
32
32
|
<meta property="og:locale" content="en_US" />
|
|
33
33
|
${t.author?`<meta property="og:site_name" content="${t.author}" />`:""}
|
|
34
34
|
${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
|
|
35
|
-
${e.ogImage?`<meta property="og:image:alt" content="${
|
|
36
|
-
`}function
|
|
37
|
-
${JSON.stringify(
|
|
38
|
-
`).map(
|
|
35
|
+
${e.ogImage?`<meta property="og:image:alt" content="${a}" />`:""}
|
|
36
|
+
`}function S(e,t,n="WebPage"){let r=e.canonical||`${t.baseUrl}${e.path}`,a=P(e.title),i=u(e.description,t.defaultDescription),s=u(e.expertise,""),o=u(e.experience,""),c=u(e.authority,""),l={"@context":"https://schema.org","@type":n,name:a,url:r,description:i,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(s||o||c)&&{creator:{"@type":"Person",name:t.author||"Unknown",...s&&{expertise:s},...o&&{experience:o},...c&&{authority:c}}}};return `<script type="application/ld+json">
|
|
37
|
+
${JSON.stringify(l,null,2).split(`
|
|
38
|
+
`).map(g=>g&&` ${g}`).join(`
|
|
39
39
|
`)}
|
|
40
|
-
</script>`}var
|
|
41
|
-
`).map(
|
|
42
|
-
`)}function
|
|
43
|
-
`),
|
|
44
|
-
`),
|
|
40
|
+
</script>`}var R=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"];function k(e){let t=[],n=/(<[^>]+>)|([^<]+)/g,r;for(;(r=n.exec(e))!==null;)r[1]?t.push({type:"tag",value:r[1]}):r[2]&&r[2].trim().length>0&&t.push({type:"text",value:r[2]});return t}function L(e,t=4){let n=" ".repeat(t),r=[],a=0;for(let i=0;i<e.length;i++){let s=e[i],o=s.value.trim();o.startsWith("</")&&(a=Math.max(0,a-1));let c=n.repeat(a);if(!(s.type==="text"&&o.length===0)&&(s.type==="text"&&r.length>0&&!r[r.length-1].trim().endsWith(">")?r[r.length-1]+=o:r.push(c+o),o.startsWith("<")&&!o.startsWith("</"))){if(o.startsWith("<!DOCTYPE")||o.startsWith("<!--"))continue;let l=o.match(/<([A-Za-z][A-Za-z0-9\\-]*)/i),p=l?l[1].toLowerCase():"",g=R.includes(p),T=o.endsWith("/>");!g&&!T&&a++;}}return r}function x(e,t=4){let n=e.split(`
|
|
41
|
+
`).map(i=>i.trim()).filter(i=>i.length>0).join(""),r=k(n);return L(r,t).join(`
|
|
42
|
+
`)}function M(e){let t={defaultLanguage:e?.defaultLanguage||"en",supportedLanguages:e?.supportedLanguages||["en"],basePath:e?.basePath||"i18n/",fileExtension:e?.fileExtension||"json"},n=t.basePath;return n=n.replace(/\\/g,"/"),n.includes("shared/static")?n="static"+(n.split("shared/static")[1]||""):n.startsWith("./")?n=n.slice(2):n.startsWith("/")&&(n=n.slice(1)),n.endsWith("/")||(n+="/"),`<meta name="app-i18n" content='${JSON.stringify({defaultLanguage:t.defaultLanguage,supportedLanguages:t.supportedLanguages,basePath:n,fileExtension:t.fileExtension})}' />`}function d(e,t,n){let r=e.clientScriptPath||t.clientScriptPath,a=e.clientStylePath||t.clientStylePath||[],i=r.map(p=>`<script type="module" src="${p}"></script>`).join(`
|
|
43
|
+
`),s=a.map(p=>`<link rel="stylesheet" href="${p}" />`).join(`
|
|
44
|
+
`),o=M(n);o||console.warn("[SPA] WARNING: i18n meta tag is empty!");let c=`<!DOCTYPE html>
|
|
45
45
|
<html lang="en">
|
|
46
46
|
<head>
|
|
47
|
-
${
|
|
48
|
-
${
|
|
47
|
+
${y(e,t)}
|
|
48
|
+
${S(e,t,e.contentType==="article"?"Article":"WebPage")}
|
|
49
|
+
${o}
|
|
49
50
|
${s}
|
|
50
|
-
${i}
|
|
51
51
|
</head>
|
|
52
52
|
<body>
|
|
53
53
|
<div id="app"></div>
|
|
54
|
-
${
|
|
54
|
+
${i}
|
|
55
55
|
</body>
|
|
56
|
-
</html>`;
|
|
56
|
+
</html>`;c.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag NOT in raw HTML!");let l=x(c);return l.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag LOST during formatting!"),l}function m(e,t,n){return {method:"GET",path:e.path,handler:r=>{let a=d(e,t,n);return r.html(a)}}}function _(){return global.__cruxjs_i18n_config}function A(e){global.__cruxjs_i18n_config=e;}function j(e,t,n,r){if(r.startsWith("/api/"))return {status:e,headers:{"Content-Type":"application/json"},body:JSON.stringify({error:`Error ${e}`})};if(t.has(e)){let a=t.get(e),i=_();console.log(`[Errors] Generating error page ${e} with i18n:`,!!i);let s=d(a,n,i);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:s}}return console.log(`[Errors] No custom error page for ${e}, returning fallback`),{status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function $(e,t){return (n,r)=>{let a=j(n,e,t,r);return new Response(a.body,{status:a.status,headers:a.headers})}}function b(){return {statusCode:404,title:"404 - Page Not Found",path:"/404",description:"The page you are looking for could not be found.",keywords:["404","not found","error"],robots:"noindex, nofollow"}}function K(e,t){let n=[],r=new Map;A(t?.i18n);let a=t?.i18n;if(e.pages&&e.pages.length>0)for(let o of e.pages)n.push(m(o,e,a));if(e.errorPages&&e.errorPages.length>0)for(let o of e.errorPages)r.set(o.statusCode,o),n.push(m(o,e,a));if(e.enableAutoNotFound&&!r.has(404)){let o=b();r.set(404,o),n.push(m(o,e,a));}let i=$(r,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:n,__spaErrorHandler:i,onRegister:async o=>{if(console.log(`[SPA Plugin] Registered ${n.length} SPA routes`),r.size>0){let c=Array.from(r.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${c}`);}},onAwake:async o=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async o=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async o=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}exports.serverSPA=K;//# sourceMappingURL=index.cjs.map
|
|
57
57
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../node_modules/@minejs/i18n/src/index.ts","../src/utils/seo.ts","../src/utils/htmlFormatter.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["I18nManager","config","stored","lang","translations","flattened","trans","obj","prefix","key","value","newKey","params","translation","param","paramValue","fallback","original","result","tokens","regex","match","fn","callback","instance","getI18n","t","isRTL","genPageTitle","appName","pageName","resolveMetaValue","defaultValue","c","resolveKeywords","keywords","kw","resolvePageTitle","title","titleKey","C","generateSEOMetaTags","baseConfig","canonicalUrl","robots","description","expertise","experience","authority","ogType","script","generateStructuredData","pageConfig","contentType","schema","line","VOID_ELEMENTS","tokenize","html","processTokens","indentSize","indent","output","indentLevel","i","token","padding","tagMatch","tagName","isVoidElement","isSelfClosing","formatHTML","normalized","generateI18nMetaTag","i18nConfig","browserPath","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","i18nMetaTag","rawHTML","formatted","createSPARoute","getGlobalI18nConfig","setGlobalI18nConfig","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","appConfig","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"aAgBW,IAAMA,CAAAA,CAAN,KAAkB,CAmBjB,WAAA,CAAYC,CAAAA,CAA2B,CAfvC,IAAA,CAAQ,YAAA,CAA6C,EAAA,CACrD,IAAA,CAAQ,eAAA,CAA2C,KACnD,IAAA,CAAQ,eAAA,CAA2C,IAAA,CACnD,IAAA,CAAQ,gBAAA,CAA2C,IAAA,CACnD,IAAA,CAAQ,kBAAA,CAAsB,IAAI,GAAA,CAAwB,CAAC,IAAI,CAAC,CAAA,CAChE,KAAQ,YAAA,CAAsB,IAAI,GAAA,CAAY,CAAC,IAAA,CAAM,IAAA,CAAM,KAAM,IAAA,CAAM,IAAA,CAAM,IAAA,CAAM,IAAA,CAAM,IAAI,CAAC,EAC9F,IAAA,CAAQ,SAAA,CAAsB,IAAI,GAAA,CAU1BA,CAAAA,GACA,IAAA,CAAK,eAAA,CAAkBA,CAAAA,CAAO,eAAA,EAAmB,IAAA,CACjD,IAAA,CAAK,gBAAA,CAAmBA,CAAAA,CAAO,gBAAA,EAAoBA,EAAO,eAAA,EAAmB,IAAA,CAC7E,IAAA,CAAK,eAAA,CAAkBA,CAAAA,CAAO,eAAA,EAAmB,KACjD,IAAA,CAAK,OAAA,CAAUA,CAAAA,CAAO,OAAA,CACtB,IAAA,CAAK,gBAAA,CAAmBA,EAAO,gBAAA,CAE3BA,CAAAA,CAAO,kBAAA,GACP,IAAA,CAAK,kBAAA,CAAqB,IAAI,GAAA,CAAIA,CAAAA,CAAO,kBAAkB,CAAA,CAAA,EAGvE,CAKA,MAAa,IAAA,EAAsB,CAC/B,GAAI,IAAA,CAAK,OAAA,CAAS,CACd,IAAMC,CAAAA,CAAS,MAAM,KAAK,OAAA,CAAQ,GAAA,CAAI,eAAe,CAAA,CACjDA,CAAAA,EAAU,IAAA,CAAK,mBAAmB,GAAA,CAAIA,CAAM,CAAA,GAC5C,IAAA,CAAK,eAAA,CAAkBA,CAAAA,EAE/B,CACJ,CAaO,YAAA,CAAaC,CAAAA,CAA0BC,CAAAA,CAAyC,CAC9E,IAAA,CAAK,YAAA,CAAaD,CAAI,CAAA,GACvB,IAAA,CAAK,YAAA,CAAaA,CAAI,CAAA,CAAI,IAG9B,IAAME,CAAAA,CAAY,IAAA,CAAK,aAAA,CAAcD,CAAY,CAAA,CACjD,KAAK,YAAA,CAAaD,CAAI,CAAA,CAAI,CAAE,GAAG,IAAA,CAAK,YAAA,CAAaA,CAAI,CAAA,CAAG,GAAGE,CAAU,CAAA,CACrE,IAAA,CAAK,kBAAA,CAAmB,IAAIF,CAAI,EACpC,CAMO,gBAAA,CAAiBC,CAAAA,CAA0C,CAC9D,OAAO,OAAA,CAAQA,CAAY,CAAA,CAAE,OAAA,CAAQ,CAAC,CAACD,EAAMG,CAAK,CAAA,GAAM,CACpD,IAAA,CAAK,YAAA,CAAaH,CAAAA,CAAMG,CAAK,EACjC,CAAC,EACL,CAOQ,aAAA,CAAcC,CAAAA,CAA0BC,CAAAA,CAAiB,GAA4B,CACzF,IAAMH,CAAAA,CAAoC,EAAA,CAE1C,IAAA,IAAWI,KAAOF,CAAAA,CAAK,CACnB,GAAI,CAAC,MAAA,CAAO,SAAA,CAAU,eAAe,IAAA,CAAKA,CAAAA,CAAKE,CAAG,CAAA,CAAG,SAErD,IAAMC,CAAAA,CAAQH,CAAAA,CAAIE,CAAG,CAAA,CACfE,CAAAA,CAASH,CAAAA,CAAS,CAAA,EAAGA,CAAM,IAAIC,CAAG,CAAA,CAAA,CAAKA,CAAAA,CAEzC,OAAOC,CAAAA,EAAU,QAAA,EAAYA,IAAU,IAAA,EAAQ,CAAC,KAAA,CAAM,OAAA,CAAQA,CAAK,CAAA,CACnE,OAAO,MAAA,CAAOL,CAAAA,CAAW,IAAA,CAAK,aAAA,CAAcK,CAAAA,CAAOC,CAAM,CAAC,CAAA,CAE1DN,CAAAA,CAAUM,CAAM,CAAA,CAAI,MAAA,CAAOD,CAAK,EAExC,CAEA,OAAOL,CACX,CAkBO,CAAA,CAAEI,CAAAA,CAAaG,CAAAA,CAAyC,CAC3D,IAAIC,CAAAA,CAAc,IAAA,CAAK,cAAA,CAAeJ,CAAG,CAAA,CAEzC,OAAIG,CAAAA,EACA,MAAA,CAAO,OAAA,CAAQA,CAAM,CAAA,CAAE,OAAA,CAAQ,CAAC,CAACE,CAAAA,CAAOJ,CAAK,CAAA,GAAM,CAE/C,IAAMK,CAAAA,CAAa,KAAK,cAAA,CAAeL,CAAAA,CAAOA,CAAK,CAAA,CACnDG,CAAAA,CAAcA,CAAAA,CAAY,QACtB,IAAI,MAAA,CAAO,CAAA,GAAA,EAAMC,CAAK,CAAA,GAAA,CAAA,CAAO,GAAG,EAChCC,CACJ,EACJ,CAAC,CAAA,CAGEF,CACX,CAMQ,cAAA,CAAeJ,CAAAA,CAAaO,CAAAA,CAA2B,CAE3D,OAAI,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,GAAIP,CAAG,CAAA,CACtC,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,CAAEA,CAAG,CAAA,CAIlD,IAAA,CAAK,gBAAA,GAAqB,IAAA,CAAK,iBAC/B,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,gBAAgB,CAAA,GAAIA,CAAG,CAAA,CACvC,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,gBAAgB,CAAA,CAAEA,CAAG,CAAA,CAInD,KAAK,eAAA,GAAoB,IAAA,CAAK,eAAA,EAC9B,IAAA,CAAK,eAAA,GAAoB,IAAA,CAAK,kBAC9B,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,GAAIA,CAAG,EACtC,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,CAAEA,CAAG,CAAA,EAItD,OAAA,CAAQ,IAAA,CAAK,CAAA,mCAAA,EAAsCA,CAAG,CAAA,SAAA,EAAY,IAAA,CAAK,eAAe,GAAG,CAAA,CAClFO,CAAAA,EAAYP,CAAAA,CACvB,CASO,KAAA,CAAMA,CAAAA,CAAaN,EAA0BS,CAAAA,CAAyC,CACzF,IAAMK,CAAAA,CAAW,IAAA,CAAK,eAAA,CACtB,KAAK,eAAA,CAAkBd,CAAAA,CACvB,IAAMe,CAAAA,CAAS,IAAA,CAAK,CAAA,CAAET,CAAAA,CAAKG,CAAM,CAAA,CACjC,OAAA,IAAA,CAAK,eAAA,CAAkBK,CAAAA,CAChBC,CACX,CAoBO,MAAA,CAAOT,CAAAA,CAAaG,CAAAA,CAA2D,CAClF,IAAIC,CAAAA,CAAc,KAAK,CAAA,CAAEJ,CAAAA,CAAKG,CAAM,CAAA,CAGpCC,CAAAA,CAAcA,CAAAA,CAAY,QAAQ,UAAA,CAAY,MAAM,CAAA,CAEpD,IAAMM,CAAAA,CAAmC,EAAA,CACnCC,CAAAA,CAAQ,oDAAA,CACVC,CAAAA,CAEJ,KAAA,CAAQA,CAAAA,CAAQD,CAAAA,CAAM,IAAA,CAAKP,CAAW,CAAA,IAAO,IAAA,EACrCQ,CAAAA,CAAM,CAAC,CAAA,CAEPF,CAAAA,CAAO,KAAK,CAAE,IAAA,CAAM,MAAA,CAAQ,OAAA,CAASE,CAAAA,CAAM,CAAC,CAAE,CAAC,CAAA,CACxCA,CAAAA,CAAM,CAAC,CAAA,CAEdF,CAAAA,CAAO,IAAA,CAAK,CAAE,IAAA,CAAM,KAAA,CAAO,GAAA,CAAKE,CAAAA,CAAM,CAAC,CAAA,CAAG,QAASA,CAAAA,CAAM,CAAC,CAAE,CAAC,CAAA,CACtDA,CAAAA,CAAM,CAAC,CAAA,EAEdF,CAAAA,CAAO,IAAA,CAAK,CAAE,IAAA,CAAM,KAAA,CAAO,GAAA,CAAKE,EAAM,CAAC,CAAA,CAAG,OAAA,CAAS,EAAG,CAAC,CAAA,CAI/D,OAAOF,CAAAA,CAAO,MAAA,CAAS,CAAA,CAAIA,CAAAA,CAAS,CAAC,CAAE,IAAA,CAAM,OAAQ,OAAA,CAASN,CAAY,CAAC,CAC/E,CAUA,MAAa,YAAYV,CAAAA,CAAyC,CAC9D,GAAI,CAAC,IAAA,CAAK,kBAAA,CAAmB,IAAIA,CAAI,CAAA,CAAG,CACpC,OAAA,CAAQ,IAAA,CAAK,CAAA,iBAAA,EAAoBA,CAAI,CAAA,eAAA,CAAiB,CAAA,CACtD,MACJ,CAEA,IAAA,CAAK,eAAA,CAAkBA,CAAAA,CAGnB,KAAK,OAAA,EACL,MAAM,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,eAAA,CAAiBA,CAAI,CAAA,CAIhD,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQmB,CAAAA,EAAMA,CAAAA,CAAGnB,CAAI,CAAC,CAAA,CAEjC,IAAA,CAAK,gBAAA,EACL,IAAA,CAAK,gBAAA,CAAiBA,CAAI,EAElC,CAKO,WAAA,EAAkC,CACrC,OAAO,IAAA,CAAK,eAChB,CAKO,qBAAA,EAA8C,CACjD,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,kBAAkB,CAC7C,CAKO,mBAAA,CAAoBA,CAAAA,CAAmC,CAC1D,OAAO,KAAK,kBAAA,CAAmB,GAAA,CAAIA,CAAI,CAC3C,CAUO,MAAA,CAAOM,CAAAA,CAAsB,CAChC,OAAO,CAAC,EACJ,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,GAAIA,CAAG,CAAA,EAC7C,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,gBAAgB,CAAA,GAAIA,CAAG,CAAA,EAC9C,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,GAAIA,CAAG,CAAA,CAErD,CAKO,eAAA,EAA0C,CAC7C,OAAO,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,eAAe,CAAA,EAAK,EACtD,CAKO,KAAA,EAAiB,CACpB,OAAO,IAAA,CAAK,YAAA,CAAa,GAAA,CAAI,KAAK,eAAA,CAAgB,WAAA,EAAA,CAAc,SAAA,CAAU,CAAA,CAAG,CAAC,CAAC,CACnF,CAKO,aAAA,CAAcN,CAAAA,CAAmC,CACpD,OAAO,IAAA,CAAK,YAAA,CAAa,GAAA,CAAIA,CAAAA,CAAK,WAAA,EAAA,CAAc,SAAA,CAAU,CAAA,CAAG,CAAC,CAAC,CACnE,CAMO,QAAA,CAASoB,CAAAA,CAA0D,CACtE,OAAA,IAAA,CAAK,SAAA,CAAU,GAAA,CAAIA,CAAQ,CAAA,CACpB,IAAM,KAAK,SAAA,CAAU,MAAA,CAAOA,CAAQ,CAC/C,CAIR,CAAA,CA+DA,IAqHIC,CAAAA,CAA+B,IAAA,CAK5B,SAASC,CAAAA,EAAuB,CACnC,OAAKD,CAAAA,GACDA,EAAW,IAAIxB,CAAAA,CAAAA,CAEZwB,CACX,CAwGO,IAAME,CAAAA,CAAI,CAACjB,CAAAA,CAAaG,CAAAA,GAC3Ba,CAAAA,EAAAA,CAAU,CAAA,CAAEhB,CAAAA,CAAKG,CAAM,CAAA,CADpB,IAqBMe,CAAAA,CAAQ,IACjBF,CAAAA,EAAAA,CAAU,KAAA,EAAA,CA6BP,SAASG,CAAAA,CAAanB,CAAAA,CAAaD,CAAAA,CAAiB,OAAA,CAAiB,CACxE,IAAMqB,CAAAA,CAAUH,CAAAA,CAAE,UAAU,CAAA,CACtBI,CAAAA,CAAWJ,CAAAA,CAAElB,CAAAA,CAASC,CAAG,CAAA,CAC/B,OAAOkB,CAAAA,EAAAA,CAAU,CAAA,EAAGE,CAAO,MAAMC,CAAQ,CAAA,CAAA,CAAK,CAAA,EAAGA,CAAQ,CAAA,GAAA,EAAMD,CAAO,CAAA,CAC1E,CC/oBA,SAASE,CAAAA,CAAiBrB,CAAAA,CAAsCsB,CAAAA,CAAuB,EAAA,CAAY,CAC/F,GAAI,CAACtB,CAAAA,CAAO,OAAOsB,CAAAA,CAGnB,GAAI,KAAA,CAAM,QAAQtB,CAAK,CAAA,CAAG,CACtB,GAAM,CAACD,CAAAA,CAAKO,CAAQ,CAAA,CAAIN,CAAAA,CACxB,GAAI,CAACD,CAAAA,CAAK,OAAOO,CAAAA,EAAYgB,CAAAA,CAC7B,GAAI,CACA,OAAOC,CAAAA,CAAExB,CAAG,CAAA,EAAKO,GAAYP,CAAAA,EAAOuB,CACxC,CAAA,KAAQ,CACJ,OAAOhB,CAAAA,EAAYP,GAAOuB,CAC9B,CACJ,CAIA,GAAI,CAEA,OADmBC,EAAEvB,CAAK,CAAA,EACLA,CACzB,CAAA,KAAQ,CACJ,OAAOA,CACX,CACJ,CAYA,SAASwB,CAAAA,CAAgBC,CAAAA,CAAqD,CAC1E,OAAI,CAACA,CAAAA,EAAYA,CAAAA,CAAS,MAAA,GAAW,CAAA,CAAU,EAAA,CAE9BA,CAAAA,CACZ,IAAIC,CAAAA,EAAM,CAEP,GAAI,OAAOA,CAAAA,EAAO,QAAA,CACd,OAAOA,CAAAA,CAIX,GAAI,KAAA,CAAM,OAAA,CAAQA,CAAE,CAAA,CAAG,CACnB,GAAM,CAAC3B,CAAAA,CAAKO,CAAQ,CAAA,CAAIoB,CAAAA,CACxB,GAAI,CAAC3B,CAAAA,CAAK,OAAOO,CAAAA,EAAY,EAAA,CAC7B,GAAI,CACA,OAAOiB,CAAAA,CAAExB,CAAG,CAAA,EAAKO,CAAAA,EAAYP,CACjC,CAAA,KAAQ,CACJ,OAAOO,CAAAA,EAAYP,CACvB,CACJ,CAEA,OAAO,EACX,CAAC,CAAA,CACA,MAAA,CAAO,OAAO,CAAA,CAEH,IAAA,CAAK,IAAI,CAC7B,CAQA,SAAS4B,CAAAA,CAAiBC,CAAAA,CAA8C,CACpE,GAAI,CAACA,CAAAA,CAAO,OAAO,MAAA,CAEnB,IAAIC,CAAAA,CAAW,EAAA,CAGX,MAAM,OAAA,CAAQD,CAAK,CAAA,CACnBC,CAAAA,CAAWD,CAAAA,CAAM,CAAC,CAAA,EAAK,EAAA,CAEvBC,CAAAA,CAAWD,CAAAA,CAIf,GAAI,CACA,OAAOE,CAAAA,CAAaD,EAAU,EAAE,CACpC,CAAA,KAAQ,CAEJ,OAAI,KAAA,CAAM,QAAQD,CAAK,CAAA,CACZA,CAAAA,CAAM,CAAC,CAAA,EAAKA,CAAAA,CAAM,CAAC,CAAA,EAAK,MAAA,CAE5BA,CACX,CACJ,CAaO,SAASG,CAAAA,CACZxC,CAAAA,CACAyC,CAAAA,CACM,CACN,IAAMC,CAAAA,CAAe1C,CAAAA,CAAO,SAAA,EAAa,GAAGyC,CAAAA,CAAW,OAAO,CAAA,EAAGzC,CAAAA,CAAO,IAAI,CAAA,CAAA,CACtE2C,EAAS3C,CAAAA,CAAO,MAAA,EAAUyC,CAAAA,CAAW,aAAA,EAAiB,8EAAA,CAGtDJ,CAAAA,CAAQD,EAAiBpC,CAAAA,CAAO,KAAK,CAAA,CACrC4C,CAAAA,CAAcd,CAAAA,CAAiB9B,CAAAA,CAAO,WAAA,CAAayC,CAAAA,CAAW,kBAAA,EAAsB,kCAAkC,CAAA,CACtHP,CAAAA,CAAWD,CAAAA,CAAgBjC,CAAAA,CAAO,UAAYyC,CAAAA,CAAW,eAAe,CAAA,CACxEI,CAAAA,CAAYf,CAAAA,CAAiB9B,CAAAA,CAAO,SAAA,CAAW,EAAE,CAAA,CACjD8C,CAAAA,CAAahB,CAAAA,CAAiB9B,CAAAA,CAAO,UAAA,CAAY,EAAE,EACnD+C,CAAAA,CAAYjB,CAAAA,CAAiB9B,CAAAA,CAAO,SAAA,CAAW,EAAE,CAAA,CAGjDgD,CAAAA,CAAShD,CAAAA,CAAO,WAAA,GAAgB,SAAA,CAAY,SAAA,CAAY,SAAA,CAE9D,OAAO,CAAA;AAAA;AAAA;AAAA,uBAAA,EAGUqC,CAAK,CAAA;AAAA,kDAAA,EACsBO,CAAW,CAAA;AAAA,gBAAA,EAC7CV,CAAAA,CAAW,CAAA,+BAAA,EAAkCA,CAAQ,CAAA,IAAA,CAAA,CAAS,EAAE;AAAA,6CAAA,EACnCS,CAAM,CAAA;AAAA;AAAA;AAAA;AAAA,gBAAA,EAInCF,EAAW,MAAA,CAAS,CAAA,6BAAA,EAAgCA,CAAAA,CAAW,MAAM,OAAS,EAAE;AAAA,gBAAA,EAChFI,CAAAA,CAAY,CAAA,gCAAA,EAAmCA,CAAS,CAAA,IAAA,CAAA,CAAS,EAAE;AAAA,gBAAA,EACnEC,CAAAA,CAAa,CAAA,iCAAA,EAAoCA,CAAU,CAAA,IAAA,CAAA,CAAS,EAAE;AAAA,gBAAA,EACtEC,CAAAA,CAAY,CAAA,gCAAA,EAAmCA,CAAS,CAAA,IAAA,CAAA,CAAS,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4CAAA,EAOvCL,CAAY,CAAA;AAAA,gBAAA,EAAA,CACvC1C,CAAAA,CAAO,gBAAA,EAAoByC,CAAAA,CAAW,gBAAA,GAAmB,GAAA,CAAIQ,GAAU,CAAA,2BAAA,EAA8BA,CAAM,CAAA,IAAA,CAAM,CAAA,CAAE,IAAA,CAAK;AAAA,CAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,kDAAA,EAK5FD,CAAM,CAAA;AAAA,mDAAA,EACLX,CAAK,CAAA;AAAA,yDAAA,EACCO,CAAW,CAAA;AAAA,iDAAA,EACnBF,CAAY,CAAA;AAAA;AAAA,gBAAA,EAE7CD,EAAW,MAAA,CAAS,CAAA,uCAAA,EAA0CA,CAAAA,CAAW,MAAM,OAAS,EAAE;AAAA,gBAAA,EAC1FzC,EAAO,OAAA,CAAU,CAAA,mCAAA,EAAsCA,CAAAA,CAAO,OAAO,OAAS,EAAE;AAAA,gBAAA,EAChFA,CAAAA,CAAO,OAAA,CAAU,CAAA,uCAAA,EAA0CqC,CAAK,OAAS,EAAE;AAAA,QAAA,CAEzF,CAaO,SAASa,CAAAA,CACZC,CAAAA,CACAV,CAAAA,CACAW,CAAAA,CAAsB,SAAA,CAChB,CACN,IAAMV,CAAAA,CAAeS,CAAAA,CAAW,SAAA,EAAa,CAAA,EAAGV,CAAAA,CAAW,OAAO,CAAA,EAAGU,CAAAA,CAAW,IAAI,CAAA,CAAA,CAG9Ed,CAAAA,CAAQD,CAAAA,CAAiBe,CAAAA,CAAW,KAAK,CAAA,CACzCP,CAAAA,CAAcd,CAAAA,CAAiBqB,CAAAA,CAAW,WAAA,CAAaV,CAAAA,CAAW,kBAAkB,CAAA,CACpFI,CAAAA,CAAYf,CAAAA,CAAiBqB,CAAAA,CAAW,SAAA,CAAW,EAAE,CAAA,CACrDL,CAAAA,CAAahB,CAAAA,CAAiBqB,CAAAA,CAAW,UAAA,CAAY,EAAE,CAAA,CACvDJ,CAAAA,CAAYjB,CAAAA,CAAiBqB,CAAAA,CAAW,SAAA,CAAW,EAAE,CAAA,CAErDE,CAAAA,CAAS,CACX,UAAA,CAAY,oBAAA,CACZ,OAAA,CAASD,CAAAA,CACT,IAAA,CAAQf,CAAAA,CACR,GAAA,CAAOK,CAAAA,CACP,WAAA,CAAeE,EACf,UAAA,CAAc,IAAA,CACd,GAAIO,CAAAA,CAAW,WAAA,EAAe,CAAE,KAAA,CAASA,CAAAA,CAAW,WAAY,CAAA,CAChE,GAAIV,CAAAA,CAAW,MAAA,EAAU,CACrB,MAAA,CAAU,CACN,OAAA,CAAS,QAAA,CACT,IAAA,CAAQA,CAAAA,CAAW,MAAA,CACnB,GAAIA,CAAAA,CAAW,SAAA,EAAa,CAAE,GAAA,CAAOA,CAAAA,CAAW,SAAU,CAC9D,CACJ,CAAA,CACA,GAAA,CAAKI,CAAAA,EAAaC,CAAAA,EAAcC,CAAAA,GAAc,CAC1C,OAAA,CAAW,CACP,OAAA,CAAS,QAAA,CACT,IAAA,CAAQN,CAAAA,CAAW,MAAA,EAAU,SAAA,CAC7B,GAAII,CAAAA,EAAa,CAAE,SAAA,CAAaA,CAAU,CAAA,CAC1C,GAAIC,CAAAA,EAAc,CAAE,UAAA,CAAcA,CAAW,CAAA,CAC7C,GAAIC,CAAAA,EAAa,CAAE,SAAA,CAAaA,CAAU,CAC9C,CACJ,CACJ,EAMA,OAAO,CAAA;AAAA,EAJS,KAAK,SAAA,CAAUM,CAAAA,CAAQ,IAAA,CAAM,CAAC,EACzC,KAAA,CAAM;AAAA,CAAI,CAAA,CACV,IAAIC,CAAAA,EAAQA,CAAAA,EAAO,OAAOA,CAAI,CAAA,CAAS,EACvC,IAAA,CAAK;AAAA,CAAI,CACwC;AAAA,SAAA,CAC1D,CC1OA,IAAMC,CAAAA,CAAgB,CAAC,MAAA,CAAQ,OAAQ,IAAA,CAAM,KAAA,CAAO,OAAA,CAAS,IAAA,CAAM,KAAA,CAAO,OAAA,CAAS,OAAQ,MAAA,CAAQ,OAAA,CAAS,QAAA,CAAU,OAAA,CAAS,KAAK,CAAA,CAUpI,SAASC,CAAAA,CAASC,CAAAA,CAAuB,CACrC,IAAMvC,CAAAA,CAAkB,GAClBC,CAAAA,CAAQ,oBAAA,CACVC,CAAAA,CAEJ,KAAA,CAAQA,CAAAA,CAAQD,CAAAA,CAAM,KAAKsC,CAAI,CAAA,IAAO,IAAA,EAC9BrC,CAAAA,CAAM,CAAC,CAAA,CAEPF,EAAO,IAAA,CAAK,CAAE,IAAA,CAAM,KAAA,CAAO,KAAA,CAAOE,CAAAA,CAAM,CAAC,CAAE,CAAC,CAAA,CACrCA,CAAAA,CAAM,CAAC,CAAA,EAAKA,CAAAA,CAAM,CAAC,CAAA,CAAE,IAAA,EAAK,CAAE,MAAA,CAAS,CAAA,EAE5CF,CAAAA,CAAO,KAAK,CAAE,IAAA,CAAM,MAAA,CAAQ,KAAA,CAAOE,CAAAA,CAAM,CAAC,CAAE,CAAC,CAAA,CAIrD,OAAOF,CACX,CAKA,SAASwC,CAAAA,CAAcxC,CAAAA,CAAiByC,CAAAA,CAAqB,CAAA,CAAa,CACtE,IAAMC,CAAAA,CAAS,GAAA,CAAI,OAAOD,CAAU,CAAA,CAC9BE,CAAAA,CAAmB,EAAC,CACtBC,CAAAA,CAAc,EAGlB,IAAA,IAASC,CAAAA,CAAI,CAAA,CAAGA,CAAAA,CAAI7C,CAAAA,CAAO,MAAA,CAAQ6C,IAAK,CACpC,IAAMC,CAAAA,CAAQ9C,CAAAA,CAAO6C,CAAC,CAAA,CAChBtD,CAAAA,CAAQuD,CAAAA,CAAM,KAAA,CAAM,IAAA,EAAK,CAG3BvD,CAAAA,CAAM,UAAA,CAAW,IAAI,IACrBqD,CAAAA,CAAc,IAAA,CAAK,GAAA,CAAI,CAAA,CAAGA,CAAAA,CAAc,CAAC,GAG7C,IAAMG,CAAAA,CAAUL,CAAAA,CAAO,MAAA,CAAOE,CAAW,CAAA,CAGzC,GAAI,EAAAE,CAAAA,CAAM,IAAA,GAAS,MAAA,EAAUvD,CAAAA,CAAM,MAAA,GAAW,KAK1CuD,CAAAA,CAAM,IAAA,GAAS,MAAA,EAEXH,CAAAA,CAAO,MAAA,CAAS,CAAA,EAAK,CAACA,CAAAA,CAAOA,CAAAA,CAAO,MAAA,CAAS,CAAC,CAAA,CAAE,IAAA,GAAO,QAAA,CAAS,GAAG,CAAA,CAEnEA,CAAAA,CAAOA,CAAAA,CAAO,MAAA,CAAS,CAAC,CAAA,EAAKpD,CAAAA,CAMjCoD,CAAAA,CAAO,IAAA,CAAKI,CAAAA,CAAUxD,CAAK,CAAA,CAI3BA,CAAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAK,CAACA,CAAAA,CAAM,UAAA,CAAW,IAAI,CAAA,CAAA,CAAG,CAElD,GAAIA,CAAAA,CAAM,UAAA,CAAW,WAAW,GAAKA,CAAAA,CAAM,UAAA,CAAW,MAAM,CAAA,CACxD,SAIJ,IAAMyD,EAAWzD,CAAAA,CAAM,KAAA,CAAM,6BAA6B,CAAA,CACpD0D,CAAAA,CAAUD,CAAAA,CAAWA,CAAAA,CAAS,CAAC,CAAA,CAAE,WAAA,EAAY,CAAI,EAAA,CAGjDE,CAAAA,CAAgBb,CAAAA,CAAc,SAASY,CAAO,CAAA,CAC9CE,CAAAA,CAAgB5D,CAAAA,CAAM,QAAA,CAAS,IAAI,EAGrC,CAAC2D,CAAAA,EAAiB,CAACC,CAAAA,EACnBP,CAAAA,GAER,CACJ,CAEA,OAAOD,CACX,CAeO,SAASS,CAAAA,CAAWb,CAAAA,CAAcE,CAAAA,CAAqB,CAAA,CAAW,CAErE,IAAMY,CAAAA,CAAad,CAAAA,CACd,KAAA,CAAM;AAAA,CAAI,CAAA,CACV,GAAA,CAAIH,CAAAA,EAAQA,CAAAA,CAAK,IAAA,EAAM,CAAA,CACvB,MAAA,CAAOA,CAAAA,EAAQA,CAAAA,CAAK,MAAA,CAAS,CAAC,EAC9B,IAAA,CAAK,EAAE,CAAA,CAGNpC,CAAAA,CAASsC,CAAAA,CAASe,CAAU,CAAA,CAMlC,OAHcb,CAAAA,CAAcxC,CAAAA,CAAQyC,CAAU,CAAA,CAGjC,IAAA,CAAK;AAAA,CAAI,CAC1B,CClGA,SAASa,CAAAA,CAAoBC,CAAAA,CAAuC,CAEhE,IAAMzE,CAAAA,CAAS,CACX,eAAA,CAAiByE,CAAAA,EAAY,eAAA,EAAmB,IAAA,CAChD,kBAAA,CAAoBA,CAAAA,EAAY,kBAAA,EAAsB,CAAC,IAAI,CAAA,CAC3D,QAAA,CAAUA,CAAAA,EAAY,QAAA,EAAY,OAAA,CAClC,aAAA,CAAeA,CAAAA,EAAY,aAAA,EAAiB,MAChD,CAAA,CAGIC,CAAAA,CAAc1E,EAAO,QAAA,CAGzB,OAAA0E,CAAAA,CAAcA,CAAAA,CAAY,OAAA,CAAQ,KAAA,CAAO,GAAG,CAAA,CAGxCA,CAAAA,CAAY,QAAA,CAAS,eAAe,CAAA,CAGpCA,CAAAA,CAAc,QAAA,EADMA,CAAAA,CAAY,KAAA,CAAM,eAAe,CAAA,CAAE,CAAC,CAAA,EAAK,EAAA,CAAA,CAEtDA,CAAAA,CAAY,UAAA,CAAW,IAAI,CAAA,CAElCA,CAAAA,CAAcA,CAAAA,CAAY,KAAA,CAAM,CAAC,CAAA,CAC1BA,EAAY,UAAA,CAAW,GAAG,CAAA,GAEjCA,CAAAA,CAAcA,CAAAA,CAAY,KAAA,CAAM,CAAC,CAAA,CAAA,CAIhCA,CAAAA,CAAY,QAAA,CAAS,GAAG,CAAA,GACzBA,CAAAA,EAAe,GAAA,CAAA,CAUZ,CAAA,+BAAA,EAPU,IAAA,CAAK,SAAA,CAAU,CAC5B,eAAA,CAAiB1E,CAAAA,CAAO,eAAA,CACxB,kBAAA,CAAoBA,CAAAA,CAAO,kBAAA,CAC3B,QAAA,CAAU0E,CAAAA,CACV,aAAA,CAAe1E,CAAAA,CAAO,aAC1B,CAAC,CAEgD,CAAA,IAAA,CACrD,CAcO,SAAS2E,CAAAA,CACZxB,CAAAA,CACAV,CAAAA,CACAgC,CAAAA,CACM,CACN,IAAMG,CAAAA,CAAgBzB,CAAAA,CAAW,gBAAA,EAAoBV,CAAAA,CAAW,gBAAA,CAC1DoC,CAAAA,CAAe1B,CAAAA,CAAW,eAAA,EAAmBV,CAAAA,CAAW,eAAA,EAAmB,EAAC,CAE5EqC,CAAAA,CAAaF,CAAAA,CACd,GAAA,CAAI3B,CAAAA,EAAU,CAAA,2BAAA,EAA8BA,CAAM,CAAA,WAAA,CAAa,CAAA,CAC/D,IAAA,CAAK;AAAA,CAAI,CAAA,CAER8B,EAAYF,CAAAA,CACb,GAAA,CAAIG,GAAS,CAAA,6BAAA,EAAgCA,CAAK,CAAA,IAAA,CAAM,CAAA,CACxD,IAAA,CAAK;AAAA,CAAI,CAAA,CAGRC,CAAAA,CAAcT,CAAAA,CAAoBC,CAAU,CAAA,CAG7CQ,CAAAA,EACD,OAAA,CAAQ,IAAA,CAAK,wCAAwC,CAAA,CAIzD,IAAMC,CAAAA,CAAU,CAAA;AAAA;AAAA;AAAA,EAGtB1C,CAAAA,CAAoBW,CAAAA,CAAYV,CAAU,CAAC;AAAA,EAC3CS,CAAAA,CAAuBC,EAAYV,CAAAA,CAAYU,CAAAA,CAAW,cAAgB,SAAA,CAAY,SAAA,CAAY,SAAS,CAAC;AAAA,EAC5G8B,CAAW;AAAA,EACXF,CAAS;AAAA;AAAA;AAAA;AAAA,EAITD,CAAU;AAAA;AAAA,OAAA,CAAA,CAKCI,CAAAA,CAAQ,SAAS,UAAU,CAAA,EAC5B,QAAQ,KAAA,CAAM,6CAA6C,EAI/D,IAAMC,CAAAA,CAAYb,EAAWY,CAAO,CAAA,CAGpC,OAAKC,CAAAA,CAAU,QAAA,CAAS,UAAU,CAAA,EAC9B,OAAA,CAAQ,MAAM,oDAAoD,CAAA,CAG/DA,CACX,CAQO,SAASC,EACZjC,CAAAA,CACAV,CAAAA,CACAgC,EACe,CACf,OAAO,CACH,MAAA,CAAQ,KAAA,CACR,KAAMtB,CAAAA,CAAW,IAAA,CACjB,QAAUnB,CAAAA,EAAkB,CACxB,IAAMyB,CAAAA,CAAOkB,CAAAA,CAAgBxB,CAAAA,CAAYV,CAAAA,CAAYgC,CAAU,CAAA,CAC/D,OAAOzC,CAAAA,CAAE,IAAA,CAAKyB,CAAI,CACtB,CACJ,CACJ,CCnJO,SAAS4B,GAAsB,CAClC,OAAQ,OAAe,oBAC3B,CAEO,SAASC,CAAAA,CAAoBtF,CAAAA,CAAa,CAC5C,MAAA,CAAe,oBAAA,CAAuBA,EAC3C,CA6BO,SAASuF,CAAAA,CACZC,EACAC,CAAAA,CACAhD,CAAAA,CACAiD,EACa,CAEb,GAAIA,EAAK,UAAA,CAAW,OAAO,EACvB,OAAO,CACH,OAAQF,CAAAA,CACR,OAAA,CAAS,CAAE,cAAA,CAAgB,kBAAmB,EAC9C,IAAA,CAAM,IAAA,CAAK,SAAA,CAAU,CAAE,KAAA,CAAO,CAAA,MAAA,EAASA,CAAU,CAAA,CAAG,CAAC,CACzD,CAAA,CAIJ,GAAIC,EAAa,GAAA,CAAID,CAAU,EAAG,CAC9B,IAAMG,EAAcF,CAAAA,CAAa,GAAA,CAAID,CAAU,CAAA,CAEzCf,CAAAA,CAAaY,GAAoB,CACvC,OAAA,CAAQ,IAAI,CAAA,+BAAA,EAAkCG,CAAU,cAAe,CAAC,CAACf,CAAU,CAAA,CACnF,IAAMhB,EAAOkB,CAAAA,CAAgBgB,CAAAA,CAAalD,EAAYgC,CAAU,CAAA,CAChE,OAAO,CACH,MAAA,CAAQe,EACR,OAAA,CAAS,CAAE,eAAgB,0BAA2B,CAAA,CACtD,IAAA,CAAM/B,CACV,CACJ,CAEA,eAAQ,GAAA,CAAI,CAAA,kCAAA,EAAqC+B,CAAU,CAAA,oBAAA,CAAsB,CAAA,CAE1E,CACH,MAAA,CAAQA,CAAAA,CACR,QAAS,CAAE,cAAA,CAAgB,YAAa,CAAA,CACxC,IAAA,CAAM,SAASA,CAAU,CAAA,CAC7B,CACJ,CAaO,SAASI,CAAAA,CACZH,CAAAA,CACAhD,CAAAA,CAC8C,CAC9C,OAAO,CAAC+C,CAAAA,CAAoBE,IAAiB,CACzC,IAAMG,EAAgBN,CAAAA,CAAmBC,CAAAA,CAAYC,EAAchD,CAAAA,CAAYiD,CAAI,EACnF,OAAO,IAAI,SAASG,CAAAA,CAAc,IAAA,CAAM,CACpC,MAAA,CAAQA,CAAAA,CAAc,MAAA,CACtB,OAAA,CAASA,CAAAA,CAAc,OAC3B,CAAC,CACL,CACJ,CAKO,SAASC,CAAAA,EAAwC,CACpD,OAAO,CACH,WAAY,GAAA,CACZ,KAAA,CAAO,uBACP,IAAA,CAAM,MAAA,CACN,YAAa,kDAAA,CACb,QAAA,CAAU,CAAC,KAAA,CAAO,WAAA,CAAa,OAAO,CAAA,CACtC,MAAA,CAAQ,mBACZ,CACJ,CC3DO,SAASC,CAAAA,CAAU/F,CAAAA,CAAqCgG,EAA2D,CACtH,IAAMC,EAAS,EAAC,CACVR,EAAe,IAAI,GAAA,CAGzBH,EAAoBU,CAAAA,EAAW,IAAI,EACnC,IAAMvB,CAAAA,CAAauB,CAAAA,EAAW,IAAA,CAG9B,GAAIhG,CAAAA,CAAO,OAASA,CAAAA,CAAO,KAAA,CAAM,OAAS,CAAA,CACtC,IAAA,IAAWmD,KAAcnD,CAAAA,CAAO,KAAA,CAC5BiG,EAAO,IAAA,CAAKb,CAAAA,CAAejC,EAAYnD,CAAAA,CAAQyE,CAAU,CAAC,CAAA,CAKlE,GAAIzE,EAAO,UAAA,EAAcA,CAAAA,CAAO,UAAA,CAAW,MAAA,CAAS,CAAA,CAChD,IAAA,IAAWkG,KAAmBlG,CAAAA,CAAO,UAAA,CACjCyF,EAAa,GAAA,CAAIS,CAAAA,CAAgB,WAAYA,CAAe,CAAA,CAE5DD,EAAO,IAAA,CAAKb,CAAAA,CAAec,EAAiBlG,CAAAA,CAAQyE,CAAU,CAAC,CAAA,CAKvE,GAAIzE,EAAO,kBAAA,EAAsB,CAACyF,CAAAA,CAAa,GAAA,CAAI,GAAG,CAAA,CAAG,CACrD,IAAMU,CAAAA,CAAmBL,GAAqB,CAC9CL,CAAAA,CAAa,IAAI,GAAA,CAAKU,CAAgB,EAEtCF,CAAAA,CAAO,IAAA,CAAKb,EAAee,CAAAA,CAAkBnG,CAAAA,CAAQyE,CAAU,CAAC,EACpE,CAGA,IAAM2B,CAAAA,CAAeR,EAAmBH,CAAAA,CAAczF,CAAM,EAgC5D,OA9ByD,CACrD,KAAM,eAAA,CACN,OAAA,CAAS,QAET,MAAA,CAAAiG,CAAAA,CAGA,kBAAmBG,CAAAA,CAEnB,UAAA,CAAY,MAAOC,CAAAA,EAAqB,CAEpC,GADA,OAAA,CAAQ,GAAA,CAAI,2BAA2BJ,CAAAA,CAAO,MAAM,CAAA,WAAA,CAAa,CAAA,CAC7DR,CAAAA,CAAa,IAAA,CAAO,EAAG,CACvB,IAAMa,EAAc,KAAA,CAAM,IAAA,CAAKb,EAAa,IAAA,EAAM,EAAE,IAAA,CAAK,IAAI,EAC7D,OAAA,CAAQ,GAAA,CAAI,4CAA4Ca,CAAW,CAAA,CAAE,EACzE,CACJ,CAAA,CAEA,OAAA,CAAS,MAAOC,CAAAA,EAAa,CACzB,QAAQ,GAAA,CAAI,6CAA6C,EAC7D,CAAA,CAEA,OAAA,CAAS,MAAOA,CAAAA,EAAa,CACzB,QAAQ,GAAA,CAAI,wCAAwC,EACxD,CAAA,CAEA,OAAA,CAAS,MAAOA,CAAAA,EAAa,CACzB,QAAQ,GAAA,CAAI,qDAAqD,EACrE,CACJ,CAGJ","file":"index.cjs","sourcesContent":["// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import * as types from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n export class I18nManager {\r\n\r\n // ┌──────────────────────────────── STATE ─────────────────────────────┐\r\n\r\n private translations : types.TranslationSet = {};\r\n private currentLanguage : types.LanguageCode = 'en';\r\n private defaultLanguage : types.LanguageCode = 'en';\r\n private fallbackLanguage : types.LanguageCode = 'en';\r\n private supportedLanguages = new Set<types.LanguageCode>(['en']);\r\n private rtlLanguages = new Set<string>(['ar', 'he', 'fa', 'ur', 'yi', 'ji', 'iw', 'ku']);\r\n private listeners = new Set<(lang: types.LanguageCode) => void>();\r\n private storage? : types.I18nStorage;\r\n private onLanguageChange? : (lang: types.LanguageCode) => void;\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌──────────────────────────────── INIT ──────────────────────────────┐\r\n\r\n constructor(config?: types.I18nConfig) {\r\n if (config) {\r\n this.defaultLanguage = config.defaultLanguage || 'en';\r\n this.fallbackLanguage = config.fallbackLanguage || config.defaultLanguage || 'en';\r\n this.currentLanguage = config.defaultLanguage || 'en';\r\n this.storage = config.storage;\r\n this.onLanguageChange = config.onLanguageChange;\r\n\r\n if (config.supportedLanguages) {\r\n this.supportedLanguages = new Set(config.supportedLanguages);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Initialize with stored language preference\r\n */\r\n public async init(): Promise<void> {\r\n if (this.storage) {\r\n const stored = await this.storage.get('i18n-language');\r\n if (stored && this.supportedLanguages.has(stored)) {\r\n this.currentLanguage = stored;\r\n }\r\n }\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌─────────────────────────────── LOADING ────────────────────────────┐\r\n\r\n /**\r\n * Load translations for a specific language\r\n * @param lang Language code\r\n * @param translations Translation object (can be nested)\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n public loadLanguage(lang: types.LanguageCode, translations: Record<string, any>): void {\r\n if (!this.translations[lang]) {\r\n this.translations[lang] = {};\r\n }\r\n\r\n const flattened = this.flattenObject(translations);\r\n this.translations[lang] = { ...this.translations[lang], ...flattened };\r\n this.supportedLanguages.add(lang);\r\n }\r\n\r\n /**\r\n * Load multiple languages at once\r\n * @param translations Object with language codes as keys\r\n */\r\n public loadTranslations(translations: types.TranslationSet): void {\r\n Object.entries(translations).forEach(([lang, trans]) => {\r\n this.loadLanguage(lang, trans);\r\n });\r\n }\r\n\r\n /**\r\n * Flatten nested object into dot notation\r\n * @private\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n private flattenObject(obj: Record<string, any>, prefix: string = ''): Record<string, string> {\r\n const flattened: Record<string, string> = {};\r\n\r\n for (const key in obj) {\r\n if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;\r\n\r\n const value = obj[key];\r\n const newKey = prefix ? `${prefix}.${key}` : key;\r\n\r\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\r\n Object.assign(flattened, this.flattenObject(value, newKey));\r\n } else {\r\n flattened[newKey] = String(value);\r\n }\r\n }\r\n\r\n return flattened;\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌───────────────────────────── TRANSLATION ──────────────────────────┐\r\n\r\n /**\r\n * Translate a key with parameter replacement\r\n *\r\n * @example\r\n * t('welcome.message', { name: 'John' })\r\n * // => \"Welcome, John!\"\r\n *\r\n * @param key Translation key (dot notation)\r\n * @param params Optional parameters for replacement\r\n * @returns Translated string\r\n */\r\n public t(key: string, params?: Record<string, string>): string {\r\n let translation = this.getTranslation(key);\r\n\r\n if (params) {\r\n Object.entries(params).forEach(([param, value]) => {\r\n // Check if parameter value is itself a translation key\r\n const paramValue = this.getTranslation(value, value);\r\n translation = translation.replace(\r\n new RegExp(`\\\\{${param}\\\\}`, 'g'),\r\n paramValue\r\n );\r\n });\r\n }\r\n\r\n return translation;\r\n }\r\n\r\n /**\r\n * Get raw translation without parameter replacement\r\n * @private\r\n */\r\n private getTranslation(key: string, fallback?: string): string {\r\n // Try current language\r\n if (this.translations[this.currentLanguage]?.[key]) {\r\n return this.translations[this.currentLanguage][key];\r\n }\r\n\r\n // Try fallback language\r\n if (this.fallbackLanguage !== this.currentLanguage &&\r\n this.translations[this.fallbackLanguage]?.[key]) {\r\n return this.translations[this.fallbackLanguage][key];\r\n }\r\n\r\n // Try default language\r\n if (this.defaultLanguage !== this.currentLanguage &&\r\n this.defaultLanguage !== this.fallbackLanguage &&\r\n this.translations[this.defaultLanguage]?.[key]) {\r\n return this.translations[this.defaultLanguage][key];\r\n }\r\n\r\n // Warn and return fallback\r\n console.warn(`[i18n] Translation key not found: \"${key}\" (lang: ${this.currentLanguage})`);\r\n return fallback || key;\r\n }\r\n\r\n /**\r\n * Translate with a specific language temporarily\r\n *\r\n * @param key Translation key\r\n * @param lang Language code\r\n * @param params Optional parameters\r\n */\r\n public tLang(key: string, lang: types.LanguageCode, params?: Record<string, string>): string {\r\n const original = this.currentLanguage;\r\n this.currentLanguage = lang;\r\n const result = this.t(key, params);\r\n this.currentLanguage = original;\r\n return result;\r\n }\r\n\r\n /**\r\n * Translate and parse HTML-like tags into tokens\r\n * Converts \\n or /n to line breaks\r\n *\r\n * @example\r\n * // Translation: \"Hello\\nWorld <strong>here</strong>\"\r\n * tParse('message')\r\n * // => [\r\n * // { type: 'text', content: 'Hello' },\r\n * // { type: 'tag', tag: 'br', content: '' },\r\n * // { type: 'text', content: 'World ' },\r\n * // { type: 'tag', tag: 'strong', content: 'here' }\r\n * // ]\r\n *\r\n * @param key Translation key\r\n * @param params Optional parameters\r\n * @returns Array of tokens\r\n */\r\n public tParse(key: string, params?: Record<string, string>): types.TranslationToken[] {\r\n let translation = this.t(key, params);\r\n\r\n // Convert newlines to <br> tags\r\n translation = translation.replace(/\\\\n|\\/n/g, '<br>');\r\n\r\n const tokens: types.TranslationToken[] = [];\r\n const regex = /<([a-z]+)>([^<]*)<\\/\\1>|<([a-z]+)\\s*\\/?>|([^<]+)/gi;\r\n let match;\r\n\r\n while ((match = regex.exec(translation)) !== null) {\r\n if (match[4]) {\r\n // Plain text\r\n tokens.push({ type: 'text', content: match[4] });\r\n } else if (match[1]) {\r\n // Paired tag: <strong>text</strong>\r\n tokens.push({ type: 'tag', tag: match[1], content: match[2] });\r\n } else if (match[3]) {\r\n // Self-closing: <br> or <br/>\r\n tokens.push({ type: 'tag', tag: match[3], content: '' });\r\n }\r\n }\r\n\r\n return tokens.length > 0 ? tokens : [{ type: 'text', content: translation }];\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌────────────────────────────── LANGUAGE ────────────────────────────┐\r\n\r\n /**\r\n * Set current language\r\n */\r\n public async setLanguage(lang: types.LanguageCode): Promise<void> {\r\n if (!this.supportedLanguages.has(lang)) {\r\n console.warn(`[i18n] Language \"${lang}\" not supported`);\r\n return;\r\n }\r\n\r\n this.currentLanguage = lang;\r\n\r\n // Persist if storage available\r\n if (this.storage) {\r\n await this.storage.set('i18n-language', lang);\r\n }\r\n\r\n // Notify listeners\r\n this.listeners.forEach(fn => fn(lang));\r\n\r\n if (this.onLanguageChange) {\r\n this.onLanguageChange(lang);\r\n }\r\n }\r\n\r\n /**\r\n * Get current language\r\n */\r\n public getLanguage(): types.LanguageCode {\r\n return this.currentLanguage;\r\n }\r\n\r\n /**\r\n * Get all supported languages\r\n */\r\n public getSupportedLanguages(): types.LanguageCode[] {\r\n return Array.from(this.supportedLanguages);\r\n }\r\n\r\n /**\r\n * Check if language is supported\r\n */\r\n public isLanguageSupported(lang: types.LanguageCode): boolean {\r\n return this.supportedLanguages.has(lang);\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌─────────────────────────────── HELPERS ────────────────────────────┐\r\n\r\n /**\r\n * Check if a translation key exists\r\n */\r\n public hasKey(key: string): boolean {\r\n return !!(\r\n this.translations[this.currentLanguage]?.[key] ||\r\n this.translations[this.fallbackLanguage]?.[key] ||\r\n this.translations[this.defaultLanguage]?.[key]\r\n );\r\n }\r\n\r\n /**\r\n * Get all translations for current language\r\n */\r\n public getTranslations(): Record<string, string> {\r\n return this.translations[this.currentLanguage] || {};\r\n }\r\n\r\n /**\r\n * Check if current language is RTL\r\n */\r\n public isRTL(): boolean {\r\n return this.rtlLanguages.has(this.currentLanguage.toLowerCase().substring(0, 2));\r\n }\r\n\r\n /**\r\n * Check if specific language is RTL\r\n */\r\n public isRTLLanguage(lang: types.LanguageCode): boolean {\r\n return this.rtlLanguages.has(lang.toLowerCase().substring(0, 2));\r\n }\r\n\r\n /**\r\n * Subscribe to language changes\r\n * @returns Unsubscribe function\r\n */\r\n public onChange(callback: (lang: types.LanguageCode) => void): () => void {\r\n this.listeners.add(callback);\r\n return () => this.listeners.delete(callback);\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n /**\r\n * Browser storage adapter using localStorage\r\n */\r\n export const browserStorage: types.I18nStorage = {\r\n get: (key: string) => {\r\n if (typeof localStorage === 'undefined') return null;\r\n return localStorage.getItem(key);\r\n },\r\n set: (key: string, value: string) => {\r\n if (typeof localStorage !== 'undefined') {\r\n localStorage.setItem(key, value);\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Memory storage adapter (for server/testing)\r\n */\r\n export const memoryStorage = (() => {\r\n const store = new Map<string, string>();\r\n return {\r\n get: (key: string) => store.get(key) || null,\r\n set: (key: string, value: string) => { store.set(key, value); }\r\n };\r\n })();\r\n\r\n /**\r\n * Fetch translations from URLs\r\n * Works in both browser and Node.js (with node-fetch)\r\n */\r\n export async function fetchTranslations(\r\n urls: string | string[],\r\n manager: I18nManager\r\n ): Promise<void> {\r\n const urlList = Array.isArray(urls) ? urls : [urls];\r\n const translations: types.TranslationSet = {};\r\n\r\n for (const url of urlList) {\r\n try {\r\n const response = await fetch(url);\r\n if (response.ok) {\r\n const data = await response.json();\r\n // Extract language from filename: /path/en.json -> en\r\n const langMatch = url.match(/([a-z]{2,3})\\.json$/i);\r\n const lang = langMatch ? langMatch[1].toLowerCase() : 'en';\r\n translations[lang] = data;\r\n }\r\n } catch (error) {\r\n console.warn(`[i18n] Failed to fetch: ${url}`, error);\r\n }\r\n }\r\n\r\n if (Object.keys(translations).length > 0) {\r\n manager.loadTranslations(translations);\r\n }\r\n }\r\n\r\n /**\r\n * Lazy loader: fetch language only when needed\r\n * Prevents loading all languages at startup\r\n */\r\n export class LazyLoader {\r\n private baseUrl: string;\r\n private manager: I18nManager;\r\n private loading = new Map<types.LanguageCode, Promise<void>>();\r\n private loaded = new Set<types.LanguageCode>();\r\n private isServerSide: boolean;\r\n private fileExtension: string;\r\n\r\n constructor(baseUrl: string, manager: I18nManager, fileExtension: string = 'json') {\r\n this.baseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';\r\n this.manager = manager;\r\n this.fileExtension = fileExtension;\r\n this.isServerSide = typeof fetch === 'undefined';\r\n }\r\n\r\n /**\r\n * Load a language file on-demand\r\n * Caches the promise to prevent duplicate requests\r\n */\r\n async load(lang: types.LanguageCode): Promise<void> {\r\n // Already loaded\r\n if (this.loaded.has(lang)) {\r\n return;\r\n }\r\n\r\n // Currently loading\r\n if (this.loading.has(lang)) {\r\n return this.loading.get(lang);\r\n }\r\n\r\n // Start loading\r\n const promise = this.doLoad(lang);\r\n this.loading.set(lang, promise);\r\n\r\n try {\r\n await promise;\r\n this.loaded.add(lang);\r\n } finally {\r\n this.loading.delete(lang);\r\n }\r\n }\r\n\r\n private async doLoad(lang: types.LanguageCode): Promise<void> {\r\n try {\r\n const filePath = `${this.baseUrl}${lang}.${this.fileExtension}`;\r\n\r\n let data: Record<string, string> | null;\r\n\r\n if (this.isServerSide) {\r\n // Node.js: Read from filesystem\r\n data = await this.loadFromFile(filePath);\r\n } else {\r\n // Browser: Fetch from URL\r\n data = await this.loadFromUrl(filePath);\r\n }\r\n\r\n if (data) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n this.manager.loadLanguage(lang, data as Record<string, any>);\r\n }\r\n } catch (error) {\r\n console.warn(`[i18n] Error loading language: ${lang}`, error);\r\n }\r\n }\r\n\r\n private async loadFromUrl(url: string): Promise<Record<string, string> | null> {\r\n try {\r\n const response = await fetch(url);\r\n\r\n if (response.ok) {\r\n return await response.json();\r\n } else {\r\n console.warn(`[i18n] Failed to load language from URL: ${url} (${response.status})`);\r\n return null;\r\n }\r\n } catch (error) {\r\n console.warn(`[i18n] Error fetching from URL: ${url}`, error);\r\n return null;\r\n }\r\n }\r\n\r\n private async loadFromFile(filePath: string): Promise<Record<string, string> | null> {\r\n try {\r\n // Dynamic import to avoid issues in browsers\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const fs = await import('fs').then(m => m.promises).catch((): any => null);\r\n\r\n if (!fs) {\r\n console.warn('[i18n] fs module not available. Running in browser?');\r\n return null;\r\n }\r\n\r\n const content = await fs.readFile(filePath, 'utf-8');\r\n return JSON.parse(content);\r\n } catch (error) {\r\n console.warn(`[i18n] Error reading file: ${filePath}`, error);\r\n return null;\r\n }\r\n }\r\n\r\n isLoaded(lang: types.LanguageCode): boolean {\r\n return this.loaded.has(lang);\r\n }\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n let instance: I18nManager | null = null;\r\n\r\n /**\r\n * Get or create the global i18n instance\r\n */\r\n export function getI18n(): I18nManager {\r\n if (!instance) {\r\n instance = new I18nManager();\r\n }\r\n return instance;\r\n }\r\n\r\n /**\r\n * Initialize i18n with config\r\n * Call this once at app startup\r\n *\r\n * @example\r\n * // Load only default language at startup\r\n * await setupI18n({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr', 'de']\r\n * });\r\n */\r\n export async function setupI18n(config: types.I18nConfig): Promise<I18nManager> {\r\n instance = new I18nManager(config);\r\n await instance.init();\r\n return instance;\r\n }\r\n\r\n /**\r\n * Create a lazy loader for translations\r\n * Only loads languages when needed\r\n *\r\n * @example\r\n * const loader = createLazyLoader('https://mycdn.com/i18n/');\r\n *\r\n * // Later, when user switches language:\r\n * await loader.load('ar');\r\n * await setLanguage('ar');\r\n */\r\n export function createLazyLoader(baseUrl: string, fileExtension: string = 'json'): LazyLoader {\r\n return new LazyLoader(baseUrl, getI18n(), fileExtension);\r\n }\r\n\r\n /**\r\n * Setup i18n with lazy loading\r\n * Only loads the default language at startup\r\n *\r\n * @example\r\n * // Browser\r\n * const loader = await setupLazy({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr', 'de', 'zh'],\r\n * basePath: '/i18n/'\r\n * });\r\n *\r\n * // Later when user switches:\r\n * await loader.load('ar');\r\n * await setLanguage('ar');\r\n */\r\n export async function setupLazy(config: types.I18nConfig & { basePath?: string; baseUrl?: string }): Promise<LazyLoader> {\r\n const manager = new I18nManager(config);\r\n await manager.init();\r\n instance = manager;\r\n\r\n const basePath = config.basePath || config.baseUrl || './locales/';\r\n const fileExtension = config.fileExtension || 'json';\r\n const loader = new LazyLoader(basePath, manager, fileExtension);\r\n\r\n // Load only the current language at startup\r\n await loader.load(manager.getLanguage());\r\n\r\n return loader;\r\n }\r\n\r\n /**\r\n * Auto-setup: Smart initialization based on environment and config\r\n * Automatically handles browser vs server and file vs URL loading\r\n *\r\n * @example\r\n * // Browser: Auto-fetches from server\r\n * const loader = await setupAuto({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr'],\r\n * basePath: 'http://localhost:3000/static/i18n/'\r\n * });\r\n *\r\n * // Server (Node.js): Auto-reads from filesystem\r\n * const loader = await setupAuto({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr'],\r\n * basePath: './public/locales/'\r\n * });\r\n */\r\n export async function setupAuto(config: types.I18nConfig & { basePath: string }): Promise<LazyLoader> {\r\n const manager = new I18nManager(config);\r\n await manager.init();\r\n instance = manager;\r\n\r\n const fileExtension = config.fileExtension || 'json';\r\n const loader = new LazyLoader(config.basePath, manager, fileExtension);\r\n\r\n // Auto-load default language\r\n await loader.load(manager.getLanguage());\r\n\r\n return loader;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n export const t = (key: string, params?: Record<string, string>) =>\r\n getI18n().t(key, params);\r\n\r\n export const tLang = (key: string, lang: types.LanguageCode, params?: Record<string, string>) =>\r\n getI18n().tLang(key, lang, params);\r\n\r\n export const tParse = (key: string, params?: Record<string, string>) =>\r\n getI18n().tParse(key, params);\r\n\r\n export const setLanguage = (lang: types.LanguageCode) =>\r\n getI18n().setLanguage(lang);\r\n\r\n export const getLanguage = () =>\r\n getI18n().getLanguage();\r\n\r\n export const getSupportedLanguages = () =>\r\n getI18n().getSupportedLanguages();\r\n\r\n export const hasKey = (key: string) =>\r\n getI18n().hasKey(key);\r\n\r\n export const isRTL = () =>\r\n getI18n().isRTL();\r\n\r\n export const isRTLLanguage = (lang: types.LanguageCode) =>\r\n getI18n().isRTLLanguage(lang);\r\n\r\n export const onChange = (callback: (lang: types.LanguageCode) => void) =>\r\n getI18n().onChange(callback);\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n export const loadLanguage = (lang: types.LanguageCode, translations: Record<string, any>) =>\r\n getI18n().loadLanguage(lang, translations);\r\n\r\n export const loadTranslations = (translations: types.TranslationSet) =>\r\n getI18n().loadTranslations(translations);\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate page title with proper RTL handling\r\n *\r\n * @example\r\n * // English: \"Profile - MyApp\"\r\n * // Arabic: \"MyApp - الملف الشخصي\"\r\n * genPageTitle('profile', 'page.')\r\n */\r\n export function genPageTitle(key: string, prefix: string = 'page.'): string {\r\n const appName = t('app.name');\r\n const pageName = t(prefix + key);\r\n return isRTL() ? `${appName} - ${pageName}` : `${pageName} - ${appName}`;\r\n }\r\n\r\n /**\r\n * Pluralization helper\r\n *\r\n * @example\r\n * plural(1, 'item.single', 'item.plural') // \"1 item\"\r\n * plural(5, 'item.single', 'item.plural') // \"5 items\"\r\n */\r\n export function plural(count: number, singleKey: string, pluralKey: string): string {\r\n const key = count === 1 ? singleKey : pluralKey;\r\n return t(key, { count: String(count) });\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n export default {\r\n I18nManager,\r\n getI18n,\r\n setupI18n,\r\n createLazyLoader,\r\n setupLazy,\r\n setupAuto,\r\n browserStorage,\r\n memoryStorage,\r\n fetchTranslations,\r\n genPageTitle,\r\n plural,\r\n };\r\n\r\n\r\n export type I18nManagerInstance = InstanceType<typeof I18nManager>;\r\n export type LazyLoaderInstance = InstanceType<typeof LazyLoader>;\r\n\r\n export * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝","// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { t, genPageTitle } from '@minejs/i18n';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Detect and translate meta tag values\r\n *\r\n * Smart detection for:\r\n * - Direct string: 'My Title' → 'My Title'\r\n * - Translation key: 'my.key' → t('my.key')\r\n * - Array with key + fallback: ['my.key', 'Fallback'] → t('my.key', fallback)\r\n * - Array with key only: ['my.key'] → t('my.key')\r\n *\r\n * Returns the translated value or original string\r\n */\r\n function resolveMetaValue(value: string | string[] | undefined, defaultValue: string = ''): string {\r\n if (!value) return defaultValue;\r\n\r\n // Array format: [translationKey] or [translationKey, fallback]\r\n if (Array.isArray(value)) {\r\n const [key, fallback] = value;\r\n if (!key) return fallback || defaultValue;\r\n try {\r\n return t(key) || fallback || key || defaultValue;\r\n } catch {\r\n return fallback || key || defaultValue;\r\n }\r\n }\r\n\r\n // String format: could be direct value or translation key\r\n // Try to translate first, if it fails/returns same value, treat as direct string\r\n try {\r\n const translated = t(value);\r\n return translated || value;\r\n } catch {\r\n return value;\r\n }\r\n }\r\n\r\n /**\r\n * Resolve keywords with smart translation detection\r\n *\r\n * Keywords can be:\r\n * - Direct string: 'keyword' → 'keyword' (NO translation)\r\n * - Translation array: ['meta.key'] or ['meta.key', 'fallback'] → translate it\r\n *\r\n * This allows keywords: ['404', 'error', 'not found'] without translation\r\n * And also: [['meta.keywords.error'], ['meta.keywords.notfound']] with translation\r\n */\r\n function resolveKeywords(keywords: (string | string[])[] | undefined): string {\r\n if (!keywords || keywords.length === 0) return '';\r\n\r\n const resolved = keywords\r\n .map(kw => {\r\n // String format: use as-is, don't translate\r\n if (typeof kw === 'string') {\r\n return kw;\r\n }\r\n\r\n // Array format: translate the keyword\r\n if (Array.isArray(kw)) {\r\n const [key, fallback] = kw;\r\n if (!key) return fallback || '';\r\n try {\r\n return t(key) || fallback || key;\r\n } catch {\r\n return fallback || key;\r\n }\r\n }\r\n\r\n return '';\r\n })\r\n .filter(Boolean);\r\n\r\n return resolved.join(', ');\r\n }\r\n\r\n /**\r\n * Generate page title with translation support\r\n *\r\n * Uses genPageTitle() from @minejs/i18n for RTL-aware titles\r\n * Detects if value is a translation key and uses it accordingly\r\n */\r\n function resolvePageTitle(title: string | string[] | undefined): string {\r\n if (!title) return 'Page';\r\n\r\n let titleKey = '';\r\n\r\n // Extract translation key if array\r\n if (Array.isArray(title)) {\r\n titleKey = title[0] || '';\r\n } else {\r\n titleKey = title;\r\n }\r\n\r\n // Use genPageTitle for RTL-aware title generation\r\n try {\r\n return genPageTitle(titleKey, '');\r\n } catch {\r\n // Fallback: if genPageTitle fails, use direct value\r\n if (Array.isArray(title)) {\r\n return title[1] || title[0] || 'Page';\r\n }\r\n return title;\r\n }\r\n }\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals and translation support\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n * - Translation support for all meta values\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n\r\n // Resolve translated values\r\n const title = resolvePageTitle(config.title);\r\n const description = resolveMetaValue(config.description, baseConfig.defaultDescription || 'A modern single-page application');\r\n const keywords = resolveKeywords(config.keywords || baseConfig.defaultKeywords);\r\n const expertise = resolveMetaValue(config.expertise, '');\r\n const experience = resolveMetaValue(config.experience, '');\r\n const authority = resolveMetaValue(config.authority, '');\r\n\r\n // Determine OG type based on content type\r\n const ogType = config.contentType === 'article' ? 'article' : 'website';\r\n\r\n return `<!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${title}</title>\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${expertise ? `<meta name=\"expertise\" content=\"${expertise}\" />` : ''}\r\n ${experience ? `<meta name=\"experience\" content=\"${experience}\" />` : ''}\r\n ${authority ? `<meta name=\"authority\" content=\"${authority}\" />` : ''}\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n')}\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n <!-- 📘 Open Graph Protocol (Social Media) -->\r\n <meta property=\"og:type\" content=\"${ogType}\" />\r\n <meta property=\"og:title\" content=\"${title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />\r\n <meta property=\"og:locale\" content=\"en_US\" />\r\n ${baseConfig.author ? `<meta property=\"og:site_name\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image:alt\" content=\"${title}\" />` : ''}\r\n `;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n * Handles translation keys in meta values\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n // Resolve translated values for structured data\r\n const title = resolvePageTitle(pageConfig.title);\r\n const description = resolveMetaValue(pageConfig.description, baseConfig.defaultDescription);\r\n const expertise = resolveMetaValue(pageConfig.expertise, '');\r\n const experience = resolveMetaValue(pageConfig.experience, '');\r\n const authority = resolveMetaValue(pageConfig.authority, '');\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': title,\r\n 'url': canonicalUrl,\r\n 'description': description,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...((expertise || experience || authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(expertise && { 'expertise': expertise }),\r\n ...(experience && { 'experience': experience }),\r\n ...(authority && { 'authority': authority })\r\n }\r\n })\r\n };\r\n\r\n const jsonStr = JSON.stringify(schema, null, 2)\r\n .split('\\n')\r\n .map(line => line ? ` ${line}` : line)\r\n .join('\\n');\r\n return `<script type=\"application/ld+json\">\\n${jsonStr}\\n</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/htmlFormatter.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];\r\n\r\n interface Token {\r\n type: 'tag' | 'text' | 'comment';\r\n value: string;\r\n }\r\n\r\n /**\r\n * Tokenize HTML into tags, text, and comments\r\n */\r\n function tokenize(html: string): Token[] {\r\n const tokens: Token[] = [];\r\n const regex = /(<[^>]+>)|([^<]+)/g;\r\n let match;\r\n\r\n while ((match = regex.exec(html)) !== null) {\r\n if (match[1]) {\r\n // It's a tag\r\n tokens.push({ type: 'tag', value: match[1] });\r\n } else if (match[2] && match[2].trim().length > 0) {\r\n // It's text content (not just whitespace)\r\n tokens.push({ type: 'text', value: match[2] });\r\n }\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Process tokens and apply indentation\r\n */\r\n function processTokens(tokens: Token[], indentSize: number = 4): string[] {\r\n const indent = ' '.repeat(indentSize);\r\n const output: string[] = [];\r\n let indentLevel = 0;\r\n\r\n // eslint-disable-next-line @typescript-eslint/prefer-for-of\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n const value = token.value.trim();\r\n\r\n // Decrease indent for closing tags BEFORE adding\r\n if (value.startsWith('</')) {\r\n indentLevel = Math.max(0, indentLevel - 1);\r\n }\r\n\r\n const padding = indent.repeat(indentLevel);\r\n\r\n // Skip empty text nodes\r\n if (token.type === 'text' && value.length === 0) {\r\n continue;\r\n }\r\n\r\n // Add to output\r\n if (token.type === 'text') {\r\n // Text nodes - check if previous line exists\r\n if (output.length > 0 && !output[output.length - 1].trim().endsWith('>')) {\r\n // Append to previous line if it doesn't end with tag\r\n output[output.length - 1] += value;\r\n } else {\r\n output.push(padding + value);\r\n }\r\n } else {\r\n // Tags and comments\r\n output.push(padding + value);\r\n }\r\n\r\n // Increase indent for OPENING tags ONLY (not closing, not void elements, not DOCTYPE/comments)\r\n if (value.startsWith('<') && !value.startsWith('</')) {\r\n // Skip DOCTYPE and comments - they don't increase indent\r\n if (value.startsWith('<!DOCTYPE') || value.startsWith('<!--')) {\r\n continue;\r\n }\r\n\r\n // Extract tag name\r\n const tagMatch = value.match(/<([A-Za-z][A-Za-z0-9\\\\-]*)/i);\r\n const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';\r\n\r\n // Check if it's a void/self-closing element\r\n const isVoidElement = VOID_ELEMENTS.includes(tagName);\r\n const isSelfClosing = value.endsWith('/>');\r\n\r\n // Only increase indent if it's NOT a void element AND NOT self-closing\r\n if (!isVoidElement && !isSelfClosing) {\r\n indentLevel++;\r\n }\r\n }\r\n }\r\n\r\n return output;\r\n }\r\n\r\n /**\r\n * Format HTML with proper indentation and cleanup\r\n *\r\n * - Tokenizes HTML into tags and content\r\n * - Applies proper indentation (4 spaces per level)\r\n * - Handles void elements correctly\r\n * - Preserves script and style tag content\r\n * - Maintains tag hierarchy\r\n *\r\n * @param html Raw HTML string\r\n * @param indentSize Number of spaces per indent level (default: 4)\r\n * @returns Formatted HTML with perfect indentation\r\n */\r\n export function formatHTML(html: string, indentSize: number = 4): string {\r\n // Remove existing whitespace and normalize\r\n const normalized = html\r\n .split('\\n')\r\n .map(line => line.trim())\r\n .filter(line => line.length > 0)\r\n .join('');\r\n\r\n // Tokenize the HTML\r\n const tokens = tokenize(normalized);\r\n\r\n // Process tokens with indentation\r\n const lines = processTokens(tokens, indentSize);\r\n\r\n // Join and return\r\n return lines.join('\\n');\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext, AppConfig } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n import { formatHTML } from './htmlFormatter';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate i18n meta tag for client injection\r\n *\r\n * The browser.tsx template will read this meta tag and inject\r\n * i18n config into the ClientManager config automatically.\r\n *\r\n * Convert server-side filesystem paths to browser-accessible URLs:\r\n * ./src/shared/static/dist/i18n → static/dist/i18n/\r\n *\r\n * Note: NO leading slash - i18n library needs to fetch as URL (not file path)\r\n * Always returns a meta tag - never empty!\r\n */\r\n function generateI18nMetaTag(i18nConfig: AppConfig['i18n']): string {\r\n // Provide defaults if config is missing or incomplete\r\n const config = {\r\n defaultLanguage: i18nConfig?.defaultLanguage || 'en',\r\n supportedLanguages: i18nConfig?.supportedLanguages || ['en'],\r\n basePath: i18nConfig?.basePath || 'i18n/',\r\n fileExtension: i18nConfig?.fileExtension || 'json'\r\n };\r\n\r\n // Convert server-side path to browser URL\r\n let browserPath = config.basePath;\r\n\r\n // Convert path separators to forward slashes\r\n browserPath = browserPath.replace(/\\\\/g, '/');\r\n\r\n // Handle server-side paths like ./src/shared/static/dist/i18n\r\n if (browserPath.includes('shared/static')) {\r\n // Extract everything after \"shared/static\" and prepend static/\r\n const afterStatic = browserPath.split('shared/static')[1] || '';\r\n browserPath = 'static' + afterStatic;\r\n } else if (browserPath.startsWith('./')) {\r\n // Remove leading ./\r\n browserPath = browserPath.slice(2);\r\n } else if (browserPath.startsWith('/')) {\r\n // Remove leading /\r\n browserPath = browserPath.slice(1);\r\n }\r\n\r\n // Ensure it ends with /\r\n if (!browserPath.endsWith('/')) {\r\n browserPath += '/';\r\n }\r\n\r\n const i18nJson = JSON.stringify({\r\n defaultLanguage: config.defaultLanguage,\r\n supportedLanguages: config.supportedLanguages,\r\n basePath: browserPath,\r\n fileExtension: config.fileExtension\r\n });\r\n\r\n return `<meta name=\"app-i18n\" content='${i18nJson}' />`;\r\n }\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - i18n meta tag (read by browser.tsx template)\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n *\r\n * Output is automatically formatted with proper indentation via formatHTML()\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => `<script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => `<link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n // Add i18n meta tag for browser.tsx to read\r\n const i18nMetaTag = generateI18nMetaTag(i18nConfig);\r\n\r\n // DEBUG: Log to see what's being generated\r\n if (!i18nMetaTag) {\r\n console.warn('[SPA] WARNING: i18n meta tag is empty!');\r\n }\r\n\r\n // Build raw HTML - GUARANTEE i18n meta tag is included!\r\n const rawHTML = `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n${generateSEOMetaTags(pageConfig, baseConfig)}\r\n${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n${i18nMetaTag}\r\n${styleTags}\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n\r\n // DEBUG: Check if i18n meta tag is in raw HTML\r\n if (!rawHTML.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag NOT in raw HTML!');\r\n }\r\n\r\n // Format the HTML with proper indentation\r\n const formatted = formatHTML(rawHTML);\r\n\r\n // DEBUG: Check if i18n meta tag made it through formatting\r\n if (!formatted.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag LOST during formatting!');\r\n }\r\n\r\n return formatted;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data and i18n meta tag\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig, i18nConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n // Get global i18n config (set by serverSPA plugin)\r\n export function getGlobalI18nConfig() {\r\n return (global as any).__cruxjs_i18n_config;\r\n }\r\n\r\n export function setGlobalI18nConfig(config: any) {\r\n (global as any).__cruxjs_i18n_config = config;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n *\r\n * Returns HTML for web requests and JSON for API requests\r\n * Automatically uses global i18n config (no need to pass it!)\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n // Use global i18n config - unified approach!\r\n const i18nConfig = getGlobalI18nConfig();\r\n console.log(`[Errors] Generating error page ${statusCode} with i18n:`, !!i18nConfig);\r\n const html = generateSPAHTML(errorConfig, baseConfig, i18nConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n console.log(`[Errors] No custom error page for ${statusCode}, returning fallback`);\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n *\r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n *\r\n * No need to pass i18nConfig - uses global!\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page, setGlobalI18nConfig } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * Features:\r\n * - Server-side rendering with full SEO support (meta tags, structured data)\r\n * - Automatic error page handling (404, 500, etc.)\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl : 'https://example.com',\r\n * clientEntry : './src/client/browser.tsx',\r\n * clientScriptPath : ['/static/dist/js/browser.js'],\r\n * clientStylePath : ['/static/dist/css/index.css'],\r\n * enableAutoNotFound : true,\r\n * pages: [\r\n * {\r\n * title : 'Home',\r\n * path : '/',\r\n * description : 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode : 404,\r\n * title : '404 - Not Found',\r\n * path : '/404',\r\n * description : 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig, appConfig?: any): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Store i18n config globally so ALL handlers can access it (unified!)\r\n setGlobalI18nConfig(appConfig?.i18n);\r\n const i18nConfig = appConfig?.i18n;\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config, i18nConfig));\r\n }\r\n\r\n // Create error handler function (uses global i18nConfig automatically!)\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/seo.ts","../src/utils/htmlFormatter.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["parseValue","value","defaultValue","t","resolveMetaValue","resolveKeywords","keywords","kw","resolvePageTitle","title","parsedTitle","genPageTitle","val","appName","isRTL","generateSEOMetaTags","config","baseConfig","canonicalUrl","robots","description","expertise","experience","authority","ogType","script","generateStructuredData","pageConfig","contentType","schema","line","VOID_ELEMENTS","tokenize","html","tokens","regex","match","processTokens","indentSize","indent","output","indentLevel","token","padding","tagMatch","tagName","isVoidElement","isSelfClosing","formatHTML","normalized","generateI18nMetaTag","i18nConfig","browserPath","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","i18nMetaTag","rawHTML","formatted","createSPARoute","c","getGlobalI18nConfig","setGlobalI18nConfig","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","appConfig","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.cjs","sourcesContent":["// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { t } from '@minejs/server';\r\n import { isRTL } from '@minejs/i18n';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Smart parser for string values\r\n *\r\n * Intelligently detects if a string is a translation key or plain text:\r\n * - Translation key format (e.g., 'meta.home.title'): must contain dots, attempts translation via t(key)\r\n * - Plain text (e.g., 'My Title', 'cruxjs', 'framework'): returns as-is\r\n *\r\n * Key format: MUST contain at least one dot (.) to be considered a translation key\r\n * Examples:\r\n * - 'meta.home.title' → translation key → tries t('meta.home.title')\r\n * - 'cruxjs' → plain text → returns 'cruxjs'\r\n * - 'My Title' → plain text → returns 'My Title'\r\n *\r\n * @param value - The string to parse\r\n * @param defaultValue - Fallback if value is empty\r\n * @returns Translated value or original string\r\n */\r\n function parseValue(value: string | undefined, defaultValue: string = ''): string {\r\n if (!value) return defaultValue;\r\n\r\n // Check if value looks like a translation key (e.g., 'meta.home.title')\r\n // Key pattern: MUST contain at least one dot AND be lowercase/numeric/underscores/dots\r\n const isTranslationKey = value.includes('.') && /^[a-z0-9._]+$/.test(value);\r\n\r\n if (isTranslationKey) {\r\n try {\r\n const translated = t(value);\r\n // If translation returns a value, use it; otherwise fall back to original\r\n return translated || value;\r\n } catch {\r\n // If translation fails, return the original value\r\n return value;\r\n }\r\n }\r\n\r\n // Not a translation key format, return as direct text\r\n return value;\r\n }\r\n\r\n /**\r\n * Resolve meta tag values with smart translation detection\r\n * Uses parseValue() to automatically detect and translate\r\n */\r\n function resolveMetaValue(value: string | undefined, defaultValue: string = ''): string {\r\n return parseValue(value, defaultValue);\r\n }\r\n\r\n /**\r\n * Resolve keywords with smart translation detection\r\n * Uses parseValue() on each keyword to automatically detect and translate\r\n */\r\n function resolveKeywords(keywords: string[] | undefined): string {\r\n if (!keywords || keywords.length === 0) return '';\r\n\r\n const resolved = keywords\r\n .map(kw => parseValue(kw))\r\n .filter(Boolean);\r\n\r\n return resolved.join(', ');\r\n }\r\n\r\n /**\r\n * Generate page title with translation support\r\n *\r\n * Uses genPageTitle() from @minejs/i18n for RTL-aware titles\r\n * First parses the value to detect and translate if needed\r\n */\r\n function resolvePageTitle(title: string | undefined): string {\r\n if (!title) return 'Page';\r\n\r\n // First parse the value to detect translation keys\r\n const parsedTitle = parseValue(title);\r\n\r\n // Use genPageTitle for RTL-aware title generation\r\n try {\r\n return genPageTitle(parsedTitle);\r\n } catch {\r\n // Fallback: if genPageTitle fails, use parsed value\r\n return parsedTitle;\r\n }\r\n }\r\n\r\n /**\r\n * Generate page title with proper RTL handling\r\n *\r\n * @example\r\n * // English: \"Profile - MyApp\"\r\n * // Arabic: \"MyApp - الملف الشخصي\"\r\n */\r\n export function genPageTitle(val: string): string {\r\n const appName = t('app.name');\r\n return isRTL() ? `${appName} - ${val}` : `${val} - ${appName}`;\r\n }\r\n\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals and translation support\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n * - Translation support for all meta values\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n\r\n // Resolve translated values\r\n const title = resolvePageTitle(config.title);\r\n const description = resolveMetaValue(config.description, baseConfig.defaultDescription || 'A modern single-page application');\r\n const keywords = resolveKeywords(config.keywords || baseConfig.defaultKeywords);\r\n const expertise = resolveMetaValue(config.expertise, '');\r\n const experience = resolveMetaValue(config.experience, '');\r\n const authority = resolveMetaValue(config.authority, '');\r\n\r\n // Determine OG type based on content type\r\n const ogType = config.contentType === 'article' ? 'article' : 'website';\r\n\r\n return `<!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${title}</title>\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${expertise ? `<meta name=\"expertise\" content=\"${expertise}\" />` : ''}\r\n ${experience ? `<meta name=\"experience\" content=\"${experience}\" />` : ''}\r\n ${authority ? `<meta name=\"authority\" content=\"${authority}\" />` : ''}\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n')}\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n <!-- 📘 Open Graph Protocol (Social Media) -->\r\n <meta property=\"og:type\" content=\"${ogType}\" />\r\n <meta property=\"og:title\" content=\"${title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />\r\n <meta property=\"og:locale\" content=\"en_US\" />\r\n ${baseConfig.author ? `<meta property=\"og:site_name\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image:alt\" content=\"${title}\" />` : ''}\r\n `;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n * Handles translation keys in meta values\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n // Resolve translated values for structured data\r\n const title = resolvePageTitle(pageConfig.title);\r\n const description = resolveMetaValue(pageConfig.description, baseConfig.defaultDescription);\r\n const expertise = resolveMetaValue(pageConfig.expertise, '');\r\n const experience = resolveMetaValue(pageConfig.experience, '');\r\n const authority = resolveMetaValue(pageConfig.authority, '');\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': title,\r\n 'url': canonicalUrl,\r\n 'description': description,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...((expertise || experience || authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(expertise && { 'expertise': expertise }),\r\n ...(experience && { 'experience': experience }),\r\n ...(authority && { 'authority': authority })\r\n }\r\n })\r\n };\r\n\r\n const jsonStr = JSON.stringify(schema, null, 2)\r\n .split('\\n')\r\n .map(line => line ? ` ${line}` : line)\r\n .join('\\n');\r\n return `<script type=\"application/ld+json\">\\n${jsonStr}\\n</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/htmlFormatter.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];\r\n\r\n interface Token {\r\n type: 'tag' | 'text' | 'comment';\r\n value: string;\r\n }\r\n\r\n /**\r\n * Tokenize HTML into tags, text, and comments\r\n */\r\n function tokenize(html: string): Token[] {\r\n const tokens: Token[] = [];\r\n const regex = /(<[^>]+>)|([^<]+)/g;\r\n let match;\r\n\r\n while ((match = regex.exec(html)) !== null) {\r\n if (match[1]) {\r\n // It's a tag\r\n tokens.push({ type: 'tag', value: match[1] });\r\n } else if (match[2] && match[2].trim().length > 0) {\r\n // It's text content (not just whitespace)\r\n tokens.push({ type: 'text', value: match[2] });\r\n }\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Process tokens and apply indentation\r\n */\r\n function processTokens(tokens: Token[], indentSize: number = 4): string[] {\r\n const indent = ' '.repeat(indentSize);\r\n const output: string[] = [];\r\n let indentLevel = 0;\r\n\r\n // eslint-disable-next-line @typescript-eslint/prefer-for-of\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n const value = token.value.trim();\r\n\r\n // Decrease indent for closing tags BEFORE adding\r\n if (value.startsWith('</')) {\r\n indentLevel = Math.max(0, indentLevel - 1);\r\n }\r\n\r\n const padding = indent.repeat(indentLevel);\r\n\r\n // Skip empty text nodes\r\n if (token.type === 'text' && value.length === 0) {\r\n continue;\r\n }\r\n\r\n // Add to output\r\n if (token.type === 'text') {\r\n // Text nodes - check if previous line exists\r\n if (output.length > 0 && !output[output.length - 1].trim().endsWith('>')) {\r\n // Append to previous line if it doesn't end with tag\r\n output[output.length - 1] += value;\r\n } else {\r\n output.push(padding + value);\r\n }\r\n } else {\r\n // Tags and comments\r\n output.push(padding + value);\r\n }\r\n\r\n // Increase indent for OPENING tags ONLY (not closing, not void elements, not DOCTYPE/comments)\r\n if (value.startsWith('<') && !value.startsWith('</')) {\r\n // Skip DOCTYPE and comments - they don't increase indent\r\n if (value.startsWith('<!DOCTYPE') || value.startsWith('<!--')) {\r\n continue;\r\n }\r\n\r\n // Extract tag name\r\n const tagMatch = value.match(/<([A-Za-z][A-Za-z0-9\\\\-]*)/i);\r\n const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';\r\n\r\n // Check if it's a void/self-closing element\r\n const isVoidElement = VOID_ELEMENTS.includes(tagName);\r\n const isSelfClosing = value.endsWith('/>');\r\n\r\n // Only increase indent if it's NOT a void element AND NOT self-closing\r\n if (!isVoidElement && !isSelfClosing) {\r\n indentLevel++;\r\n }\r\n }\r\n }\r\n\r\n return output;\r\n }\r\n\r\n /**\r\n * Format HTML with proper indentation and cleanup\r\n *\r\n * - Tokenizes HTML into tags and content\r\n * - Applies proper indentation (4 spaces per level)\r\n * - Handles void elements correctly\r\n * - Preserves script and style tag content\r\n * - Maintains tag hierarchy\r\n *\r\n * @param html Raw HTML string\r\n * @param indentSize Number of spaces per indent level (default: 4)\r\n * @returns Formatted HTML with perfect indentation\r\n */\r\n export function formatHTML(html: string, indentSize: number = 4): string {\r\n // Remove existing whitespace and normalize\r\n const normalized = html\r\n .split('\\n')\r\n .map(line => line.trim())\r\n .filter(line => line.length > 0)\r\n .join('');\r\n\r\n // Tokenize the HTML\r\n const tokens = tokenize(normalized);\r\n\r\n // Process tokens with indentation\r\n const lines = processTokens(tokens, indentSize);\r\n\r\n // Join and return\r\n return lines.join('\\n');\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext, AppConfig } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n import { formatHTML } from './htmlFormatter';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate i18n meta tag for client injection\r\n *\r\n * The browser.tsx template will read this meta tag and inject\r\n * i18n config into the ClientManager config automatically.\r\n *\r\n * Convert server-side filesystem paths to browser-accessible URLs:\r\n * ./src/shared/static/dist/i18n → static/dist/i18n/\r\n *\r\n * Note: NO leading slash - i18n library needs to fetch as URL (not file path)\r\n * Always returns a meta tag - never empty!\r\n */\r\n function generateI18nMetaTag(i18nConfig: AppConfig['i18n']): string {\r\n // Provide defaults if config is missing or incomplete\r\n const config = {\r\n defaultLanguage: i18nConfig?.defaultLanguage || 'en',\r\n supportedLanguages: i18nConfig?.supportedLanguages || ['en'],\r\n basePath: i18nConfig?.basePath || 'i18n/',\r\n fileExtension: i18nConfig?.fileExtension || 'json'\r\n };\r\n\r\n // Convert server-side path to browser URL\r\n let browserPath = config.basePath;\r\n\r\n // Convert path separators to forward slashes\r\n browserPath = browserPath.replace(/\\\\/g, '/');\r\n\r\n // Handle server-side paths like ./src/shared/static/dist/i18n\r\n if (browserPath.includes('shared/static')) {\r\n // Extract everything after \"shared/static\" and prepend static/\r\n const afterStatic = browserPath.split('shared/static')[1] || '';\r\n browserPath = 'static' + afterStatic;\r\n } else if (browserPath.startsWith('./')) {\r\n // Remove leading ./\r\n browserPath = browserPath.slice(2);\r\n } else if (browserPath.startsWith('/')) {\r\n // Remove leading /\r\n browserPath = browserPath.slice(1);\r\n }\r\n\r\n // Ensure it ends with /\r\n if (!browserPath.endsWith('/')) {\r\n browserPath += '/';\r\n }\r\n\r\n const i18nJson = JSON.stringify({\r\n defaultLanguage: config.defaultLanguage,\r\n supportedLanguages: config.supportedLanguages,\r\n basePath: browserPath,\r\n fileExtension: config.fileExtension\r\n });\r\n\r\n return `<meta name=\"app-i18n\" content='${i18nJson}' />`;\r\n }\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - i18n meta tag (read by browser.tsx template)\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n *\r\n * Output is automatically formatted with proper indentation via formatHTML()\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => `<script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => `<link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n // Add i18n meta tag for browser.tsx to read\r\n const i18nMetaTag = generateI18nMetaTag(i18nConfig);\r\n\r\n // DEBUG: Log to see what's being generated\r\n if (!i18nMetaTag) {\r\n console.warn('[SPA] WARNING: i18n meta tag is empty!');\r\n }\r\n\r\n // Build raw HTML - GUARANTEE i18n meta tag is included!\r\n const rawHTML = `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n${generateSEOMetaTags(pageConfig, baseConfig)}\r\n${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n${i18nMetaTag}\r\n${styleTags}\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n\r\n // DEBUG: Check if i18n meta tag is in raw HTML\r\n if (!rawHTML.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag NOT in raw HTML!');\r\n }\r\n\r\n // Format the HTML with proper indentation\r\n const formatted = formatHTML(rawHTML);\r\n\r\n // DEBUG: Check if i18n meta tag made it through formatting\r\n if (!formatted.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag LOST during formatting!');\r\n }\r\n\r\n return formatted;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data and i18n meta tag\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig, i18nConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n // Get global i18n config (set by serverSPA plugin)\r\n export function getGlobalI18nConfig() {\r\n return (global as any).__cruxjs_i18n_config;\r\n }\r\n\r\n export function setGlobalI18nConfig(config: any) {\r\n (global as any).__cruxjs_i18n_config = config;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n *\r\n * Returns HTML for web requests and JSON for API requests\r\n * Automatically uses global i18n config (no need to pass it!)\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n // Use global i18n config - unified approach!\r\n const i18nConfig = getGlobalI18nConfig();\r\n console.log(`[Errors] Generating error page ${statusCode} with i18n:`, !!i18nConfig);\r\n const html = generateSPAHTML(errorConfig, baseConfig, i18nConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n console.log(`[Errors] No custom error page for ${statusCode}, returning fallback`);\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n *\r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n *\r\n * No need to pass i18nConfig - uses global!\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page, setGlobalI18nConfig } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * Features:\r\n * - Server-side rendering with full SEO support (meta tags, structured data)\r\n * - Automatic error page handling (404, 500, etc.)\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl : 'https://example.com',\r\n * clientEntry : './src/client/browser.tsx',\r\n * clientScriptPath : ['/static/dist/js/browser.js'],\r\n * clientStylePath : ['/static/dist/css/index.css'],\r\n * enableAutoNotFound : true,\r\n * pages: [\r\n * {\r\n * title : 'Home',\r\n * path : '/',\r\n * description : 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode : 404,\r\n * title : '404 - Not Found',\r\n * path : '/404',\r\n * description : 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig, appConfig?: any): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Store i18n config globally so ALL handlers can access it (unified!)\r\n setGlobalI18nConfig(appConfig?.i18n);\r\n const i18nConfig = appConfig?.i18n;\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config, i18nConfig));\r\n }\r\n\r\n // Create error handler function (uses global i18nConfig automatically!)\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -9,18 +9,17 @@ import { CruxPlugin } from '@cruxjs/base';
|
|
|
9
9
|
* - description, keywords, etc: Use t() for translations
|
|
10
10
|
*
|
|
11
11
|
* Meta tag values can be:
|
|
12
|
-
* - Direct string: 'My Title'
|
|
13
|
-
* -
|
|
14
|
-
* - Array [key]: ['my.key'] (no fallback)
|
|
12
|
+
* - Direct string: 'My Title'
|
|
13
|
+
* - Translation key: 'my.translation.key'
|
|
15
14
|
*/
|
|
16
15
|
interface SPAPageConfig {
|
|
17
|
-
title: string
|
|
16
|
+
title: string;
|
|
18
17
|
path: string;
|
|
19
|
-
description?: string
|
|
20
|
-
keywords?:
|
|
21
|
-
expertise?: string
|
|
22
|
-
experience?: string
|
|
23
|
-
authority?: string
|
|
18
|
+
description?: string;
|
|
19
|
+
keywords?: string[];
|
|
20
|
+
expertise?: string;
|
|
21
|
+
experience?: string;
|
|
22
|
+
authority?: string;
|
|
24
23
|
contentType?: 'article' | 'product' | 'service' | 'app' | 'workspace' | 'page';
|
|
25
24
|
ogImage?: string;
|
|
26
25
|
canonical?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,18 +9,17 @@ import { CruxPlugin } from '@cruxjs/base';
|
|
|
9
9
|
* - description, keywords, etc: Use t() for translations
|
|
10
10
|
*
|
|
11
11
|
* Meta tag values can be:
|
|
12
|
-
* - Direct string: 'My Title'
|
|
13
|
-
* -
|
|
14
|
-
* - Array [key]: ['my.key'] (no fallback)
|
|
12
|
+
* - Direct string: 'My Title'
|
|
13
|
+
* - Translation key: 'my.translation.key'
|
|
15
14
|
*/
|
|
16
15
|
interface SPAPageConfig {
|
|
17
|
-
title: string
|
|
16
|
+
title: string;
|
|
18
17
|
path: string;
|
|
19
|
-
description?: string
|
|
20
|
-
keywords?:
|
|
21
|
-
expertise?: string
|
|
22
|
-
experience?: string
|
|
23
|
-
authority?: string
|
|
18
|
+
description?: string;
|
|
19
|
+
keywords?: string[];
|
|
20
|
+
expertise?: string;
|
|
21
|
+
experience?: string;
|
|
22
|
+
authority?: string;
|
|
24
23
|
contentType?: 'article' | 'product' | 'service' | 'app' | 'workspace' | 'page';
|
|
25
24
|
ogImage?: string;
|
|
26
25
|
canonical?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import {t}from'@minejs/server';import {isRTL}from'@minejs/i18n';function h(e,t$1=""){if(!e)return t$1;if(e.includes(".")&&/^[a-z0-9._]+$/.test(e))try{return t(e)||e}catch{return e}return e}function u(e,t=""){return h(e,t)}function w(e){return !e||e.length===0?"":e.map(n=>h(n)).filter(Boolean).join(", ")}function P(e){if(!e)return "Page";let t=h(e);try{return v(t)}catch{return t}}function v(e){let t$1=t("app.name");return isRTL()?`${t$1} - ${e}`:`${e} - ${t$1}`}function y(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,r=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",a=P(e.title),i=u(e.description,t.defaultDescription||"A modern single-page application"),s=w(e.keywords||t.defaultKeywords),o=u(e.expertise,""),c=u(e.experience,""),l=u(e.authority,""),p=e.contentType==="article"?"article":"website";return `<!-- \u{1F50D} Core SEO Meta Tags -->
|
|
2
2
|
<meta charset="UTF-8" />
|
|
3
3
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4
|
-
<title>${
|
|
5
|
-
<meta name="description" content="${
|
|
6
|
-
${
|
|
7
|
-
<meta name="robots" content="${
|
|
4
|
+
<title>${a}</title>
|
|
5
|
+
<meta name="description" content="${i}" />
|
|
6
|
+
${s?`<meta name="keywords" content="${s}" />`:""}
|
|
7
|
+
<meta name="robots" content="${r}" />
|
|
8
8
|
<meta name="language" content="en" />
|
|
9
9
|
<meta http-equiv="content-language" content="en-us" />
|
|
10
10
|
<!-- \u{1F465} E-E-A-T Signals for AI Search -->
|
|
11
11
|
${t.author?`<meta name="author" content="${t.author}" />`:""}
|
|
12
|
-
${
|
|
13
|
-
${
|
|
14
|
-
${
|
|
12
|
+
${o?`<meta name="expertise" content="${o}" />`:""}
|
|
13
|
+
${c?`<meta name="experience" content="${c}" />`:""}
|
|
14
|
+
${l?`<meta name="authority" content="${l}" />`:""}
|
|
15
15
|
<!-- \u{1F4F1} Mobile & Performance -->
|
|
16
16
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
17
17
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
@@ -19,39 +19,39 @@ var E=class{constructor(e){this.translations={},this.currentLanguage="en",this.d
|
|
|
19
19
|
<meta name="theme-color" content="#000000" />
|
|
20
20
|
<!-- \u{1F517} Canonical & Prefetch -->
|
|
21
21
|
<link rel="canonical" href="${n}" />
|
|
22
|
-
${(e.clientScriptPath||t.clientScriptPath)?.map(
|
|
22
|
+
${(e.clientScriptPath||t.clientScriptPath)?.map(g=>`<link rel="prefetch" href="${g}" />`).join(`
|
|
23
23
|
`)}
|
|
24
24
|
<!-- \u26A1 Performance & Security -->
|
|
25
25
|
<meta name="format-detection" content="telephone=no" />
|
|
26
26
|
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
|
27
27
|
<!-- \u{1F4D8} Open Graph Protocol (Social Media) -->
|
|
28
|
-
<meta property="og:type" content="${
|
|
29
|
-
<meta property="og:title" content="${
|
|
30
|
-
<meta property="og:description" content="${
|
|
28
|
+
<meta property="og:type" content="${p}" />
|
|
29
|
+
<meta property="og:title" content="${a}" />
|
|
30
|
+
<meta property="og:description" content="${i}" />
|
|
31
31
|
<meta property="og:url" content="${n}" />
|
|
32
32
|
<meta property="og:locale" content="en_US" />
|
|
33
33
|
${t.author?`<meta property="og:site_name" content="${t.author}" />`:""}
|
|
34
34
|
${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
|
|
35
|
-
${e.ogImage?`<meta property="og:image:alt" content="${
|
|
36
|
-
`}function
|
|
37
|
-
${JSON.stringify(
|
|
38
|
-
`).map(
|
|
35
|
+
${e.ogImage?`<meta property="og:image:alt" content="${a}" />`:""}
|
|
36
|
+
`}function S(e,t,n="WebPage"){let r=e.canonical||`${t.baseUrl}${e.path}`,a=P(e.title),i=u(e.description,t.defaultDescription),s=u(e.expertise,""),o=u(e.experience,""),c=u(e.authority,""),l={"@context":"https://schema.org","@type":n,name:a,url:r,description:i,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(s||o||c)&&{creator:{"@type":"Person",name:t.author||"Unknown",...s&&{expertise:s},...o&&{experience:o},...c&&{authority:c}}}};return `<script type="application/ld+json">
|
|
37
|
+
${JSON.stringify(l,null,2).split(`
|
|
38
|
+
`).map(g=>g&&` ${g}`).join(`
|
|
39
39
|
`)}
|
|
40
|
-
</script>`}var
|
|
41
|
-
`).map(
|
|
42
|
-
`)}function
|
|
43
|
-
`),
|
|
44
|
-
`),
|
|
40
|
+
</script>`}var R=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"];function k(e){let t=[],n=/(<[^>]+>)|([^<]+)/g,r;for(;(r=n.exec(e))!==null;)r[1]?t.push({type:"tag",value:r[1]}):r[2]&&r[2].trim().length>0&&t.push({type:"text",value:r[2]});return t}function L(e,t=4){let n=" ".repeat(t),r=[],a=0;for(let i=0;i<e.length;i++){let s=e[i],o=s.value.trim();o.startsWith("</")&&(a=Math.max(0,a-1));let c=n.repeat(a);if(!(s.type==="text"&&o.length===0)&&(s.type==="text"&&r.length>0&&!r[r.length-1].trim().endsWith(">")?r[r.length-1]+=o:r.push(c+o),o.startsWith("<")&&!o.startsWith("</"))){if(o.startsWith("<!DOCTYPE")||o.startsWith("<!--"))continue;let l=o.match(/<([A-Za-z][A-Za-z0-9\\-]*)/i),p=l?l[1].toLowerCase():"",g=R.includes(p),T=o.endsWith("/>");!g&&!T&&a++;}}return r}function x(e,t=4){let n=e.split(`
|
|
41
|
+
`).map(i=>i.trim()).filter(i=>i.length>0).join(""),r=k(n);return L(r,t).join(`
|
|
42
|
+
`)}function M(e){let t={defaultLanguage:e?.defaultLanguage||"en",supportedLanguages:e?.supportedLanguages||["en"],basePath:e?.basePath||"i18n/",fileExtension:e?.fileExtension||"json"},n=t.basePath;return n=n.replace(/\\/g,"/"),n.includes("shared/static")?n="static"+(n.split("shared/static")[1]||""):n.startsWith("./")?n=n.slice(2):n.startsWith("/")&&(n=n.slice(1)),n.endsWith("/")||(n+="/"),`<meta name="app-i18n" content='${JSON.stringify({defaultLanguage:t.defaultLanguage,supportedLanguages:t.supportedLanguages,basePath:n,fileExtension:t.fileExtension})}' />`}function d(e,t,n){let r=e.clientScriptPath||t.clientScriptPath,a=e.clientStylePath||t.clientStylePath||[],i=r.map(p=>`<script type="module" src="${p}"></script>`).join(`
|
|
43
|
+
`),s=a.map(p=>`<link rel="stylesheet" href="${p}" />`).join(`
|
|
44
|
+
`),o=M(n);o||console.warn("[SPA] WARNING: i18n meta tag is empty!");let c=`<!DOCTYPE html>
|
|
45
45
|
<html lang="en">
|
|
46
46
|
<head>
|
|
47
|
-
${
|
|
48
|
-
${
|
|
47
|
+
${y(e,t)}
|
|
48
|
+
${S(e,t,e.contentType==="article"?"Article":"WebPage")}
|
|
49
|
+
${o}
|
|
49
50
|
${s}
|
|
50
|
-
${i}
|
|
51
51
|
</head>
|
|
52
52
|
<body>
|
|
53
53
|
<div id="app"></div>
|
|
54
|
-
${
|
|
54
|
+
${i}
|
|
55
55
|
</body>
|
|
56
|
-
</html>`;
|
|
56
|
+
</html>`;c.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag NOT in raw HTML!");let l=x(c);return l.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag LOST during formatting!"),l}function m(e,t,n){return {method:"GET",path:e.path,handler:r=>{let a=d(e,t,n);return r.html(a)}}}function _(){return global.__cruxjs_i18n_config}function A(e){global.__cruxjs_i18n_config=e;}function j(e,t,n,r){if(r.startsWith("/api/"))return {status:e,headers:{"Content-Type":"application/json"},body:JSON.stringify({error:`Error ${e}`})};if(t.has(e)){let a=t.get(e),i=_();console.log(`[Errors] Generating error page ${e} with i18n:`,!!i);let s=d(a,n,i);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:s}}return console.log(`[Errors] No custom error page for ${e}, returning fallback`),{status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function $(e,t){return (n,r)=>{let a=j(n,e,t,r);return new Response(a.body,{status:a.status,headers:a.headers})}}function b(){return {statusCode:404,title:"404 - Page Not Found",path:"/404",description:"The page you are looking for could not be found.",keywords:["404","not found","error"],robots:"noindex, nofollow"}}function K(e,t){let n=[],r=new Map;A(t?.i18n);let a=t?.i18n;if(e.pages&&e.pages.length>0)for(let o of e.pages)n.push(m(o,e,a));if(e.errorPages&&e.errorPages.length>0)for(let o of e.errorPages)r.set(o.statusCode,o),n.push(m(o,e,a));if(e.enableAutoNotFound&&!r.has(404)){let o=b();r.set(404,o),n.push(m(o,e,a));}let i=$(r,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:n,__spaErrorHandler:i,onRegister:async o=>{if(console.log(`[SPA Plugin] Registered ${n.length} SPA routes`),r.size>0){let c=Array.from(r.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${c}`);}},onAwake:async o=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async o=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async o=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}export{K as serverSPA};//# sourceMappingURL=index.js.map
|
|
57
57
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../node_modules/@minejs/i18n/src/index.ts","../src/utils/seo.ts","../src/utils/htmlFormatter.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["I18nManager","config","stored","lang","translations","flattened","trans","obj","prefix","key","value","newKey","params","translation","param","paramValue","fallback","original","result","tokens","regex","match","fn","callback","instance","getI18n","t","isRTL","genPageTitle","appName","pageName","resolveMetaValue","defaultValue","c","resolveKeywords","keywords","kw","resolvePageTitle","title","titleKey","C","generateSEOMetaTags","baseConfig","canonicalUrl","robots","description","expertise","experience","authority","ogType","script","generateStructuredData","pageConfig","contentType","schema","line","VOID_ELEMENTS","tokenize","html","processTokens","indentSize","indent","output","indentLevel","i","token","padding","tagMatch","tagName","isVoidElement","isSelfClosing","formatHTML","normalized","generateI18nMetaTag","i18nConfig","browserPath","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","i18nMetaTag","rawHTML","formatted","createSPARoute","getGlobalI18nConfig","setGlobalI18nConfig","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","appConfig","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.js","sourcesContent":["// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import * as types from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n export class I18nManager {\r\n\r\n // ┌──────────────────────────────── STATE ─────────────────────────────┐\r\n\r\n private translations : types.TranslationSet = {};\r\n private currentLanguage : types.LanguageCode = 'en';\r\n private defaultLanguage : types.LanguageCode = 'en';\r\n private fallbackLanguage : types.LanguageCode = 'en';\r\n private supportedLanguages = new Set<types.LanguageCode>(['en']);\r\n private rtlLanguages = new Set<string>(['ar', 'he', 'fa', 'ur', 'yi', 'ji', 'iw', 'ku']);\r\n private listeners = new Set<(lang: types.LanguageCode) => void>();\r\n private storage? : types.I18nStorage;\r\n private onLanguageChange? : (lang: types.LanguageCode) => void;\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌──────────────────────────────── INIT ──────────────────────────────┐\r\n\r\n constructor(config?: types.I18nConfig) {\r\n if (config) {\r\n this.defaultLanguage = config.defaultLanguage || 'en';\r\n this.fallbackLanguage = config.fallbackLanguage || config.defaultLanguage || 'en';\r\n this.currentLanguage = config.defaultLanguage || 'en';\r\n this.storage = config.storage;\r\n this.onLanguageChange = config.onLanguageChange;\r\n\r\n if (config.supportedLanguages) {\r\n this.supportedLanguages = new Set(config.supportedLanguages);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Initialize with stored language preference\r\n */\r\n public async init(): Promise<void> {\r\n if (this.storage) {\r\n const stored = await this.storage.get('i18n-language');\r\n if (stored && this.supportedLanguages.has(stored)) {\r\n this.currentLanguage = stored;\r\n }\r\n }\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌─────────────────────────────── LOADING ────────────────────────────┐\r\n\r\n /**\r\n * Load translations for a specific language\r\n * @param lang Language code\r\n * @param translations Translation object (can be nested)\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n public loadLanguage(lang: types.LanguageCode, translations: Record<string, any>): void {\r\n if (!this.translations[lang]) {\r\n this.translations[lang] = {};\r\n }\r\n\r\n const flattened = this.flattenObject(translations);\r\n this.translations[lang] = { ...this.translations[lang], ...flattened };\r\n this.supportedLanguages.add(lang);\r\n }\r\n\r\n /**\r\n * Load multiple languages at once\r\n * @param translations Object with language codes as keys\r\n */\r\n public loadTranslations(translations: types.TranslationSet): void {\r\n Object.entries(translations).forEach(([lang, trans]) => {\r\n this.loadLanguage(lang, trans);\r\n });\r\n }\r\n\r\n /**\r\n * Flatten nested object into dot notation\r\n * @private\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n private flattenObject(obj: Record<string, any>, prefix: string = ''): Record<string, string> {\r\n const flattened: Record<string, string> = {};\r\n\r\n for (const key in obj) {\r\n if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;\r\n\r\n const value = obj[key];\r\n const newKey = prefix ? `${prefix}.${key}` : key;\r\n\r\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\r\n Object.assign(flattened, this.flattenObject(value, newKey));\r\n } else {\r\n flattened[newKey] = String(value);\r\n }\r\n }\r\n\r\n return flattened;\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌───────────────────────────── TRANSLATION ──────────────────────────┐\r\n\r\n /**\r\n * Translate a key with parameter replacement\r\n *\r\n * @example\r\n * t('welcome.message', { name: 'John' })\r\n * // => \"Welcome, John!\"\r\n *\r\n * @param key Translation key (dot notation)\r\n * @param params Optional parameters for replacement\r\n * @returns Translated string\r\n */\r\n public t(key: string, params?: Record<string, string>): string {\r\n let translation = this.getTranslation(key);\r\n\r\n if (params) {\r\n Object.entries(params).forEach(([param, value]) => {\r\n // Check if parameter value is itself a translation key\r\n const paramValue = this.getTranslation(value, value);\r\n translation = translation.replace(\r\n new RegExp(`\\\\{${param}\\\\}`, 'g'),\r\n paramValue\r\n );\r\n });\r\n }\r\n\r\n return translation;\r\n }\r\n\r\n /**\r\n * Get raw translation without parameter replacement\r\n * @private\r\n */\r\n private getTranslation(key: string, fallback?: string): string {\r\n // Try current language\r\n if (this.translations[this.currentLanguage]?.[key]) {\r\n return this.translations[this.currentLanguage][key];\r\n }\r\n\r\n // Try fallback language\r\n if (this.fallbackLanguage !== this.currentLanguage &&\r\n this.translations[this.fallbackLanguage]?.[key]) {\r\n return this.translations[this.fallbackLanguage][key];\r\n }\r\n\r\n // Try default language\r\n if (this.defaultLanguage !== this.currentLanguage &&\r\n this.defaultLanguage !== this.fallbackLanguage &&\r\n this.translations[this.defaultLanguage]?.[key]) {\r\n return this.translations[this.defaultLanguage][key];\r\n }\r\n\r\n // Warn and return fallback\r\n console.warn(`[i18n] Translation key not found: \"${key}\" (lang: ${this.currentLanguage})`);\r\n return fallback || key;\r\n }\r\n\r\n /**\r\n * Translate with a specific language temporarily\r\n *\r\n * @param key Translation key\r\n * @param lang Language code\r\n * @param params Optional parameters\r\n */\r\n public tLang(key: string, lang: types.LanguageCode, params?: Record<string, string>): string {\r\n const original = this.currentLanguage;\r\n this.currentLanguage = lang;\r\n const result = this.t(key, params);\r\n this.currentLanguage = original;\r\n return result;\r\n }\r\n\r\n /**\r\n * Translate and parse HTML-like tags into tokens\r\n * Converts \\n or /n to line breaks\r\n *\r\n * @example\r\n * // Translation: \"Hello\\nWorld <strong>here</strong>\"\r\n * tParse('message')\r\n * // => [\r\n * // { type: 'text', content: 'Hello' },\r\n * // { type: 'tag', tag: 'br', content: '' },\r\n * // { type: 'text', content: 'World ' },\r\n * // { type: 'tag', tag: 'strong', content: 'here' }\r\n * // ]\r\n *\r\n * @param key Translation key\r\n * @param params Optional parameters\r\n * @returns Array of tokens\r\n */\r\n public tParse(key: string, params?: Record<string, string>): types.TranslationToken[] {\r\n let translation = this.t(key, params);\r\n\r\n // Convert newlines to <br> tags\r\n translation = translation.replace(/\\\\n|\\/n/g, '<br>');\r\n\r\n const tokens: types.TranslationToken[] = [];\r\n const regex = /<([a-z]+)>([^<]*)<\\/\\1>|<([a-z]+)\\s*\\/?>|([^<]+)/gi;\r\n let match;\r\n\r\n while ((match = regex.exec(translation)) !== null) {\r\n if (match[4]) {\r\n // Plain text\r\n tokens.push({ type: 'text', content: match[4] });\r\n } else if (match[1]) {\r\n // Paired tag: <strong>text</strong>\r\n tokens.push({ type: 'tag', tag: match[1], content: match[2] });\r\n } else if (match[3]) {\r\n // Self-closing: <br> or <br/>\r\n tokens.push({ type: 'tag', tag: match[3], content: '' });\r\n }\r\n }\r\n\r\n return tokens.length > 0 ? tokens : [{ type: 'text', content: translation }];\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌────────────────────────────── LANGUAGE ────────────────────────────┐\r\n\r\n /**\r\n * Set current language\r\n */\r\n public async setLanguage(lang: types.LanguageCode): Promise<void> {\r\n if (!this.supportedLanguages.has(lang)) {\r\n console.warn(`[i18n] Language \"${lang}\" not supported`);\r\n return;\r\n }\r\n\r\n this.currentLanguage = lang;\r\n\r\n // Persist if storage available\r\n if (this.storage) {\r\n await this.storage.set('i18n-language', lang);\r\n }\r\n\r\n // Notify listeners\r\n this.listeners.forEach(fn => fn(lang));\r\n\r\n if (this.onLanguageChange) {\r\n this.onLanguageChange(lang);\r\n }\r\n }\r\n\r\n /**\r\n * Get current language\r\n */\r\n public getLanguage(): types.LanguageCode {\r\n return this.currentLanguage;\r\n }\r\n\r\n /**\r\n * Get all supported languages\r\n */\r\n public getSupportedLanguages(): types.LanguageCode[] {\r\n return Array.from(this.supportedLanguages);\r\n }\r\n\r\n /**\r\n * Check if language is supported\r\n */\r\n public isLanguageSupported(lang: types.LanguageCode): boolean {\r\n return this.supportedLanguages.has(lang);\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n\r\n // ┌─────────────────────────────── HELPERS ────────────────────────────┐\r\n\r\n /**\r\n * Check if a translation key exists\r\n */\r\n public hasKey(key: string): boolean {\r\n return !!(\r\n this.translations[this.currentLanguage]?.[key] ||\r\n this.translations[this.fallbackLanguage]?.[key] ||\r\n this.translations[this.defaultLanguage]?.[key]\r\n );\r\n }\r\n\r\n /**\r\n * Get all translations for current language\r\n */\r\n public getTranslations(): Record<string, string> {\r\n return this.translations[this.currentLanguage] || {};\r\n }\r\n\r\n /**\r\n * Check if current language is RTL\r\n */\r\n public isRTL(): boolean {\r\n return this.rtlLanguages.has(this.currentLanguage.toLowerCase().substring(0, 2));\r\n }\r\n\r\n /**\r\n * Check if specific language is RTL\r\n */\r\n public isRTLLanguage(lang: types.LanguageCode): boolean {\r\n return this.rtlLanguages.has(lang.toLowerCase().substring(0, 2));\r\n }\r\n\r\n /**\r\n * Subscribe to language changes\r\n * @returns Unsubscribe function\r\n */\r\n public onChange(callback: (lang: types.LanguageCode) => void): () => void {\r\n this.listeners.add(callback);\r\n return () => this.listeners.delete(callback);\r\n }\r\n\r\n // └────────────────────────────────────────────────────────────────────┘\r\n\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n /**\r\n * Browser storage adapter using localStorage\r\n */\r\n export const browserStorage: types.I18nStorage = {\r\n get: (key: string) => {\r\n if (typeof localStorage === 'undefined') return null;\r\n return localStorage.getItem(key);\r\n },\r\n set: (key: string, value: string) => {\r\n if (typeof localStorage !== 'undefined') {\r\n localStorage.setItem(key, value);\r\n }\r\n }\r\n };\r\n\r\n /**\r\n * Memory storage adapter (for server/testing)\r\n */\r\n export const memoryStorage = (() => {\r\n const store = new Map<string, string>();\r\n return {\r\n get: (key: string) => store.get(key) || null,\r\n set: (key: string, value: string) => { store.set(key, value); }\r\n };\r\n })();\r\n\r\n /**\r\n * Fetch translations from URLs\r\n * Works in both browser and Node.js (with node-fetch)\r\n */\r\n export async function fetchTranslations(\r\n urls: string | string[],\r\n manager: I18nManager\r\n ): Promise<void> {\r\n const urlList = Array.isArray(urls) ? urls : [urls];\r\n const translations: types.TranslationSet = {};\r\n\r\n for (const url of urlList) {\r\n try {\r\n const response = await fetch(url);\r\n if (response.ok) {\r\n const data = await response.json();\r\n // Extract language from filename: /path/en.json -> en\r\n const langMatch = url.match(/([a-z]{2,3})\\.json$/i);\r\n const lang = langMatch ? langMatch[1].toLowerCase() : 'en';\r\n translations[lang] = data;\r\n }\r\n } catch (error) {\r\n console.warn(`[i18n] Failed to fetch: ${url}`, error);\r\n }\r\n }\r\n\r\n if (Object.keys(translations).length > 0) {\r\n manager.loadTranslations(translations);\r\n }\r\n }\r\n\r\n /**\r\n * Lazy loader: fetch language only when needed\r\n * Prevents loading all languages at startup\r\n */\r\n export class LazyLoader {\r\n private baseUrl: string;\r\n private manager: I18nManager;\r\n private loading = new Map<types.LanguageCode, Promise<void>>();\r\n private loaded = new Set<types.LanguageCode>();\r\n private isServerSide: boolean;\r\n private fileExtension: string;\r\n\r\n constructor(baseUrl: string, manager: I18nManager, fileExtension: string = 'json') {\r\n this.baseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';\r\n this.manager = manager;\r\n this.fileExtension = fileExtension;\r\n this.isServerSide = typeof fetch === 'undefined';\r\n }\r\n\r\n /**\r\n * Load a language file on-demand\r\n * Caches the promise to prevent duplicate requests\r\n */\r\n async load(lang: types.LanguageCode): Promise<void> {\r\n // Already loaded\r\n if (this.loaded.has(lang)) {\r\n return;\r\n }\r\n\r\n // Currently loading\r\n if (this.loading.has(lang)) {\r\n return this.loading.get(lang);\r\n }\r\n\r\n // Start loading\r\n const promise = this.doLoad(lang);\r\n this.loading.set(lang, promise);\r\n\r\n try {\r\n await promise;\r\n this.loaded.add(lang);\r\n } finally {\r\n this.loading.delete(lang);\r\n }\r\n }\r\n\r\n private async doLoad(lang: types.LanguageCode): Promise<void> {\r\n try {\r\n const filePath = `${this.baseUrl}${lang}.${this.fileExtension}`;\r\n\r\n let data: Record<string, string> | null;\r\n\r\n if (this.isServerSide) {\r\n // Node.js: Read from filesystem\r\n data = await this.loadFromFile(filePath);\r\n } else {\r\n // Browser: Fetch from URL\r\n data = await this.loadFromUrl(filePath);\r\n }\r\n\r\n if (data) {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n this.manager.loadLanguage(lang, data as Record<string, any>);\r\n }\r\n } catch (error) {\r\n console.warn(`[i18n] Error loading language: ${lang}`, error);\r\n }\r\n }\r\n\r\n private async loadFromUrl(url: string): Promise<Record<string, string> | null> {\r\n try {\r\n const response = await fetch(url);\r\n\r\n if (response.ok) {\r\n return await response.json();\r\n } else {\r\n console.warn(`[i18n] Failed to load language from URL: ${url} (${response.status})`);\r\n return null;\r\n }\r\n } catch (error) {\r\n console.warn(`[i18n] Error fetching from URL: ${url}`, error);\r\n return null;\r\n }\r\n }\r\n\r\n private async loadFromFile(filePath: string): Promise<Record<string, string> | null> {\r\n try {\r\n // Dynamic import to avoid issues in browsers\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const fs = await import('fs').then(m => m.promises).catch((): any => null);\r\n\r\n if (!fs) {\r\n console.warn('[i18n] fs module not available. Running in browser?');\r\n return null;\r\n }\r\n\r\n const content = await fs.readFile(filePath, 'utf-8');\r\n return JSON.parse(content);\r\n } catch (error) {\r\n console.warn(`[i18n] Error reading file: ${filePath}`, error);\r\n return null;\r\n }\r\n }\r\n\r\n isLoaded(lang: types.LanguageCode): boolean {\r\n return this.loaded.has(lang);\r\n }\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n let instance: I18nManager | null = null;\r\n\r\n /**\r\n * Get or create the global i18n instance\r\n */\r\n export function getI18n(): I18nManager {\r\n if (!instance) {\r\n instance = new I18nManager();\r\n }\r\n return instance;\r\n }\r\n\r\n /**\r\n * Initialize i18n with config\r\n * Call this once at app startup\r\n *\r\n * @example\r\n * // Load only default language at startup\r\n * await setupI18n({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr', 'de']\r\n * });\r\n */\r\n export async function setupI18n(config: types.I18nConfig): Promise<I18nManager> {\r\n instance = new I18nManager(config);\r\n await instance.init();\r\n return instance;\r\n }\r\n\r\n /**\r\n * Create a lazy loader for translations\r\n * Only loads languages when needed\r\n *\r\n * @example\r\n * const loader = createLazyLoader('https://mycdn.com/i18n/');\r\n *\r\n * // Later, when user switches language:\r\n * await loader.load('ar');\r\n * await setLanguage('ar');\r\n */\r\n export function createLazyLoader(baseUrl: string, fileExtension: string = 'json'): LazyLoader {\r\n return new LazyLoader(baseUrl, getI18n(), fileExtension);\r\n }\r\n\r\n /**\r\n * Setup i18n with lazy loading\r\n * Only loads the default language at startup\r\n *\r\n * @example\r\n * // Browser\r\n * const loader = await setupLazy({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr', 'de', 'zh'],\r\n * basePath: '/i18n/'\r\n * });\r\n *\r\n * // Later when user switches:\r\n * await loader.load('ar');\r\n * await setLanguage('ar');\r\n */\r\n export async function setupLazy(config: types.I18nConfig & { basePath?: string; baseUrl?: string }): Promise<LazyLoader> {\r\n const manager = new I18nManager(config);\r\n await manager.init();\r\n instance = manager;\r\n\r\n const basePath = config.basePath || config.baseUrl || './locales/';\r\n const fileExtension = config.fileExtension || 'json';\r\n const loader = new LazyLoader(basePath, manager, fileExtension);\r\n\r\n // Load only the current language at startup\r\n await loader.load(manager.getLanguage());\r\n\r\n return loader;\r\n }\r\n\r\n /**\r\n * Auto-setup: Smart initialization based on environment and config\r\n * Automatically handles browser vs server and file vs URL loading\r\n *\r\n * @example\r\n * // Browser: Auto-fetches from server\r\n * const loader = await setupAuto({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr'],\r\n * basePath: 'http://localhost:3000/static/i18n/'\r\n * });\r\n *\r\n * // Server (Node.js): Auto-reads from filesystem\r\n * const loader = await setupAuto({\r\n * defaultLanguage: 'en',\r\n * supportedLanguages: ['en', 'ar', 'fr'],\r\n * basePath: './public/locales/'\r\n * });\r\n */\r\n export async function setupAuto(config: types.I18nConfig & { basePath: string }): Promise<LazyLoader> {\r\n const manager = new I18nManager(config);\r\n await manager.init();\r\n instance = manager;\r\n\r\n const fileExtension = config.fileExtension || 'json';\r\n const loader = new LazyLoader(config.basePath, manager, fileExtension);\r\n\r\n // Auto-load default language\r\n await loader.load(manager.getLanguage());\r\n\r\n return loader;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n export const t = (key: string, params?: Record<string, string>) =>\r\n getI18n().t(key, params);\r\n\r\n export const tLang = (key: string, lang: types.LanguageCode, params?: Record<string, string>) =>\r\n getI18n().tLang(key, lang, params);\r\n\r\n export const tParse = (key: string, params?: Record<string, string>) =>\r\n getI18n().tParse(key, params);\r\n\r\n export const setLanguage = (lang: types.LanguageCode) =>\r\n getI18n().setLanguage(lang);\r\n\r\n export const getLanguage = () =>\r\n getI18n().getLanguage();\r\n\r\n export const getSupportedLanguages = () =>\r\n getI18n().getSupportedLanguages();\r\n\r\n export const hasKey = (key: string) =>\r\n getI18n().hasKey(key);\r\n\r\n export const isRTL = () =>\r\n getI18n().isRTL();\r\n\r\n export const isRTLLanguage = (lang: types.LanguageCode) =>\r\n getI18n().isRTLLanguage(lang);\r\n\r\n export const onChange = (callback: (lang: types.LanguageCode) => void) =>\r\n getI18n().onChange(callback);\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n export const loadLanguage = (lang: types.LanguageCode, translations: Record<string, any>) =>\r\n getI18n().loadLanguage(lang, translations);\r\n\r\n export const loadTranslations = (translations: types.TranslationSet) =>\r\n getI18n().loadTranslations(translations);\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate page title with proper RTL handling\r\n *\r\n * @example\r\n * // English: \"Profile - MyApp\"\r\n * // Arabic: \"MyApp - الملف الشخصي\"\r\n * genPageTitle('profile', 'page.')\r\n */\r\n export function genPageTitle(key: string, prefix: string = 'page.'): string {\r\n const appName = t('app.name');\r\n const pageName = t(prefix + key);\r\n return isRTL() ? `${appName} - ${pageName}` : `${pageName} - ${appName}`;\r\n }\r\n\r\n /**\r\n * Pluralization helper\r\n *\r\n * @example\r\n * plural(1, 'item.single', 'item.plural') // \"1 item\"\r\n * plural(5, 'item.single', 'item.plural') // \"5 items\"\r\n */\r\n export function plural(count: number, singleKey: string, pluralKey: string): string {\r\n const key = count === 1 ? singleKey : pluralKey;\r\n return t(key, { count: String(count) });\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ ════ ════════════════════════════════════════╗\r\n\r\n export default {\r\n I18nManager,\r\n getI18n,\r\n setupI18n,\r\n createLazyLoader,\r\n setupLazy,\r\n setupAuto,\r\n browserStorage,\r\n memoryStorage,\r\n fetchTranslations,\r\n genPageTitle,\r\n plural,\r\n };\r\n\r\n\r\n export type I18nManagerInstance = InstanceType<typeof I18nManager>;\r\n export type LazyLoaderInstance = InstanceType<typeof LazyLoader>;\r\n\r\n export * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝","// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { t, genPageTitle } from '@minejs/i18n';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Detect and translate meta tag values\r\n *\r\n * Smart detection for:\r\n * - Direct string: 'My Title' → 'My Title'\r\n * - Translation key: 'my.key' → t('my.key')\r\n * - Array with key + fallback: ['my.key', 'Fallback'] → t('my.key', fallback)\r\n * - Array with key only: ['my.key'] → t('my.key')\r\n *\r\n * Returns the translated value or original string\r\n */\r\n function resolveMetaValue(value: string | string[] | undefined, defaultValue: string = ''): string {\r\n if (!value) return defaultValue;\r\n\r\n // Array format: [translationKey] or [translationKey, fallback]\r\n if (Array.isArray(value)) {\r\n const [key, fallback] = value;\r\n if (!key) return fallback || defaultValue;\r\n try {\r\n return t(key) || fallback || key || defaultValue;\r\n } catch {\r\n return fallback || key || defaultValue;\r\n }\r\n }\r\n\r\n // String format: could be direct value or translation key\r\n // Try to translate first, if it fails/returns same value, treat as direct string\r\n try {\r\n const translated = t(value);\r\n return translated || value;\r\n } catch {\r\n return value;\r\n }\r\n }\r\n\r\n /**\r\n * Resolve keywords with smart translation detection\r\n *\r\n * Keywords can be:\r\n * - Direct string: 'keyword' → 'keyword' (NO translation)\r\n * - Translation array: ['meta.key'] or ['meta.key', 'fallback'] → translate it\r\n *\r\n * This allows keywords: ['404', 'error', 'not found'] without translation\r\n * And also: [['meta.keywords.error'], ['meta.keywords.notfound']] with translation\r\n */\r\n function resolveKeywords(keywords: (string | string[])[] | undefined): string {\r\n if (!keywords || keywords.length === 0) return '';\r\n\r\n const resolved = keywords\r\n .map(kw => {\r\n // String format: use as-is, don't translate\r\n if (typeof kw === 'string') {\r\n return kw;\r\n }\r\n\r\n // Array format: translate the keyword\r\n if (Array.isArray(kw)) {\r\n const [key, fallback] = kw;\r\n if (!key) return fallback || '';\r\n try {\r\n return t(key) || fallback || key;\r\n } catch {\r\n return fallback || key;\r\n }\r\n }\r\n\r\n return '';\r\n })\r\n .filter(Boolean);\r\n\r\n return resolved.join(', ');\r\n }\r\n\r\n /**\r\n * Generate page title with translation support\r\n *\r\n * Uses genPageTitle() from @minejs/i18n for RTL-aware titles\r\n * Detects if value is a translation key and uses it accordingly\r\n */\r\n function resolvePageTitle(title: string | string[] | undefined): string {\r\n if (!title) return 'Page';\r\n\r\n let titleKey = '';\r\n\r\n // Extract translation key if array\r\n if (Array.isArray(title)) {\r\n titleKey = title[0] || '';\r\n } else {\r\n titleKey = title;\r\n }\r\n\r\n // Use genPageTitle for RTL-aware title generation\r\n try {\r\n return genPageTitle(titleKey, '');\r\n } catch {\r\n // Fallback: if genPageTitle fails, use direct value\r\n if (Array.isArray(title)) {\r\n return title[1] || title[0] || 'Page';\r\n }\r\n return title;\r\n }\r\n }\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals and translation support\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n * - Translation support for all meta values\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n\r\n // Resolve translated values\r\n const title = resolvePageTitle(config.title);\r\n const description = resolveMetaValue(config.description, baseConfig.defaultDescription || 'A modern single-page application');\r\n const keywords = resolveKeywords(config.keywords || baseConfig.defaultKeywords);\r\n const expertise = resolveMetaValue(config.expertise, '');\r\n const experience = resolveMetaValue(config.experience, '');\r\n const authority = resolveMetaValue(config.authority, '');\r\n\r\n // Determine OG type based on content type\r\n const ogType = config.contentType === 'article' ? 'article' : 'website';\r\n\r\n return `<!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${title}</title>\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${expertise ? `<meta name=\"expertise\" content=\"${expertise}\" />` : ''}\r\n ${experience ? `<meta name=\"experience\" content=\"${experience}\" />` : ''}\r\n ${authority ? `<meta name=\"authority\" content=\"${authority}\" />` : ''}\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n')}\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n <!-- 📘 Open Graph Protocol (Social Media) -->\r\n <meta property=\"og:type\" content=\"${ogType}\" />\r\n <meta property=\"og:title\" content=\"${title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />\r\n <meta property=\"og:locale\" content=\"en_US\" />\r\n ${baseConfig.author ? `<meta property=\"og:site_name\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image:alt\" content=\"${title}\" />` : ''}\r\n `;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n * Handles translation keys in meta values\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n // Resolve translated values for structured data\r\n const title = resolvePageTitle(pageConfig.title);\r\n const description = resolveMetaValue(pageConfig.description, baseConfig.defaultDescription);\r\n const expertise = resolveMetaValue(pageConfig.expertise, '');\r\n const experience = resolveMetaValue(pageConfig.experience, '');\r\n const authority = resolveMetaValue(pageConfig.authority, '');\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': title,\r\n 'url': canonicalUrl,\r\n 'description': description,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...((expertise || experience || authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(expertise && { 'expertise': expertise }),\r\n ...(experience && { 'experience': experience }),\r\n ...(authority && { 'authority': authority })\r\n }\r\n })\r\n };\r\n\r\n const jsonStr = JSON.stringify(schema, null, 2)\r\n .split('\\n')\r\n .map(line => line ? ` ${line}` : line)\r\n .join('\\n');\r\n return `<script type=\"application/ld+json\">\\n${jsonStr}\\n</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/htmlFormatter.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];\r\n\r\n interface Token {\r\n type: 'tag' | 'text' | 'comment';\r\n value: string;\r\n }\r\n\r\n /**\r\n * Tokenize HTML into tags, text, and comments\r\n */\r\n function tokenize(html: string): Token[] {\r\n const tokens: Token[] = [];\r\n const regex = /(<[^>]+>)|([^<]+)/g;\r\n let match;\r\n\r\n while ((match = regex.exec(html)) !== null) {\r\n if (match[1]) {\r\n // It's a tag\r\n tokens.push({ type: 'tag', value: match[1] });\r\n } else if (match[2] && match[2].trim().length > 0) {\r\n // It's text content (not just whitespace)\r\n tokens.push({ type: 'text', value: match[2] });\r\n }\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Process tokens and apply indentation\r\n */\r\n function processTokens(tokens: Token[], indentSize: number = 4): string[] {\r\n const indent = ' '.repeat(indentSize);\r\n const output: string[] = [];\r\n let indentLevel = 0;\r\n\r\n // eslint-disable-next-line @typescript-eslint/prefer-for-of\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n const value = token.value.trim();\r\n\r\n // Decrease indent for closing tags BEFORE adding\r\n if (value.startsWith('</')) {\r\n indentLevel = Math.max(0, indentLevel - 1);\r\n }\r\n\r\n const padding = indent.repeat(indentLevel);\r\n\r\n // Skip empty text nodes\r\n if (token.type === 'text' && value.length === 0) {\r\n continue;\r\n }\r\n\r\n // Add to output\r\n if (token.type === 'text') {\r\n // Text nodes - check if previous line exists\r\n if (output.length > 0 && !output[output.length - 1].trim().endsWith('>')) {\r\n // Append to previous line if it doesn't end with tag\r\n output[output.length - 1] += value;\r\n } else {\r\n output.push(padding + value);\r\n }\r\n } else {\r\n // Tags and comments\r\n output.push(padding + value);\r\n }\r\n\r\n // Increase indent for OPENING tags ONLY (not closing, not void elements, not DOCTYPE/comments)\r\n if (value.startsWith('<') && !value.startsWith('</')) {\r\n // Skip DOCTYPE and comments - they don't increase indent\r\n if (value.startsWith('<!DOCTYPE') || value.startsWith('<!--')) {\r\n continue;\r\n }\r\n\r\n // Extract tag name\r\n const tagMatch = value.match(/<([A-Za-z][A-Za-z0-9\\\\-]*)/i);\r\n const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';\r\n\r\n // Check if it's a void/self-closing element\r\n const isVoidElement = VOID_ELEMENTS.includes(tagName);\r\n const isSelfClosing = value.endsWith('/>');\r\n\r\n // Only increase indent if it's NOT a void element AND NOT self-closing\r\n if (!isVoidElement && !isSelfClosing) {\r\n indentLevel++;\r\n }\r\n }\r\n }\r\n\r\n return output;\r\n }\r\n\r\n /**\r\n * Format HTML with proper indentation and cleanup\r\n *\r\n * - Tokenizes HTML into tags and content\r\n * - Applies proper indentation (4 spaces per level)\r\n * - Handles void elements correctly\r\n * - Preserves script and style tag content\r\n * - Maintains tag hierarchy\r\n *\r\n * @param html Raw HTML string\r\n * @param indentSize Number of spaces per indent level (default: 4)\r\n * @returns Formatted HTML with perfect indentation\r\n */\r\n export function formatHTML(html: string, indentSize: number = 4): string {\r\n // Remove existing whitespace and normalize\r\n const normalized = html\r\n .split('\\n')\r\n .map(line => line.trim())\r\n .filter(line => line.length > 0)\r\n .join('');\r\n\r\n // Tokenize the HTML\r\n const tokens = tokenize(normalized);\r\n\r\n // Process tokens with indentation\r\n const lines = processTokens(tokens, indentSize);\r\n\r\n // Join and return\r\n return lines.join('\\n');\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext, AppConfig } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n import { formatHTML } from './htmlFormatter';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate i18n meta tag for client injection\r\n *\r\n * The browser.tsx template will read this meta tag and inject\r\n * i18n config into the ClientManager config automatically.\r\n *\r\n * Convert server-side filesystem paths to browser-accessible URLs:\r\n * ./src/shared/static/dist/i18n → static/dist/i18n/\r\n *\r\n * Note: NO leading slash - i18n library needs to fetch as URL (not file path)\r\n * Always returns a meta tag - never empty!\r\n */\r\n function generateI18nMetaTag(i18nConfig: AppConfig['i18n']): string {\r\n // Provide defaults if config is missing or incomplete\r\n const config = {\r\n defaultLanguage: i18nConfig?.defaultLanguage || 'en',\r\n supportedLanguages: i18nConfig?.supportedLanguages || ['en'],\r\n basePath: i18nConfig?.basePath || 'i18n/',\r\n fileExtension: i18nConfig?.fileExtension || 'json'\r\n };\r\n\r\n // Convert server-side path to browser URL\r\n let browserPath = config.basePath;\r\n\r\n // Convert path separators to forward slashes\r\n browserPath = browserPath.replace(/\\\\/g, '/');\r\n\r\n // Handle server-side paths like ./src/shared/static/dist/i18n\r\n if (browserPath.includes('shared/static')) {\r\n // Extract everything after \"shared/static\" and prepend static/\r\n const afterStatic = browserPath.split('shared/static')[1] || '';\r\n browserPath = 'static' + afterStatic;\r\n } else if (browserPath.startsWith('./')) {\r\n // Remove leading ./\r\n browserPath = browserPath.slice(2);\r\n } else if (browserPath.startsWith('/')) {\r\n // Remove leading /\r\n browserPath = browserPath.slice(1);\r\n }\r\n\r\n // Ensure it ends with /\r\n if (!browserPath.endsWith('/')) {\r\n browserPath += '/';\r\n }\r\n\r\n const i18nJson = JSON.stringify({\r\n defaultLanguage: config.defaultLanguage,\r\n supportedLanguages: config.supportedLanguages,\r\n basePath: browserPath,\r\n fileExtension: config.fileExtension\r\n });\r\n\r\n return `<meta name=\"app-i18n\" content='${i18nJson}' />`;\r\n }\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - i18n meta tag (read by browser.tsx template)\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n *\r\n * Output is automatically formatted with proper indentation via formatHTML()\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => `<script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => `<link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n // Add i18n meta tag for browser.tsx to read\r\n const i18nMetaTag = generateI18nMetaTag(i18nConfig);\r\n\r\n // DEBUG: Log to see what's being generated\r\n if (!i18nMetaTag) {\r\n console.warn('[SPA] WARNING: i18n meta tag is empty!');\r\n }\r\n\r\n // Build raw HTML - GUARANTEE i18n meta tag is included!\r\n const rawHTML = `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n${generateSEOMetaTags(pageConfig, baseConfig)}\r\n${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n${i18nMetaTag}\r\n${styleTags}\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n\r\n // DEBUG: Check if i18n meta tag is in raw HTML\r\n if (!rawHTML.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag NOT in raw HTML!');\r\n }\r\n\r\n // Format the HTML with proper indentation\r\n const formatted = formatHTML(rawHTML);\r\n\r\n // DEBUG: Check if i18n meta tag made it through formatting\r\n if (!formatted.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag LOST during formatting!');\r\n }\r\n\r\n return formatted;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data and i18n meta tag\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig, i18nConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n // Get global i18n config (set by serverSPA plugin)\r\n export function getGlobalI18nConfig() {\r\n return (global as any).__cruxjs_i18n_config;\r\n }\r\n\r\n export function setGlobalI18nConfig(config: any) {\r\n (global as any).__cruxjs_i18n_config = config;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n *\r\n * Returns HTML for web requests and JSON for API requests\r\n * Automatically uses global i18n config (no need to pass it!)\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n // Use global i18n config - unified approach!\r\n const i18nConfig = getGlobalI18nConfig();\r\n console.log(`[Errors] Generating error page ${statusCode} with i18n:`, !!i18nConfig);\r\n const html = generateSPAHTML(errorConfig, baseConfig, i18nConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n console.log(`[Errors] No custom error page for ${statusCode}, returning fallback`);\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n *\r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n *\r\n * No need to pass i18nConfig - uses global!\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page, setGlobalI18nConfig } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * Features:\r\n * - Server-side rendering with full SEO support (meta tags, structured data)\r\n * - Automatic error page handling (404, 500, etc.)\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl : 'https://example.com',\r\n * clientEntry : './src/client/browser.tsx',\r\n * clientScriptPath : ['/static/dist/js/browser.js'],\r\n * clientStylePath : ['/static/dist/css/index.css'],\r\n * enableAutoNotFound : true,\r\n * pages: [\r\n * {\r\n * title : 'Home',\r\n * path : '/',\r\n * description : 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode : 404,\r\n * title : '404 - Not Found',\r\n * path : '/404',\r\n * description : 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig, appConfig?: any): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Store i18n config globally so ALL handlers can access it (unified!)\r\n setGlobalI18nConfig(appConfig?.i18n);\r\n const i18nConfig = appConfig?.i18n;\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config, i18nConfig));\r\n }\r\n\r\n // Create error handler function (uses global i18nConfig automatically!)\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/seo.ts","../src/utils/htmlFormatter.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["parseValue","value","defaultValue","t","resolveMetaValue","resolveKeywords","keywords","kw","resolvePageTitle","title","parsedTitle","genPageTitle","val","appName","isRTL","generateSEOMetaTags","config","baseConfig","canonicalUrl","robots","description","expertise","experience","authority","ogType","script","generateStructuredData","pageConfig","contentType","schema","line","VOID_ELEMENTS","tokenize","html","tokens","regex","match","processTokens","indentSize","indent","output","indentLevel","token","padding","tagMatch","tagName","isVoidElement","isSelfClosing","formatHTML","normalized","generateI18nMetaTag","i18nConfig","browserPath","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","i18nMetaTag","rawHTML","formatted","createSPARoute","c","getGlobalI18nConfig","setGlobalI18nConfig","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","appConfig","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.js","sourcesContent":["// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { t } from '@minejs/server';\r\n import { isRTL } from '@minejs/i18n';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Smart parser for string values\r\n *\r\n * Intelligently detects if a string is a translation key or plain text:\r\n * - Translation key format (e.g., 'meta.home.title'): must contain dots, attempts translation via t(key)\r\n * - Plain text (e.g., 'My Title', 'cruxjs', 'framework'): returns as-is\r\n *\r\n * Key format: MUST contain at least one dot (.) to be considered a translation key\r\n * Examples:\r\n * - 'meta.home.title' → translation key → tries t('meta.home.title')\r\n * - 'cruxjs' → plain text → returns 'cruxjs'\r\n * - 'My Title' → plain text → returns 'My Title'\r\n *\r\n * @param value - The string to parse\r\n * @param defaultValue - Fallback if value is empty\r\n * @returns Translated value or original string\r\n */\r\n function parseValue(value: string | undefined, defaultValue: string = ''): string {\r\n if (!value) return defaultValue;\r\n\r\n // Check if value looks like a translation key (e.g., 'meta.home.title')\r\n // Key pattern: MUST contain at least one dot AND be lowercase/numeric/underscores/dots\r\n const isTranslationKey = value.includes('.') && /^[a-z0-9._]+$/.test(value);\r\n\r\n if (isTranslationKey) {\r\n try {\r\n const translated = t(value);\r\n // If translation returns a value, use it; otherwise fall back to original\r\n return translated || value;\r\n } catch {\r\n // If translation fails, return the original value\r\n return value;\r\n }\r\n }\r\n\r\n // Not a translation key format, return as direct text\r\n return value;\r\n }\r\n\r\n /**\r\n * Resolve meta tag values with smart translation detection\r\n * Uses parseValue() to automatically detect and translate\r\n */\r\n function resolveMetaValue(value: string | undefined, defaultValue: string = ''): string {\r\n return parseValue(value, defaultValue);\r\n }\r\n\r\n /**\r\n * Resolve keywords with smart translation detection\r\n * Uses parseValue() on each keyword to automatically detect and translate\r\n */\r\n function resolveKeywords(keywords: string[] | undefined): string {\r\n if (!keywords || keywords.length === 0) return '';\r\n\r\n const resolved = keywords\r\n .map(kw => parseValue(kw))\r\n .filter(Boolean);\r\n\r\n return resolved.join(', ');\r\n }\r\n\r\n /**\r\n * Generate page title with translation support\r\n *\r\n * Uses genPageTitle() from @minejs/i18n for RTL-aware titles\r\n * First parses the value to detect and translate if needed\r\n */\r\n function resolvePageTitle(title: string | undefined): string {\r\n if (!title) return 'Page';\r\n\r\n // First parse the value to detect translation keys\r\n const parsedTitle = parseValue(title);\r\n\r\n // Use genPageTitle for RTL-aware title generation\r\n try {\r\n return genPageTitle(parsedTitle);\r\n } catch {\r\n // Fallback: if genPageTitle fails, use parsed value\r\n return parsedTitle;\r\n }\r\n }\r\n\r\n /**\r\n * Generate page title with proper RTL handling\r\n *\r\n * @example\r\n * // English: \"Profile - MyApp\"\r\n * // Arabic: \"MyApp - الملف الشخصي\"\r\n */\r\n export function genPageTitle(val: string): string {\r\n const appName = t('app.name');\r\n return isRTL() ? `${appName} - ${val}` : `${val} - ${appName}`;\r\n }\r\n\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals and translation support\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n * - Translation support for all meta values\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n\r\n // Resolve translated values\r\n const title = resolvePageTitle(config.title);\r\n const description = resolveMetaValue(config.description, baseConfig.defaultDescription || 'A modern single-page application');\r\n const keywords = resolveKeywords(config.keywords || baseConfig.defaultKeywords);\r\n const expertise = resolveMetaValue(config.expertise, '');\r\n const experience = resolveMetaValue(config.experience, '');\r\n const authority = resolveMetaValue(config.authority, '');\r\n\r\n // Determine OG type based on content type\r\n const ogType = config.contentType === 'article' ? 'article' : 'website';\r\n\r\n return `<!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${title}</title>\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${expertise ? `<meta name=\"expertise\" content=\"${expertise}\" />` : ''}\r\n ${experience ? `<meta name=\"experience\" content=\"${experience}\" />` : ''}\r\n ${authority ? `<meta name=\"authority\" content=\"${authority}\" />` : ''}\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n')}\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n <!-- 📘 Open Graph Protocol (Social Media) -->\r\n <meta property=\"og:type\" content=\"${ogType}\" />\r\n <meta property=\"og:title\" content=\"${title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />\r\n <meta property=\"og:locale\" content=\"en_US\" />\r\n ${baseConfig.author ? `<meta property=\"og:site_name\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image:alt\" content=\"${title}\" />` : ''}\r\n `;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n * Handles translation keys in meta values\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n // Resolve translated values for structured data\r\n const title = resolvePageTitle(pageConfig.title);\r\n const description = resolveMetaValue(pageConfig.description, baseConfig.defaultDescription);\r\n const expertise = resolveMetaValue(pageConfig.expertise, '');\r\n const experience = resolveMetaValue(pageConfig.experience, '');\r\n const authority = resolveMetaValue(pageConfig.authority, '');\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': title,\r\n 'url': canonicalUrl,\r\n 'description': description,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...((expertise || experience || authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(expertise && { 'expertise': expertise }),\r\n ...(experience && { 'experience': experience }),\r\n ...(authority && { 'authority': authority })\r\n }\r\n })\r\n };\r\n\r\n const jsonStr = JSON.stringify(schema, null, 2)\r\n .split('\\n')\r\n .map(line => line ? ` ${line}` : line)\r\n .join('\\n');\r\n return `<script type=\"application/ld+json\">\\n${jsonStr}\\n</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/htmlFormatter.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];\r\n\r\n interface Token {\r\n type: 'tag' | 'text' | 'comment';\r\n value: string;\r\n }\r\n\r\n /**\r\n * Tokenize HTML into tags, text, and comments\r\n */\r\n function tokenize(html: string): Token[] {\r\n const tokens: Token[] = [];\r\n const regex = /(<[^>]+>)|([^<]+)/g;\r\n let match;\r\n\r\n while ((match = regex.exec(html)) !== null) {\r\n if (match[1]) {\r\n // It's a tag\r\n tokens.push({ type: 'tag', value: match[1] });\r\n } else if (match[2] && match[2].trim().length > 0) {\r\n // It's text content (not just whitespace)\r\n tokens.push({ type: 'text', value: match[2] });\r\n }\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Process tokens and apply indentation\r\n */\r\n function processTokens(tokens: Token[], indentSize: number = 4): string[] {\r\n const indent = ' '.repeat(indentSize);\r\n const output: string[] = [];\r\n let indentLevel = 0;\r\n\r\n // eslint-disable-next-line @typescript-eslint/prefer-for-of\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n const value = token.value.trim();\r\n\r\n // Decrease indent for closing tags BEFORE adding\r\n if (value.startsWith('</')) {\r\n indentLevel = Math.max(0, indentLevel - 1);\r\n }\r\n\r\n const padding = indent.repeat(indentLevel);\r\n\r\n // Skip empty text nodes\r\n if (token.type === 'text' && value.length === 0) {\r\n continue;\r\n }\r\n\r\n // Add to output\r\n if (token.type === 'text') {\r\n // Text nodes - check if previous line exists\r\n if (output.length > 0 && !output[output.length - 1].trim().endsWith('>')) {\r\n // Append to previous line if it doesn't end with tag\r\n output[output.length - 1] += value;\r\n } else {\r\n output.push(padding + value);\r\n }\r\n } else {\r\n // Tags and comments\r\n output.push(padding + value);\r\n }\r\n\r\n // Increase indent for OPENING tags ONLY (not closing, not void elements, not DOCTYPE/comments)\r\n if (value.startsWith('<') && !value.startsWith('</')) {\r\n // Skip DOCTYPE and comments - they don't increase indent\r\n if (value.startsWith('<!DOCTYPE') || value.startsWith('<!--')) {\r\n continue;\r\n }\r\n\r\n // Extract tag name\r\n const tagMatch = value.match(/<([A-Za-z][A-Za-z0-9\\\\-]*)/i);\r\n const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';\r\n\r\n // Check if it's a void/self-closing element\r\n const isVoidElement = VOID_ELEMENTS.includes(tagName);\r\n const isSelfClosing = value.endsWith('/>');\r\n\r\n // Only increase indent if it's NOT a void element AND NOT self-closing\r\n if (!isVoidElement && !isSelfClosing) {\r\n indentLevel++;\r\n }\r\n }\r\n }\r\n\r\n return output;\r\n }\r\n\r\n /**\r\n * Format HTML with proper indentation and cleanup\r\n *\r\n * - Tokenizes HTML into tags and content\r\n * - Applies proper indentation (4 spaces per level)\r\n * - Handles void elements correctly\r\n * - Preserves script and style tag content\r\n * - Maintains tag hierarchy\r\n *\r\n * @param html Raw HTML string\r\n * @param indentSize Number of spaces per indent level (default: 4)\r\n * @returns Formatted HTML with perfect indentation\r\n */\r\n export function formatHTML(html: string, indentSize: number = 4): string {\r\n // Remove existing whitespace and normalize\r\n const normalized = html\r\n .split('\\n')\r\n .map(line => line.trim())\r\n .filter(line => line.length > 0)\r\n .join('');\r\n\r\n // Tokenize the HTML\r\n const tokens = tokenize(normalized);\r\n\r\n // Process tokens with indentation\r\n const lines = processTokens(tokens, indentSize);\r\n\r\n // Join and return\r\n return lines.join('\\n');\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext, AppConfig } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n import { formatHTML } from './htmlFormatter';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate i18n meta tag for client injection\r\n *\r\n * The browser.tsx template will read this meta tag and inject\r\n * i18n config into the ClientManager config automatically.\r\n *\r\n * Convert server-side filesystem paths to browser-accessible URLs:\r\n * ./src/shared/static/dist/i18n → static/dist/i18n/\r\n *\r\n * Note: NO leading slash - i18n library needs to fetch as URL (not file path)\r\n * Always returns a meta tag - never empty!\r\n */\r\n function generateI18nMetaTag(i18nConfig: AppConfig['i18n']): string {\r\n // Provide defaults if config is missing or incomplete\r\n const config = {\r\n defaultLanguage: i18nConfig?.defaultLanguage || 'en',\r\n supportedLanguages: i18nConfig?.supportedLanguages || ['en'],\r\n basePath: i18nConfig?.basePath || 'i18n/',\r\n fileExtension: i18nConfig?.fileExtension || 'json'\r\n };\r\n\r\n // Convert server-side path to browser URL\r\n let browserPath = config.basePath;\r\n\r\n // Convert path separators to forward slashes\r\n browserPath = browserPath.replace(/\\\\/g, '/');\r\n\r\n // Handle server-side paths like ./src/shared/static/dist/i18n\r\n if (browserPath.includes('shared/static')) {\r\n // Extract everything after \"shared/static\" and prepend static/\r\n const afterStatic = browserPath.split('shared/static')[1] || '';\r\n browserPath = 'static' + afterStatic;\r\n } else if (browserPath.startsWith('./')) {\r\n // Remove leading ./\r\n browserPath = browserPath.slice(2);\r\n } else if (browserPath.startsWith('/')) {\r\n // Remove leading /\r\n browserPath = browserPath.slice(1);\r\n }\r\n\r\n // Ensure it ends with /\r\n if (!browserPath.endsWith('/')) {\r\n browserPath += '/';\r\n }\r\n\r\n const i18nJson = JSON.stringify({\r\n defaultLanguage: config.defaultLanguage,\r\n supportedLanguages: config.supportedLanguages,\r\n basePath: browserPath,\r\n fileExtension: config.fileExtension\r\n });\r\n\r\n return `<meta name=\"app-i18n\" content='${i18nJson}' />`;\r\n }\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - i18n meta tag (read by browser.tsx template)\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n *\r\n * Output is automatically formatted with proper indentation via formatHTML()\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => `<script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => `<link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n // Add i18n meta tag for browser.tsx to read\r\n const i18nMetaTag = generateI18nMetaTag(i18nConfig);\r\n\r\n // DEBUG: Log to see what's being generated\r\n if (!i18nMetaTag) {\r\n console.warn('[SPA] WARNING: i18n meta tag is empty!');\r\n }\r\n\r\n // Build raw HTML - GUARANTEE i18n meta tag is included!\r\n const rawHTML = `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n${generateSEOMetaTags(pageConfig, baseConfig)}\r\n${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n${i18nMetaTag}\r\n${styleTags}\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n\r\n // DEBUG: Check if i18n meta tag is in raw HTML\r\n if (!rawHTML.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag NOT in raw HTML!');\r\n }\r\n\r\n // Format the HTML with proper indentation\r\n const formatted = formatHTML(rawHTML);\r\n\r\n // DEBUG: Check if i18n meta tag made it through formatting\r\n if (!formatted.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag LOST during formatting!');\r\n }\r\n\r\n return formatted;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data and i18n meta tag\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig, i18nConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n // Get global i18n config (set by serverSPA plugin)\r\n export function getGlobalI18nConfig() {\r\n return (global as any).__cruxjs_i18n_config;\r\n }\r\n\r\n export function setGlobalI18nConfig(config: any) {\r\n (global as any).__cruxjs_i18n_config = config;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n *\r\n * Returns HTML for web requests and JSON for API requests\r\n * Automatically uses global i18n config (no need to pass it!)\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n // Use global i18n config - unified approach!\r\n const i18nConfig = getGlobalI18nConfig();\r\n console.log(`[Errors] Generating error page ${statusCode} with i18n:`, !!i18nConfig);\r\n const html = generateSPAHTML(errorConfig, baseConfig, i18nConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n console.log(`[Errors] No custom error page for ${statusCode}, returning fallback`);\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n *\r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n *\r\n * No need to pass i18nConfig - uses global!\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page, setGlobalI18nConfig } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * Features:\r\n * - Server-side rendering with full SEO support (meta tags, structured data)\r\n * - Automatic error page handling (404, 500, etc.)\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl : 'https://example.com',\r\n * clientEntry : './src/client/browser.tsx',\r\n * clientScriptPath : ['/static/dist/js/browser.js'],\r\n * clientStylePath : ['/static/dist/css/index.css'],\r\n * enableAutoNotFound : true,\r\n * pages: [\r\n * {\r\n * title : 'Home',\r\n * path : '/',\r\n * description : 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode : 404,\r\n * title : '404 - Not Found',\r\n * path : '/404',\r\n * description : 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig, appConfig?: any): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Store i18n config globally so ALL handlers can access it (unified!)\r\n setGlobalI18nConfig(appConfig?.i18n);\r\n const i18nConfig = appConfig?.i18n;\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config, i18nConfig));\r\n }\r\n\r\n // Create error handler function (uses global i18nConfig automatically!)\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxplug/spa",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Single Page Application (SPA) plugin for CruxJS with built-in SEO/CEO support, E-E-A-T signals, and JSON-LD structured data generation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cruxjs",
|
|
@@ -47,11 +47,10 @@
|
|
|
47
47
|
"bun": "^1.3.3"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@cruxjs/base": "^0.0.
|
|
50
|
+
"@cruxjs/base": "^0.0.7",
|
|
51
51
|
"@minejs/db": "^0.0.3",
|
|
52
|
-
"@minejs/server": "^0.0.
|
|
53
|
-
"
|
|
54
|
-
"test": "^3.3.0"
|
|
52
|
+
"@minejs/server": "^0.0.9",
|
|
53
|
+
"@minejs/i18n": "^0.1.0"
|
|
55
54
|
},
|
|
56
55
|
"devDependencies": {
|
|
57
56
|
"@eslint/js": "^9.39.2",
|