@symbo.ls/mermaid 3.14.4 → 3.14.5

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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @symbo.ls/mermaid
2
+
3
+ ## 3.14.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Auto-generated patch release for changed packages.
8
+ - Updated dependencies
9
+ - @symbo.ls/utils@3.14.2
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{createMermaidApp as M,loadJsonData as d}from"../src/server.js";import o from"../src/utils/logger.js";const p=process.argv.slice(2);function e(n,E){const c=p.indexOf(`--${n}`);return c!==-1?p[c+1]:E}const i=parseInt(e("port",process.env.PORT||"3333"),10),s=e("json",process.env.JSON_PATH||"./app.json"),r=e("appkey",process.env.APPKEY||null),t="production",l=process.env.LOG_LEVEL||(t==="production"?"info":"debug");o.level=l,o.info("[MERMAID] Starting standalone runner..."),o.info(`[MERMAID] Environment: ${t}`),o.info(`[MERMAID] JSON: ${s}`),r&&o.info(`[MERMAID] Locked to appkey: ${r}`);try{d(s)}catch(n){o.error(`[MERMAID] ${n.message}`),process.exit(1)}const v=M({jsonPath:s,appkeyOverride:r,channel:t==="production"?"production":"development"}),I=v.listen(i,()=>{o.info(`[MERMAID] Running on http://localhost:${i}`)}),a=n=>{o.info(`[MERMAID] ${n} received, shutting down...`),I.close(),process.exit(0)};process.on("SIGTERM",()=>a("SIGTERM")),process.on("SIGINT",()=>a("SIGINT"));
@@ -0,0 +1 @@
1
+ import{createMermaidApp as t,loadJsonData as o}from"./src/server.js";import{renderProjectHtml as n,renderNotPublishedHtml as i,transpileFunctionStrings as p,generateSitemap as l,prefetchPageData as s}from"./src/render.js";import{getBundleScript as f,getSmblsIife as c}from"./src/bundle.js";import{normalizeRoute as d,resolvePageRoute as g,extractRouteParams as x,STATIC_EXT as S}from"./src/route.js";import{deepDestringifyFunctions as P,stripFunctionStrings as b}from"./src/utils/deepDestringify.js";import{transpile as I}from"./src/utils/funcql-transpile.js";import{default as T}from"./src/utils/vartranformer.js";import{default as y}from"./src/utils/logger.js";export{S as STATIC_EXT,t as createMermaidApp,P as deepDestringifyFunctions,x as extractRouteParams,l as generateSitemap,f as getBundleScript,c as getSmblsIife,o as loadJsonData,y as log,d as normalizeRoute,s as prefetchPageData,i as renderNotPublishedHtml,n as renderProjectHtml,T as replaceVariablesInFileSync,g as resolvePageRoute,b as stripFunctionStrings,I as transpileFuncql,p as transpileFunctionStrings};
@@ -0,0 +1,97 @@
1
+ import r from"./utils/logger.js";import h from"smbls/iife-string";r.debug(`[BUNDLE] smbls IIFE loaded from smbls/iife-string (${h.length} bytes)`);const u=n=>n.replace(/</g,"\\u003c");function v(){return h}function w(n,e,t,{ProjectDataService:i}={}){if(r.debug(`[BUNDLE] getBundleScript started for ${n}`),!e)return r.warn("[BUNDLE] No data provided, returning empty"),"";try{if(e.merged)Object.assign(e,e.merged),r.debug(`[BUNDLE] Applied precomputed merged data for project ${e.id}`);else if(i&&Array.isArray(e.sharedLibraries)&&e.sharedLibraries.length>0){const s=i.mergeSharedLibraries(e,e.sharedLibraries,e.projectType);Object.assign(e,s),r.debug(`[BUNDLE] Merged ${e.sharedLibraries.length} shared libraries`)}}catch(s){r.error(`[BUNDLE] Failed to ensure merged data: ${s.message}`)}const m=u(JSON.stringify(e)),f=t?.headers&&typeof t.headers.get=="function"&&t.headers.get("host")||t?.headers?.host||t?.host||t?.url&&(()=>{try{return new URL(t.url).host}catch{return""}})()||"",o=String(f).toLowerCase().split(":")[0],a=(()=>{if(t?.query)return t.query;try{return Object.fromEntries(new URL(t?.url||"","http://x").searchParams)}catch{return{}}})(),g=a.release||!1,p=a.version,c=o.endsWith(".symbo.ls")||o.endsWith(".preview.symbols.app")||o==="localhost"||o.endsWith(".localhost"),l=!!g||!!p||!c,b=e&&(e._id||e.projectId||e.id)||null,y=c?u(JSON.stringify({...e.settings,liveSync:!l,inspect:!0,verbose:!l,projectId:b})):"null",d=`
2
+ var context = Smbls.deepDestringifyFunctions(${m});
3
+ context.packageManager = 'esm.sh';
4
+ if (context.db && context.db.createClient) delete context.db.createClient;
5
+ if (context.fetch && context.fetch.createClient) delete context.fetch.createClient;
6
+
7
+ // Runtime defaults (these are baked into the starter-kit/frank but missing from server data)
8
+ if (context.useReset === undefined) context.useReset = true;
9
+ if (context.useVariable === undefined) context.useVariable = true;
10
+ if (context.useFontImport === undefined) context.useFontImport = true;
11
+ if (context.useIconSprite === undefined) context.useIconSprite = true;
12
+ if (context.useSvgSprite === undefined) context.useSvgSprite = true;
13
+ if (context.useDefaultConfig === undefined) context.useDefaultConfig = true;
14
+ if (context.useDocumentTheme === undefined) context.useDocumentTheme = true;
15
+ if (!context.routerOptions) context.routerOptions = { scrollToTop: true };
16
+
17
+ var app = context.app || {};
18
+ delete context.app;
19
+ var settings = ${y};
20
+ if (settings) context.editor = settings;
21
+
22
+ // Theme initialization: set data-theme before app renders to avoid FOUC.
23
+ // Mirrors the precedence in resolveAndApplyTheme (packages/smbls/src/index.js):
24
+ // 1. URL param ?globalTheme=<name> \u2014 embedded-iframe override
25
+ // 2. localStorage[themeStorageKey] \u2014 persisted user choice
26
+ // 3. context.globalTheme \u2014 project's configured value
27
+ // 4. matchMedia(prefers-color-scheme: dark) \u2014 OS fallback (when 'auto')
28
+ //
29
+ // Writing the literal 'auto' as data-theme would leave the
30
+ // [data-theme="dark"] / [data-theme="light"] CSS selectors unmatched and
31
+ // the page would render with system defaults instead of the chosen
32
+ // theme \u2014 so 'auto' is always resolved to a concrete light/dark value.
33
+ //
34
+ // PRIOR BUG: this script previously ignored context.globalTheme entirely
35
+ // and went straight to matchMedia after localStorage. A project with
36
+ // globalTheme: light in config.js would render in OS dark on a
37
+ // dark-mode device because the inline boot ran first and locked in
38
+ // dark before resolveAndApplyTheme could correct it.
39
+ //
40
+ // IDEMPOTENT GUARD: the SSR-side render in @symbo.ls/mermaid/render.js
41
+ // now bakes data-theme onto <html> for concrete-theme projects and
42
+ // emits an inline <head> script that runs synchronously before paint.
43
+ // When that path runs, data-theme is already set by the time the bundle
44
+ // executes \u2014 skip this block to avoid re-resolving and overwriting
45
+ // (which would race with URL/localStorage values the head script
46
+ // already honored). Kept as a safety net for older renders / cases
47
+ // where the head script couldn't run.
48
+ if (
49
+ context.useDocumentTheme !== false &&
50
+ !document.documentElement.hasAttribute('data-theme')
51
+ ) {
52
+ (function() {
53
+ try {
54
+ var t = null;
55
+ // 1. URL param wins (used by preview/canvas iframe embedding).
56
+ try {
57
+ var p = new URLSearchParams(window.location.search).get('globalTheme');
58
+ if (p) t = p;
59
+ } catch(e) {}
60
+ // 2. localStorage \u2014 only consulted when no forcing source above.
61
+ if (!t) {
62
+ var key = context.themeStorageKey;
63
+ if (key) { try { t = localStorage.getItem(key) } catch(e) {} }
64
+ }
65
+ // 3. Project's configured globalTheme (config.js export). Honors an
66
+ // explicit 'light'/'dark'/<scheme> instead of falling through to OS.
67
+ if (!t && context.globalTheme && context.globalTheme !== 'auto') {
68
+ t = context.globalTheme;
69
+ }
70
+ // 4. OS preference fallback \u2014 only when nothing concrete chose.
71
+ if (!t || t === 'auto') {
72
+ t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
73
+ }
74
+ if (t) document.documentElement.setAttribute('data-theme', t);
75
+ } catch(e) {}
76
+ })();
77
+ }
78
+
79
+ // When brender pre-rendered this page, adopt the existing DOM nodes
80
+ // instead of tearing them down. The [data-ssr] wrapper is moved out of
81
+ // the way so pre-rendered children land at document.body level \u2014 matching
82
+ // the level at which DOMQL creates its own elements. The __BRENDER__ flag
83
+ // and __BR_REGISTRY__ map were injected into <head> by the server.
84
+ var ssrShell = document.querySelector('[data-ssr]');
85
+ if (window.__BRENDER__ && ssrShell) {
86
+ // Move pre-rendered children to body before DOMQL skeleton creation
87
+ var frag = document.createDocumentFragment();
88
+ while (ssrShell.firstChild) frag.appendChild(ssrShell.firstChild);
89
+ document.body.insertBefore(frag, ssrShell);
90
+ ssrShell.remove();
91
+ } else if (ssrShell) {
92
+ // No brender hydration \u2014 remove empty/stale SSR shell
93
+ ssrShell.remove();
94
+ }
95
+
96
+ Smbls.create(app, context);
97
+ `;return r.debug(`[BUNDLE] Script generated (${d.length} bytes)`),d}export{w as getBundleScript,v as getSmblsIife};
@@ -0,0 +1,61 @@
1
+ import b from"./utils/logger.js";import at from"./utils/vartranformer.js";import{getBundleScript as ct,getSmblsIife as lt}from"./bundle.js";import{deepDestringifyFunctions as pt}from"./utils/deepDestringify.js";import{transpile as J}from"./utils/funcql-transpile.js";import{normalizeRoute as gt,resolvePageRoute as ft}from"./route.js";import{generateMetaTags as mt,getPageMetadata as dt,getImportMapScript as ht}from"@symbo.ls/utils";const ut=process.env.BRENDER!=="false";let L=null,C=null,H=null;if(ut)try{const t=await import("@symbo.ls/brender");L=t.renderRoute,C=t.generateSitemap,H=t.prefetchPageData}catch(t){b.debug(`[MERMAID] Brender not available: ${t.message}`)}const yt=`<!doctype html>
2
+ <html'\${{{ ROOT_ATTRS }}}'>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ '\${{{ HEAD_BOOT }}}'
6
+ '\${{{ META }}}'
7
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ '\${{{ IMPORTMAP }}}'
9
+ </head>
10
+ <body>
11
+ <div data-ssr>'\${{{ BODY }}}'</div>
12
+ <script>
13
+ window.process = { env: { NODE_ENV: '\${{{ NODE_ENV }}}' } };
14
+ <\/script>
15
+ <script>
16
+ '\${{{ SMBLS_IIFE }}}';
17
+ <\/script>
18
+ <script>
19
+ '\${{{ BUNDLE }}}';
20
+ <\/script>
21
+ '\${{{ EDITOR }}}'
22
+ </body>
23
+ </html>`;function M(t,i=Array.isArray(t)?[]:{}){for(const o in t){if(!Object.prototype.hasOwnProperty.call(t,o))continue;const n=t[o];if(typeof n=="string"){const s=n.trim();if(s.startsWith("(")&&s.includes("=>")||s.startsWith("function")||s.startsWith("async ")){const l=J(n);i[o]=l?l.schema:null}else i[o]=n}else Array.isArray(n)?i[o]=n.map(s=>{if(typeof s=="string"){const a=s.trim();if(a.startsWith("(")&&a.includes("=>")||a.startsWith("function")||a.startsWith("async ")){const y=J(s);return y?y.schema:null}}return s&&typeof s=="object"?M(s):s}):n&&typeof n=="object"&&!Array.isArray(n)?i[o]=M(n):i[o]=n}return i}async function xt(t,i={}){const{appkey:o,pathname:n="/",channel:s="production",userEnv:a=s,isProd:l=!1,req:y,query:bt,userId:B,smblsIife:U,editorBundle:A,analyzedScript:z="",indexHtml:K,ssrCache:r,ProjectDataService:W}=i;if(!t)return{html:"",status:404,contentType:"text/html"};if(typeof o=="string"&&o.includes("/")){const[c,p]=o.split("/");t.key||(t.key=p),t.owner||(t.owner=c)}const w=gt(n),S=ft(t.pages,n),N=a==="production"||a==="staging";let E="",I="",e=null,O=null,x="";if(r&&N){E=r.html||"";const c=[];r.fontLinks&&c.push(r.fontLinks);const p=[r.globalCSS||"",r.resetCss||""].filter(Boolean).join(`
24
+ `);p&&c.push(`<style>${p}</style>`),r.css&&c.push(`<style data-emotion="smbls">${r.css}</style>`),I=c.join(`
25
+ `);const g=[];if(r.ssrTranslations){const d=(t.config||t.settings||{}).polyglot||t.polyglot,h=d?.storagePrefix||"",T=d?.storageLangKey||"",f=[];for(const u in r.ssrTranslations){const m=r.ssrTranslations[u];m&&typeof m=="object"&&f.push(`localStorage.setItem(${JSON.stringify(h+u)},${JSON.stringify(JSON.stringify(m))})`)}if(T){const u=d?.defaultLang||"en";f.push(`if(!localStorage.getItem(${JSON.stringify(T)}))localStorage.setItem(${JSON.stringify(T)},${JSON.stringify(u)})`)}f.length&&g.push(`<script>try{${f.join(";")}}catch(e){}<\/script>`)}g.push("<script>window.__BRENDER__=true<\/script>"),x=g.join(`
26
+ `),e={metadata:r.metadata,prefetchedState:r.prefetchedState,activeLang:r.activeLang}}else if(L&&N)try{const c=S?S.route:w;b.debug(`[MERMAID] Running brender for route: ${c}`);let p=!1;try{(0,eval)("1"),p=!0}catch{}if(O=p?pt(t):M(t),e=await L(O,{route:c,pathname:w}),e){E=e.html||"";const g=[];e.fontLinks&&g.push(e.fontLinks);const D=e.globalCSS||{},d=[D.rootRule||"",e.resetCss||"",D.keyframeRules||""].filter(Boolean).join(`
27
+ `);d&&g.push(`<style>${d}</style>`),e.css&&g.push(`<style data-emotion="smbls">${e.css}</style>`),I=g.join(`
28
+ `);const h=[];if(e.ssrTranslations){const f=(O?.config||{}).polyglot||O?.polyglot,u=f?.storagePrefix||"",m=f?.storageLangKey||"",v=[];for(const R in e.ssrTranslations){const P=e.ssrTranslations[R];P&&typeof P=="object"&&v.push(`localStorage.setItem(${JSON.stringify(u+R)},${JSON.stringify(JSON.stringify(P))})`)}if(m){const R=f?.defaultLang||"en";v.push(`if(!localStorage.getItem(${JSON.stringify(m)}))localStorage.setItem(${JSON.stringify(m)},${JSON.stringify(R)})`)}v.length&&h.push(`<script>try{${v.join(";")}}catch(e){}<\/script>`)}p&&e.brRegistry&&Object.keys(e.brRegistry).length&&h.push(`<script>window.__BR_REGISTRY__=${JSON.stringify(e.brRegistry)}<\/script>`),h.push("<script>window.__BRENDER__=true<\/script>"),x=h.join(`
29
+ `),b.debug(`[MERMAID] Brender completed: body=${E.length}b, registry=${Object.keys(e.brRegistry||{}).length}`)}}catch(c){b.warn(`[MERMAID] Brender failed, falling back to client render: ${c.message}`)}const V=ct(o,t,y,{ProjectDataService:W}),Y=ht(t,t?.settings?.cdnProvider||"esmsh"),q=t?.files&&t.files["index.html"],G=K||q?.code||yt,Q=B?`<script>window.__SYMBOLS_AUTH__=${JSON.stringify({userId:B})}<\/script>`:"",_=e?.prefetchedState,X=(t.config||t.settings||{}).polyglot||t.polyglot,Z=e?.activeLang||X?.defaultLang||"en",tt=_&&Object.keys(_).length?{state:_,lang:Z,actualPathname:n}:{actualPathname:n},et=e?.metadata&&Object.keys(e.metadata).length?e.metadata:dt(t,S?S.route:w,tt,{isProduction:l,channel:s}),nt=mt(et,l),j=U||lt();if(!j)return b.error("[MERMAID] smbls IIFE bundle not available"),{html:"Internal Server Error: smbls bundle not found",status:500,contentType:"text/plain"};const st=A&&!l?`<script defer type="text/javascript">${A}<\/script>`:"",ot=l?`<!-- built with symbols.app -->
30
+ `:`<!-- symbols | ${o} | ${a} | ${N?"ssr":"csr"} | noindex -->
31
+ `,k=t.useDocumentTheme!==!1,$=t.globalTheme&&t.globalTheme!=="auto"?String(t.globalTheme):null,F=t.themeStorageKey?String(t.themeStorageKey):"",rt=k&&$?' data-theme="'+$+'"':"",it=k?"<script>(function(){try{var t=null;try{t=new URLSearchParams(location.search).get('globalTheme')}catch(e){}"+(F?"if(!t){try{t=localStorage.getItem("+JSON.stringify(F)+")}catch(e){}}":"")+"if(!t||t==='auto'){"+($?"t="+JSON.stringify($)+";":"try{t=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}catch(e){}")+`}if(t&&t!=='auto')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();<\/script>
32
+ <meta name="color-scheme" content="light dark">`:"";return{html:at(ot+G,{ROOT_ATTRS:rt,HEAD_BOOT:it,META:nt+`
33
+ `+I+`
34
+ `+x+`
35
+ `+Q,BUNDLE:V,BODY:E,SMBLS_IIFE:j,NODE_ENV:"'production'",IMPORTMAP:Y,EDITOR:st+z},{allowScript:!0}),status:200,contentType:"text/html"}}function _t(t,i="production",o=null){const n=i!=="production"?` (${i})`:"",s=o==="not_public",a=s?"Site Not Yet Public":"Not Published",l=s?`This site isn't public yet${n}`:`This project hasn't been published yet${n}`;return`<!doctype html>
36
+ <html>
37
+ <head>
38
+ <meta charset="UTF-8">
39
+ <meta name="viewport" content="width=device-width, initial-scale=1">
40
+ <meta name="robots" content="noindex">
41
+ <title>${a}</title>
42
+ <style>
43
+ * { margin: 0; padding: 0; box-sizing: border-box; }
44
+ body { min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0a0a0a; color: #fafafa; }
45
+ .container { text-align: center; max-width: 520px; padding: 2rem; }
46
+ h1 { font-size: 1.25rem; font-weight: 500; margin-bottom: 0.75rem; }
47
+ p { color: #888; font-size: 0.875rem; line-height: 1.6; }
48
+ code { background: #1a1a1a; padding: 0.1rem 0.35rem; border-radius: 3px; font-size: 0.8rem; color: #ccc; }
49
+ a { color: #888; text-decoration: underline; text-underline-offset: 2px; }
50
+ a:hover { color: #fafafa; }
51
+ .env { display: inline-block; font-size: 0.75rem; color: #555; background: #1a1a1a; padding: 0.2rem 0.5rem; border-radius: 4px; margin-top: 1rem; }
52
+ </style>
53
+ </head>
54
+ <body>
55
+ <div class="container">
56
+ <h1>${l}</h1>
57
+ <p>${s?"The owner has connected this domain but the project's visibility is set to private. Once they switch it to public in the workspace settings, the site will appear here automatically.":'Run <code>smbls publish</code> to deploy your project, or visit <a href="https://symbols.app">symbols.app</a> to get started.'}</p>
58
+ <span class="env">${t}${n}</span>
59
+ </div>
60
+ </body>
61
+ </html>`}export{yt as DEFAULT_TEMPLATE_HTML,C as generateSitemap,H as prefetchPageData,_t as renderNotPublishedHtml,xt as renderProjectHtml,M as transpileFunctionStrings};
@@ -0,0 +1 @@
1
+ function f(n){let t=n||"/";return t.startsWith("/")||(t="/"+t),t.length>1&&t.endsWith("/")&&(t=t.slice(0,-1)),t}function a(n,t){const r=n.split("/"),i=t.split("/"),s={};for(let e=0;e<r.length;e++)r[e].startsWith(":")&&(s[r[e].slice(1)]=i[e]);return s}function c(n,t){if(!n||!Object.keys(n).length)return null;const r=f(t);if(n[r])return{route:r,fallback:!1};const i=r.split("/");for(const s of Object.keys(n)){if(!s.includes(":"))continue;const e=s.split("/");if(e.length!==i.length)continue;let l=!0;for(let o=0;o<e.length;o++)if(!e[o].startsWith(":")&&e[o]!==i[o]){l=!1;break}if(l)return{route:s,fallback:!1,params:a(s,r)}}return r!=="/"&&n["/"]?{route:"/",fallback:!0}:null}const u=/\.(js|mjs|css|map|png|jpe?g|gif|svg|ico|webp|avif|woff2?|ttf|eot|otf|json|xml|txt|webmanifest|mp4|webm|mp3|ogg|wav|pdf)$/i;export{u as STATIC_EXT,a as extractRouteParams,f as normalizeRoute,c as resolvePageRoute};
@@ -0,0 +1 @@
1
+ import P from"path";import y from"fs";import b from"express";import D from"cors";import u from"./utils/logger.js";import{STATIC_EXT as I}from"./route.js";import{renderProjectHtml as $,renderNotPublishedHtml as w,generateSitemap as j}from"./render.js";function R(t){const e=P.resolve(t);let s=e;if(y.existsSync(e)&&y.statSync(e).isDirectory()&&(s=P.join(e,"app.json")),!y.existsSync(s))throw new Error(`JSON data file not found: ${s}`);const d=y.readFileSync(s,"utf8"),a=JSON.parse(d);return u.info(`[MERMAID] Loaded project data from ${s}`),a}function T(t,e){if(typeof t!="string")return t;if(t.includes("?")&&e){const[s,d]=t.split("?");t=s.replace(/\/$/,"");try{const a=new URLSearchParams(d);for(const[f,g]of a.entries())e.query[f]===void 0&&(e.query[f]=g)}catch(a){u.warn(`[MERMAID] Failed to parse nested query in renderUrl: ${a.message}`)}}return t.replace(/^https?:\/\//,"").split(":")[0].replace(/^www\./,"").replace(/\/$/,"")}function F(t={}){const{jsonPath:e,appkeyOverride:s,channel:d="production",getProjectData:a,cors:f=!0,editor:g}=t;let S=null;e&&(S=R(e));const c=b();return f&&c.use(D({origin:"*",methods:"GET, POST"})),c.use(b.json()),c.get("/health",(o,r)=>r.send({alive:!0,service:"mermaid",version:"3.14.0"})),c.get("*",async(o,r)=>{try{const p=o.originalUrl.split("?")[0];if(I.test(p))return p==="/favicon.ico"?r.status(204).end():r.status(404).send("Not Found");let m=s||o.query.renderUrl||o.headers.host;m=T(m,o);let i=null,l=m,v=d,M=!1;if(S)i=S,u.debug("[MERMAID] Using local JSON data");else if(a){const n=await a(o,{appkey:m});n&&(i=n.data,n.appkey&&(l=n.appkey),n.channel&&(v=n.channel),typeof n.isProd=="boolean"&&(M=n.isProd))}if(!l)return r.send("Hello, this is Mermaid (Symbols runner). Read developer documentation at https://symbols.app/developers");if(!i)return u.warn(`[MERMAID] Project not found for appkey: ${l}`),r.status(404).type("html").send(w(l,v));if(p==="/sitemap.xml"){if(j&&i.pages&&Object.keys(i.pages).length){const n=`https://${l}`,x=j(n,i.pages);return r.type("application/xml").send(x)}return r.status(404).send("No sitemap available")}const E=o.headers["x-user-id"]||"",h=await $(i,{appkey:l,pathname:p,channel:v,isProd:M,req:o,userId:E,editorBundle:g?.bundle});return r.status(h.status).set({host:"https://"+l+"/"}).type(h.contentType==="text/html"?"html":h.contentType).send(h.html)}catch(p){return u.error(`[MERMAID] Error in request: ${p}`),r.status(500).send("Internal Server Error")}}),c}export{F as createMermaidApp,R as loadJsonData};
@@ -0,0 +1 @@
1
+ function u(c){if(!c)return!1;const n=c.trim().replace(/\n\s*/g," ").trim();if(n===""||n==="{}"||n==="[]")return!1;const r=[/^\(\s*\{[^}]*\}\s*\)\s*=>/,/^(\([^)]*\)|[^=]*)\s*=>/,/^function[\s(]/,/^async\s+/,/^\(\s*function/,/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>/].some(a=>a.test(n)),i=n.startsWith("{")&&!n.includes("=>"),o=n.startsWith("["),y=/^["[{]/.test(n)&&!n.includes("=>");return r&&!i&&!o&&!y}const e=c=>typeof c=="string",t=Array.isArray,f=c=>c!==null&&typeof c=="object"&&!t(c);function p(c,n=Array.isArray(c)?[]:{}){for(const s in c){if(!Object.prototype.hasOwnProperty.call(c,s))continue;const r=c[s];if(e(r)){if(u(r))try{const i=Function("return ("+r+")")();n[s]=i}catch{n[s]=r}else n[s]=r;continue}if(t(r)){n[s]=[],r.forEach(i=>{if(e(i))if(u(i))try{const o=Function("return ("+i+")")();n[s].push(o)}catch{n[s].push(i)}else n[s].push(i);else f(i)||t(i)?n[s].push(p(i)):n[s].push(i)});continue}if(f(r)){n[s]=p(r);continue}n[s]=r}return n}function l(c,n=Array.isArray(c)?[]:{}){for(const s in c){if(!Object.prototype.hasOwnProperty.call(c,s))continue;const r=c[s];e(r)?n[s]=u(r)?null:r:t(r)?n[s]=r.map(i=>e(i)&&u(i)?null:f(i)||t(i)?l(i):i):f(r)?n[s]=l(r):n[s]=r}return n}var h=p;export{p as deepDestringifyFunctions,h as default,l as stripFunctionStrings};
@@ -0,0 +1 @@
1
+ const w=e=>{if(typeof e!="string")return null;const n=e.trim().match(/^\(?([^)]*?)\)?\s*=>\s*([\s\S]+)$/);if(!n)return null;const c=n[1],l=n[2].trim(),t=c.split(",").map(o=>o.trim()).filter(Boolean);if(l.startsWith("{"))return null;const i=h(t),r=u(l,i);return r===null?null:{schema:r,args:t}},h=e=>{const s={};return e[0]&&(s[e[0]]="el"),e[1]&&(s[e[1]]="state"),e[2]&&(s[e[2]]="context"),s},u=(e,s)=>{const n=e.trim();if(!n)return null;const c=m(n);if(c){const t=u(c.condition,s),i=u(c.consequent,s),r=u(c.alternate,s);return t!==null&&i!==null&&r!==null?{if:[t,i,r]}:null}const l=n.match(/^(\w+)\.call\((.+)\)$/);if(l){const t=g(l[2]);if(t){const i=t.map(r=>u(r.trim(),s));if(i.every(r=>r!==null))return["call",...i]}return null}{const t=f(n,"||");if(t){const i=u(t.left,s),r=u(t.right,s);return i!==null&&r!==null?["||",i,r]:null}}{const t=f(n,"&&");if(t){const i=u(t.left,s),r=u(t.right,s);return i!==null&&r!==null?["&&",i,r]:null}}for(const t of["!==","===",">=","<=",">","<"]){const i=f(n,t);if(i){const r=u(i.left,s),o=u(i.right,s);return r!==null&&o!==null?[t,r,o]:null}}if(n.startsWith("!")){const t=u(n.slice(1).trim(),s);return t!==null&&typeof t=="string"?"!"+t:null}if(n.includes("+")&&!n.startsWith('"')&&!n.startsWith("'")){const t=a(n,"+");if(t&&t.length>=2){const i=t.map(r=>u(r.trim(),s));if(i.every(r=>r!==null)){let r=i[0];for(let o=1;o<i.length;o++)r=["+",r,i[o]];return r}}}return n.startsWith('"')&&n.endsWith('"')||n.startsWith("'")&&n.endsWith("'")?"="+n.slice(1,-1):n==="void 0"||n==="undefined"?"__undefined__":/^-?\d+(\.\d+)?$/.test(n)?Number(n):n==="true"?!0:n==="false"?!1:n==="null"?null:n==='""'||n==="''"?"":/^[\w$.[\]]+$/.test(n)&&!n.includes("(")?d(n,s):null},d=(e,s)=>{const n=e.split("."),c=n[0];return s[c]?(n[0]=s[c],n.join(".")):e},m=e=>{let s=0,n=-1,c=-1;for(let l=0;l<e.length;l++){const t=e[l];if(t==="("||t==="["||t==="{")s++;else if(t===")"||t==="]"||t==="}")s--;else if(t==="?"&&s===0&&n===-1)n=l;else if(t===":"&&s===0&&n!==-1){c=l;break}if(t==='"'||t==="'"){const i=t;for(l++;l<e.length&&e[l]!==i;)e[l]==="\\"&&l++,l++}}return n===-1||c===-1?null:{condition:e.slice(0,n).trim(),consequent:e.slice(n+1,c).trim(),alternate:e.slice(c+1).trim()}},f=(e,s)=>{let n=0;const c=s.length;for(let l=0;l<e.length-c+1;l++){const t=e[l];if(t==="("||t==="["||t==="{"?n++:(t===")"||t==="]"||t==="}")&&n--,t==='"'||t==="'"){const i=t;for(l++;l<e.length&&e[l]!==i;)e[l]==="\\"&&l++,l++;continue}if(n===0&&e.slice(l,l+c)===s){const i=l>0?e[l-1]:"",r=l+c<e.length?e[l+c]:"";if(s==="="&&(i==="!"||i==="="||r==="=")||s===">"&&i==="="||s==="<"&&r==="=")continue;return{left:e.slice(0,l).trim(),right:e.slice(l+c).trim()}}}return null},a=(e,s)=>{const n=[];let c=0,l=0;const t=s.length;for(let i=0;i<e.length;i++){const r=e[i];if(r==="("||r==="["||r==="{"?c++:(r===")"||r==="]"||r==="}")&&c--,r==='"'||r==="'"){const o=r;for(i++;i<e.length&&e[i]!==o;)e[i]==="\\"&&i++,i++;continue}c===0&&e.slice(i,i+t)===s&&(n.push(e.slice(l,i)),l=i+t)}return n.push(e.slice(l)),n.length<2?null:n},g=e=>{const s=[];let n=0,c=0;for(let l=0;l<e.length;l++){const t=e[l];if(t==="("||t==="["||t==="{"?n++:t===")"||t==="]"||t==="}"?n--:t===","&&n===0&&(s.push(e.slice(c,l)),c=l+1),t==='"'||t==="'"){const i=t;for(l++;l<e.length&&e[l]!==i;)e[l]==="\\"&&l++,l++}}return s.push(e.slice(c)),s.filter(l=>l.trim())};export{w as transpile};
@@ -0,0 +1 @@
1
+ const s={trace:10,debug:20,info:30,warn:40,error:50,fatal:60};let i=process.env.LOG_LEVEL||"info";Object.prototype.hasOwnProperty.call(s,i)||(i="info");let m=s[i],b=!1;const d={trace:"\x1B[34m",debug:"\x1B[36m",info:"\x1B[32m",warn:"\x1B[33m",error:"\x1B[31m",fatal:"\x1B[35m",reset:"\x1B[0m"};function c(e,o=2){let n=String(e);for(;n.length<o;)n="0"+n;return n}function O(){const e=new Date,o=e.getUTCFullYear(),n=c(e.getUTCMonth()+1),f=c(e.getUTCDate()),t=c(e.getUTCHours()),u=c(e.getUTCMinutes()),r=c(e.getUTCSeconds()),g=c(e.getUTCMilliseconds(),3);return`${o}-${n}-${f} ${t}:${u}:${r}.${g} +0000`}function h(e){return s[e]>=m}function L(e){if(!e.length)return"";try{return JSON.stringify(Object.assign({},...e))}catch{return""}}function C(e){const o=d[e],f=e==="error"||e==="fatal"?console.error:console.log;return(...t)=>{if(!h(e))return;if(!t.length){f("");return}let u="",r=[];typeof t[0]=="string"?(u=t[0],r=t.slice(1).filter(l=>typeof l=="object"&&l!==null),e==="error"&&t[1]instanceof Error&&r.push({err:{message:t[1].message,stack:t[1].stack}})):r=t.filter(l=>typeof l=="object"&&l!==null);const g=L(r),y=O(),$=e.toUpperCase();let p=`[${y}] ${$}: ${u}`;g&&(p+=` ${g}`),b&&o&&(p=`${o}${p}${d.reset}`),f(p)}}const a={};for(const e of Object.keys(s))a[e]=C(e);Object.defineProperty(a,"level",{get:()=>i,set:e=>{Object.prototype.hasOwnProperty.call(s,e)&&(i=e,m=s[e])}});const v=e=>{e?.logging?.level&&(a.level=e.logging.level),e?.env&&(b=e.env==="local"||e.env==="development")};var x=a;export{x as default,v as updateLoggerConfig};
@@ -0,0 +1 @@
1
+ import n from"./logger.js";function s(a,l,i={}){try{const r=/(['"])\${{{\s*([^}\s]+)\s*}}}\1/g;if(!a){const e=new Error("File data is empty");throw n.error("[REQ_FLOW] File data is empty in replaceVariablesInFileSync"),e}return a.replace(r,(e,p,t)=>{if(Object.hasOwn(l,t)){const c=l[t];return i.allowScript?c:c.replace(/<\/?script>/g,"&lt;/?script&gt;")}else return n.warn(`[REQ_FLOW] Missing replacement value for ${JSON.stringify({name:t})}`),e})}catch(r){return n.error(`[REQ_FLOW] Error in replaceVariablesInFileSync: ${r}`),null}}export{s as default};
package/package.json CHANGED
@@ -1,29 +1,73 @@
1
1
  {
2
2
  "name": "@symbo.ls/mermaid",
3
- "version": "3.14.4",
3
+ "version": "3.14.5",
4
4
  "type": "module",
5
5
  "license": "CC-BY-NC-4.0",
6
6
  "description": "Self-sustaining server runner for Symbols projects from JSON. Renders DOMQL projects to HTML with optional brender SSR.",
7
- "main": "index.js",
8
7
  "exports": {
9
- ".": "./index.js",
10
- "./server": "./src/server.js",
11
- "./render": "./src/render.js",
12
- "./bundle": "./src/bundle.js",
13
- "./route": "./src/route.js",
14
- "./utils/deepDestringify": "./src/utils/deepDestringify.js",
15
- "./utils/funcql-transpile": "./src/utils/funcql-transpile.js",
16
- "./utils/vartranformer": "./src/utils/vartranformer.js",
17
- "./utils/logger": "./src/utils/logger.js",
18
- "./template.html": "./src/template.html"
8
+ ".": {
9
+ "development": "./index.js",
10
+ "import": "./dist/esm/index.js",
11
+ "default": "./dist/esm/index.js"
12
+ },
13
+ "./package.json": "./package.json",
14
+ "./server": {
15
+ "development": "./server.js",
16
+ "import": "./dist/esm/server.js",
17
+ "default": "./dist/esm/server.js"
18
+ },
19
+ "./render": {
20
+ "development": "./render.js",
21
+ "import": "./dist/esm/render.js",
22
+ "default": "./dist/esm/render.js"
23
+ },
24
+ "./bundle": {
25
+ "development": "./bundle.js",
26
+ "import": "./dist/esm/bundle.js",
27
+ "default": "./dist/esm/bundle.js"
28
+ },
29
+ "./route": {
30
+ "development": "./route.js",
31
+ "import": "./dist/esm/route.js",
32
+ "default": "./dist/esm/route.js"
33
+ },
34
+ "./utils/deepDestringify": {
35
+ "development": "./utils/deepDestringify.js",
36
+ "import": "./dist/esm/utils/deepDestringify.js",
37
+ "default": "./dist/esm/utils/deepDestringify.js"
38
+ },
39
+ "./utils/funcql-transpile": {
40
+ "development": "./utils/funcql-transpile.js",
41
+ "import": "./dist/esm/utils/funcql-transpile.js",
42
+ "default": "./dist/esm/utils/funcql-transpile.js"
43
+ },
44
+ "./utils/vartranformer": {
45
+ "development": "./utils/vartranformer.js",
46
+ "import": "./dist/esm/utils/vartranformer.js",
47
+ "default": "./dist/esm/utils/vartranformer.js"
48
+ },
49
+ "./utils/logger": {
50
+ "development": "./utils/logger.js",
51
+ "import": "./dist/esm/utils/logger.js",
52
+ "default": "./dist/esm/utils/logger.js"
53
+ },
54
+ "./template.html": {
55
+ "development": "./template.html.js",
56
+ "import": "./dist/esm/template.html.js",
57
+ "default": "./dist/esm/template.html.js"
58
+ }
19
59
  },
20
60
  "files": [
21
- "index.js",
22
- "src",
61
+ "dist",
62
+ "*.md",
63
+ "LICENSE",
23
64
  "README.md"
24
65
  ],
25
66
  "scripts": {
26
- "start": "node bin/start.js"
67
+ "start": "node bin/start.js",
68
+ "build:esm": "esbuild $(find . -type f -name \"*.js\" -not -path \"*/dist/*\" -not -path \"*/node_modules/*\" -not -path \"*/__tests__/*\" -not -path \"*/test/*\" -not -name \"*.test.js\") --target=es2022 --format=esm --outbase=. --outdir=dist/esm --minify --log-level=warning",
69
+ "build": "rimraf dist && npm run build:esm",
70
+ "prepublishOnly": "npm run build"
27
71
  },
28
72
  "bin": {
29
73
  "mermaid": "./bin/start.js"
@@ -37,6 +81,7 @@
37
81
  "@symbo.ls/brender": "^3.14.0"
38
82
  },
39
83
  "publishConfig": {
40
- "access": "public"
41
- }
84
+ "access": "restricted"
85
+ },
86
+ "module": "./dist/esm/index.js"
42
87
  }
package/index.js DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * @symbo.ls/mermaid — self-sustaining server runner for Symbols projects.
3
- *
4
- * Renders DOMQL projects from JSON to HTML, with optional brender SSR.
5
- * Works as an Express server, a pure render function, or in CF Workers.
6
- */
7
-
8
- export { createMermaidApp, loadJsonData } from './src/server.js';
9
- export {
10
- renderProjectHtml,
11
- renderNotPublishedHtml,
12
- transpileFunctionStrings,
13
- generateSitemap,
14
- prefetchPageData,
15
- } from './src/render.js';
16
- export { getBundleScript, getSmblsIife } from './src/bundle.js';
17
- export { normalizeRoute, resolvePageRoute, extractRouteParams, STATIC_EXT } from './src/route.js';
18
- export { deepDestringifyFunctions, stripFunctionStrings } from './src/utils/deepDestringify.js';
19
- export { transpile as transpileFuncql } from './src/utils/funcql-transpile.js';
20
- export { default as replaceVariablesInFileSync } from './src/utils/vartranformer.js';
21
- export { default as log } from './src/utils/logger.js';
package/src/bundle.js DELETED
@@ -1,217 +0,0 @@
1
- import log from './utils/logger.js';
2
- // The smbls IIFE source as a string. Single source of truth — generated by
3
- // `smbls build:iife` and exported from the smbls package itself, so this works
4
- // identically in Node, CF Workers, and any other runtime as a static import.
5
- import smblsIife from 'smbls/iife-string';
6
-
7
- log.debug(`[BUNDLE] smbls IIFE loaded from smbls/iife-string (${smblsIife.length} bytes)`);
8
-
9
- // FRAMEWORK-6: any `<` in JSON that gets inlined into a `<script>` block can
10
- // terminate the wrapper (`</script>`) or trigger HTML-comment / CDATA modes
11
- // (`<!--`, `<![CDATA[`) inside the script-data state. Replacing every `<`
12
- // with the JS unicode escape `<` neutralizes HTML interpretation while
13
- // preserving the runtime string value — `<` decodes to `<` at JS parse
14
- // time, so the embedded object is identical to the original. Standard
15
- // mitigation: see OWASP §RULE-3.1, React's renderToString, Rails json_escape.
16
- const escapeJsonForScript = (s) => s.replace(/</g, '\\u003c');
17
-
18
- export function getSmblsIife() {
19
- return smblsIife;
20
- }
21
-
22
- /**
23
- * Generate the client-side bundle script that creates the Smbls app.
24
- * Uses the same approach as templates/start.js runFromJson:
25
- * Smbls.deepDestringifyFunctions(data) -> Smbls.create(app, context)
26
- */
27
- export function getBundleScript(appKey, data, req, { ProjectDataService } = {}) {
28
- log.debug(`[BUNDLE] getBundleScript started for ${appKey}`);
29
-
30
- if (!data) {
31
- log.warn('[BUNDLE] No data provided, returning empty');
32
- return '';
33
- }
34
-
35
- // Ensure data is fully merged before bundling
36
- try {
37
- if (data.merged) {
38
- Object.assign(data, data.merged);
39
- log.debug(`[BUNDLE] Applied precomputed merged data for project ${data.id}`);
40
- } else if (
41
- ProjectDataService &&
42
- Array.isArray(data.sharedLibraries) &&
43
- data.sharedLibraries.length > 0
44
- ) {
45
- const mergedData = ProjectDataService.mergeSharedLibraries(
46
- data,
47
- data.sharedLibraries,
48
- data.projectType,
49
- );
50
- Object.assign(data, mergedData);
51
- log.debug(`[BUNDLE] Merged ${data.sharedLibraries.length} shared libraries`);
52
- }
53
- } catch (mergeError) {
54
- log.error(`[BUNDLE] Failed to ensure merged data: ${mergeError.message}`);
55
- }
56
-
57
- // Serialize the project data as JSON (same as templates __mockedData approach).
58
- // escapeJsonForScript: see FRAMEWORK-6 — any `</script>` (or `<!--`) inside a
59
- // string value would break out of the inline `<script>` wrapper.
60
- const jsonData = escapeJsonForScript(JSON.stringify(data));
61
-
62
- /*
63
- * Determine liveSync setting.
64
- *
65
- * Realtime is enabled on Symbols-managed dev hosts (any *.symbo.ls env,
66
- * *.preview.symbols.app, and localhost / *.localhost for local dev) and
67
- * disabled on custom domains so we don't leak the realtime backend to
68
- * third-party hosts. Post-§45 the appkey is a bare `owner/key` slug, so
69
- * gating on `appKey.endsWith('symbo.ls')` would always evaluate false
70
- * (rendering all browsers blind). The signal we want is the request host.
71
- */
72
- // Resolve incoming host across Express (req.headers.host),
73
- // Cloudflare Workers / Fetch (req.headers is a Headers instance with .get),
74
- // and plain options bags ({ host }). Falls back to req.url parsing so a
75
- // missing Host header still degrades to a URL-based read instead of ''.
76
- const rawHost =
77
- (req?.headers && typeof req.headers.get === 'function' && req.headers.get('host')) ||
78
- req?.headers?.host ||
79
- req?.host ||
80
- (req?.url && (() => { try { return new URL(req.url).host; } catch { return ''; } })()) ||
81
- '';
82
- const host = String(rawHost).toLowerCase().split(':')[0];
83
- // Express exposes parsed query on req.query; CF Workers expose req.url only —
84
- // pull `release` / `version` from a parsed URL when the structured form is
85
- // absent so feature flags work in both runtimes.
86
- const queryFrom = (() => {
87
- if (req?.query) return req.query;
88
- try { return Object.fromEntries(new URL(req?.url || '', 'http://x').searchParams); } catch { return {}; }
89
- })();
90
- const release = queryFrom.release || false;
91
- const version = queryFrom.version;
92
- const isSymboLs =
93
- host.endsWith('.symbo.ls') ||
94
- host.endsWith('.preview.symbols.app') ||
95
- host === 'localhost' ||
96
- host.endsWith('.localhost');
97
- const shouldDisableLiveSync = Boolean(release) || Boolean(version) || !isSymboLs;
98
-
99
- // Inject the persisted `_id` so the realtime client can hand the socket
100
- // server a concrete projectId (mongo _id) on handshake. Without this, the
101
- // server falls back to a `/v1/projects/by-key/...` lookup that 404s on the
102
- // current API and drops the connection mid-join.
103
- const projectId =
104
- (data && (data._id || data.projectId || data.id)) ||
105
- null;
106
- const settingsStr = isSymboLs
107
- ? escapeJsonForScript(JSON.stringify({
108
- ...data.settings,
109
- liveSync: !shouldDisableLiveSync,
110
- inspect: true,
111
- verbose: !shouldDisableLiveSync,
112
- projectId,
113
- }))
114
- : 'null';
115
-
116
- // Generate the client script - same pattern as templates/start.js runFromJson
117
- const script = `
118
- var context = Smbls.deepDestringifyFunctions(${jsonData});
119
- context.packageManager = 'esm.sh';
120
- if (context.db && context.db.createClient) delete context.db.createClient;
121
- if (context.fetch && context.fetch.createClient) delete context.fetch.createClient;
122
-
123
- // Runtime defaults (these are baked into the starter-kit/frank but missing from server data)
124
- if (context.useReset === undefined) context.useReset = true;
125
- if (context.useVariable === undefined) context.useVariable = true;
126
- if (context.useFontImport === undefined) context.useFontImport = true;
127
- if (context.useIconSprite === undefined) context.useIconSprite = true;
128
- if (context.useSvgSprite === undefined) context.useSvgSprite = true;
129
- if (context.useDefaultConfig === undefined) context.useDefaultConfig = true;
130
- if (context.useDocumentTheme === undefined) context.useDocumentTheme = true;
131
- if (!context.routerOptions) context.routerOptions = { scrollToTop: true };
132
-
133
- var app = context.app || {};
134
- delete context.app;
135
- var settings = ${settingsStr};
136
- if (settings) context.editor = settings;
137
-
138
- // Theme initialization: set data-theme before app renders to avoid FOUC.
139
- // Mirrors the precedence in resolveAndApplyTheme (packages/smbls/src/index.js):
140
- // 1. URL param ?globalTheme=<name> — embedded-iframe override
141
- // 2. localStorage[themeStorageKey] — persisted user choice
142
- // 3. context.globalTheme — project's configured value
143
- // 4. matchMedia(prefers-color-scheme: dark) — OS fallback (when 'auto')
144
- //
145
- // Writing the literal 'auto' as data-theme would leave the
146
- // [data-theme="dark"] / [data-theme="light"] CSS selectors unmatched and
147
- // the page would render with system defaults instead of the chosen
148
- // theme — so 'auto' is always resolved to a concrete light/dark value.
149
- //
150
- // PRIOR BUG: this script previously ignored context.globalTheme entirely
151
- // and went straight to matchMedia after localStorage. A project with
152
- // globalTheme: light in config.js would render in OS dark on a
153
- // dark-mode device because the inline boot ran first and locked in
154
- // dark before resolveAndApplyTheme could correct it.
155
- //
156
- // IDEMPOTENT GUARD: the SSR-side render in @symbo.ls/mermaid/render.js
157
- // now bakes data-theme onto <html> for concrete-theme projects and
158
- // emits an inline <head> script that runs synchronously before paint.
159
- // When that path runs, data-theme is already set by the time the bundle
160
- // executes — skip this block to avoid re-resolving and overwriting
161
- // (which would race with URL/localStorage values the head script
162
- // already honored). Kept as a safety net for older renders / cases
163
- // where the head script couldn't run.
164
- if (
165
- context.useDocumentTheme !== false &&
166
- !document.documentElement.hasAttribute('data-theme')
167
- ) {
168
- (function() {
169
- try {
170
- var t = null;
171
- // 1. URL param wins (used by preview/canvas iframe embedding).
172
- try {
173
- var p = new URLSearchParams(window.location.search).get('globalTheme');
174
- if (p) t = p;
175
- } catch(e) {}
176
- // 2. localStorage — only consulted when no forcing source above.
177
- if (!t) {
178
- var key = context.themeStorageKey;
179
- if (key) { try { t = localStorage.getItem(key) } catch(e) {} }
180
- }
181
- // 3. Project's configured globalTheme (config.js export). Honors an
182
- // explicit 'light'/'dark'/<scheme> instead of falling through to OS.
183
- if (!t && context.globalTheme && context.globalTheme !== 'auto') {
184
- t = context.globalTheme;
185
- }
186
- // 4. OS preference fallback — only when nothing concrete chose.
187
- if (!t || t === 'auto') {
188
- t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
189
- }
190
- if (t) document.documentElement.setAttribute('data-theme', t);
191
- } catch(e) {}
192
- })();
193
- }
194
-
195
- // When brender pre-rendered this page, adopt the existing DOM nodes
196
- // instead of tearing them down. The [data-ssr] wrapper is moved out of
197
- // the way so pre-rendered children land at document.body level — matching
198
- // the level at which DOMQL creates its own elements. The __BRENDER__ flag
199
- // and __BR_REGISTRY__ map were injected into <head> by the server.
200
- var ssrShell = document.querySelector('[data-ssr]');
201
- if (window.__BRENDER__ && ssrShell) {
202
- // Move pre-rendered children to body before DOMQL skeleton creation
203
- var frag = document.createDocumentFragment();
204
- while (ssrShell.firstChild) frag.appendChild(ssrShell.firstChild);
205
- document.body.insertBefore(frag, ssrShell);
206
- ssrShell.remove();
207
- } else if (ssrShell) {
208
- // No brender hydration — remove empty/stale SSR shell
209
- ssrShell.remove();
210
- }
211
-
212
- Smbls.create(app, context);
213
- `;
214
-
215
- log.debug(`[BUNDLE] Script generated (${script.length} bytes)`);
216
- return script;
217
- }