@aihu/app 2.0.0 → 2.0.1
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 +7 -7
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npm install @aihu/app
|
|
|
21
21
|
bun add @aihu/app
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
<sub><i>Auto-generated against `@aihu/app@2.0.
|
|
24
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.1`.</i></sub>
|
|
25
25
|
|
|
26
26
|
<!-- END_AUTOGEN: install -->
|
|
27
27
|
|
|
@@ -32,12 +32,12 @@ bun add @aihu/app
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
|---|---|
|
|
35
|
-
| **Version** | `2.0.
|
|
35
|
+
| **Version** | `2.0.1` |
|
|
36
36
|
| **Tier** | B — Meta-framework — top-level integration of runtime, router, adapter |
|
|
37
37
|
| **Published files** | 3 entries |
|
|
38
38
|
| **License** | MIT |
|
|
39
39
|
|
|
40
|
-
<sub><i>Auto-generated against `@aihu/app@2.0.
|
|
40
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.1`.</i></sub>
|
|
41
41
|
|
|
42
42
|
<!-- END_AUTOGEN: stats -->
|
|
43
43
|
|
|
@@ -51,7 +51,7 @@ bun add @aihu/app
|
|
|
51
51
|
| `.` | `./dist/index.js` | `—` |
|
|
52
52
|
| `./client` | `./dist/client.js` | `—` |
|
|
53
53
|
|
|
54
|
-
<sub><i>Auto-generated against `@aihu/app@2.0.
|
|
54
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.1`.</i></sub>
|
|
55
55
|
|
|
56
56
|
<!-- END_AUTOGEN: exports -->
|
|
57
57
|
|
|
@@ -69,7 +69,7 @@ bun add @aihu/app
|
|
|
69
69
|
- `@aihu/signals` — `workspace:*`
|
|
70
70
|
- `vite` — `>=5.0.0`
|
|
71
71
|
|
|
72
|
-
<sub><i>Auto-generated against `@aihu/app@2.0.
|
|
72
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.1`.</i></sub>
|
|
73
73
|
|
|
74
74
|
<!-- END_AUTOGEN: deps -->
|
|
75
75
|
|
|
@@ -83,7 +83,7 @@ bun add @aihu/app
|
|
|
83
83
|
- [@aihu/adapter-cloudflare](../adapter-cloudflare)
|
|
84
84
|
- [Aihu framework root](../../README.md)
|
|
85
85
|
|
|
86
|
-
<sub><i>Auto-generated against `@aihu/app@2.0.
|
|
86
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.1`.</i></sub>
|
|
87
87
|
|
|
88
88
|
<!-- END_AUTOGEN: see-also -->
|
|
89
89
|
|
|
@@ -94,6 +94,6 @@ bun add @aihu/app
|
|
|
94
94
|
|
|
95
95
|
MIT — see [LICENSE](../../LICENSE).
|
|
96
96
|
|
|
97
|
-
<sub><i>Auto-generated against `@aihu/app@2.0.
|
|
97
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.1`.</i></sub>
|
|
98
98
|
|
|
99
99
|
<!-- END_AUTOGEN: license -->
|
package/dist/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"virtual:aihu-layouts";import t from"virtual:aihu-routes";import{hydrate as n,mount as r}from"@aihu/arbor";import{createRouter as i}from"@aihu/router";import{_setHydrate as a,_setMount as o,_setSignal as s}from"@aihu/runtime";import{routeHeadToSsrHead as c}from"@aihu/server/head-lowering";import{signal as l}from"@aihu/signals";const u=`data-aihu-head`;function d(e){return typeof e.name==`string`?{attr:`name`,value:e.name}:typeof e.property==`string`?{attr:`property`,value:e.property}:null}function f(e){let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n]=String(r));return t}function p(e){return{title:e.title,metas:(e.meta??[]).map(e=>f(e)),links:(e.links??[]).map(e=>f(e)),scripts:(e.scripts??[]).map(e=>({type:e.type,content:e.content}))}}function m(e=document){let t=e.head;if(t)for(let e of Array.from(t.querySelectorAll(`[${u}]`)))e.remove()}function h(e,t=document){let n=t.head;if(!n)return;m(t);let{title:r,metas:i,links:a,scripts:o}=p(e);r!==void 0&&(t.title=r);for(let e of i){let r=d(e),i=null;r&&(i=n.querySelector(`meta[${r.attr}="${_(r.value)}"]`)),i?i.setAttribute(u,``):(i=t.createElement(`meta`),i.setAttribute(u,``),n.appendChild(i)),g(i,e)}for(let e of a){let r=(e.rel??``).toLowerCase()===`canonical`,i=null;r&&(i=n.querySelector(`link[rel="canonical"]`)),i?i.setAttribute(u,``):(i=t.createElement(`link`),i.setAttribute(u,``),n.appendChild(i)),g(i,e)}for(let e of o){let r=n.querySelector(`script[type="${_(e.type)}"]`);r?r.setAttribute(u,``):(r=t.createElement(`script`),r.type=e.type,r.setAttribute(u,``),n.appendChild(r)),r.textContent=e.content}}function g(e,t){for(let[n,r]of Object.entries(t))e.setAttribute(n,r)}function _(e){let t=globalThis;return t.CSS&&typeof t.CSS.escape==`function`?t.CSS.escape(e):e.replace(/["\\]/g,`\\$&`)}function v(u){u?.provide&&Object.assign(globalThis,u.provide),o(r),s(l),u?.rendering?.mode!==`spa`&&a(n);let d=u?.outletId??`outlet`,f=document.getElementById(d);if(!f)throw Error(`@aihu/app: no element with id="${d}" found. Add <div id="${d}"></div> to your index.html`);let p=f,g=i(t),_=u?.site?.url,v=u?.head;function y(e){if(e===void 0&&v===void 0){m();return}h(c(e,{..._===void 0?{}:{siteUrl:_},...v===void 0?{}:{globalHead:v}}))}let b=null,x;async function S(n){if(b=n,!n){let e=t.find(e=>e.pattern===`*`||e.name===`not-found`);if(e){await e.module();let t=e.name;if(t?.includes(`-`)){y(e.head),p.replaceChildren(document.createElement(t));return}}y(void 0);let n=document.createElement(`p`);n.style.cssText=`font-family:system-ui;padding:2rem;color:#888`,n.textContent=`404 — page not found`,p.replaceChildren(n);return}y(n.route.head),await n.route.module();let r=n.route.name;if(!r?.includes(`-`))return;let i=document.createElement(r);if(n.params)for(let[e,t]of Object.entries(n.params))i.setAttribute(e,String(t));let a=x===void 0?n.route.layout:x??void 0,o=a?e[a]:void 0;if(o){await o.load();let e=document.createElement(o.tag);p.replaceChildren(e);let t=e.shadowRoot??e,n=t.querySelector(`[data-aihu-outlet]`);n?n.replaceChildren(i):(console.warn(`[@aihu/app] layout "${a}" has no <$outlet>`),t.appendChild(i));return}p.replaceChildren(i)}function C(e){x=void 0,S(e)}function w(e){return x=e,S(b)}return C(g.match(location.pathname)),document.addEventListener(`click`,e=>{let t=e.
|
|
1
|
+
import e from"virtual:aihu-layouts";import t from"virtual:aihu-routes";import{hydrate as n,mount as r}from"@aihu/arbor";import{createRouter as i}from"@aihu/router";import{_setHydrate as a,_setMount as o,_setSignal as s}from"@aihu/runtime";import{routeHeadToSsrHead as c}from"@aihu/server/head-lowering";import{signal as l}from"@aihu/signals";const u=`data-aihu-head`;function d(e){return typeof e.name==`string`?{attr:`name`,value:e.name}:typeof e.property==`string`?{attr:`property`,value:e.property}:null}function f(e){let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n]=String(r));return t}function p(e){return{title:e.title,metas:(e.meta??[]).map(e=>f(e)),links:(e.links??[]).map(e=>f(e)),scripts:(e.scripts??[]).map(e=>({type:e.type,content:e.content}))}}function m(e=document){let t=e.head;if(t)for(let e of Array.from(t.querySelectorAll(`[${u}]`)))e.remove()}function h(e,t=document){let n=t.head;if(!n)return;m(t);let{title:r,metas:i,links:a,scripts:o}=p(e);r!==void 0&&(t.title=r);for(let e of i){let r=d(e),i=null;r&&(i=n.querySelector(`meta[${r.attr}="${_(r.value)}"]`)),i?i.setAttribute(u,``):(i=t.createElement(`meta`),i.setAttribute(u,``),n.appendChild(i)),g(i,e)}for(let e of a){let r=(e.rel??``).toLowerCase()===`canonical`,i=null;r&&(i=n.querySelector(`link[rel="canonical"]`)),i?i.setAttribute(u,``):(i=t.createElement(`link`),i.setAttribute(u,``),n.appendChild(i)),g(i,e)}for(let e of o){let r=n.querySelector(`script[type="${_(e.type)}"]`);r?r.setAttribute(u,``):(r=t.createElement(`script`),r.type=e.type,r.setAttribute(u,``),n.appendChild(r)),r.textContent=e.content}}function g(e,t){for(let[n,r]of Object.entries(t))e.setAttribute(n,r)}function _(e){let t=globalThis;return t.CSS&&typeof t.CSS.escape==`function`?t.CSS.escape(e):e.replace(/["\\]/g,`\\$&`)}function v(u){u?.provide&&Object.assign(globalThis,u.provide),o(r),s(l),u?.rendering?.mode!==`spa`&&a(n);let d=u?.outletId??`outlet`,f=document.getElementById(d);if(!f)throw Error(`@aihu/app: no element with id="${d}" found. Add <div id="${d}"></div> to your index.html`);let p=f,g=i(t),_=u?.site?.url,v=u?.head;function y(e){if(e===void 0&&v===void 0){m();return}h(c(e,{..._===void 0?{}:{siteUrl:_},...v===void 0?{}:{globalHead:v}}))}let b=null,x;async function S(n){if(b=n,!n){let e=t.find(e=>e.pattern===`*`||e.name===`not-found`);if(e){await e.module();let t=e.name;if(t?.includes(`-`)){y(e.head),p.replaceChildren(document.createElement(t));return}}y(void 0);let n=document.createElement(`p`);n.style.cssText=`font-family:system-ui;padding:2rem;color:#888`,n.textContent=`404 — page not found`,p.replaceChildren(n);return}y(n.route.head),await n.route.module();let r=n.route.name;if(!r?.includes(`-`))return;let i=document.createElement(r);if(n.params)for(let[e,t]of Object.entries(n.params))i.setAttribute(e,String(t));let a=x===void 0?n.route.layout:x??void 0,o=a?e[a]:void 0;if(o){await o.load();let e=document.createElement(o.tag);p.replaceChildren(e);let t=e.shadowRoot??e,n=t.querySelector(`[data-aihu-outlet]`);n?n.replaceChildren(i):(console.warn(`[@aihu/app] layout "${a}" has no <$outlet>`),t.appendChild(i));return}p.replaceChildren(i)}function C(e){x=void 0,S(e)}function w(e){return x=e,S(b)}return C(g.match(location.pathname)),document.addEventListener(`click`,e=>{let t=e.composedPath().find(e=>e instanceof HTMLAnchorElement);if(!t)return;let n=t.getAttribute(`href`);!n||n.startsWith(`http`)||n.startsWith(`//`)||n.startsWith(`mailto:`)||(e.preventDefault(),history.pushState({},``,n),C(g.match(location.pathname)))}),window.addEventListener(`popstate`,()=>{C(g.match(location.pathname))}),{setLayout:w}}export{v as createApp};
|
|
2
2
|
//# sourceMappingURL=client.js.map
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../src/head-apply.ts","../src/client.ts"],"sourcesContent":["/**\n * Shared head-application core (SEO arc).\n *\n * A single `HeadConfig` → keyed-tag decomposition feeds BOTH appliers so the\n * server-side (build-time) and client-side (runtime) paths can never diverge on\n * how a meta/link/script is keyed, merged, or escaped:\n *\n * - `applyHeadToHtml` — B4 (SSG prerender, `prerender.ts`): regex-rewrites a\n * built `index.html` string. Build-time only, never shipped to the browser.\n * - `applyHeadToDocument` — B5 (client-nav, `client.ts`): mutates the LIVE\n * `document.head` on SPA navigation and tracks the per-page tags it owns so\n * they can be cleaned up on the next navigation.\n *\n * This module is PURE (no `node:` builtin, no module-level DOM access) so it is\n * safe in the browser-edge `dist/client.js` bundle that `check:runtime-purity`\n * guards. The DOM applier receives its `Document` as an argument (defaulting to\n * the ambient `document` only when called) — there is no top-level `document`\n * reference, so importing this module never assumes a DOM.\n */\n\nimport type { HeadConfig } from '@aihu/server/head-lowering'\n\n/**\n * Attribute stamped on every tag the head appliers own. Tags carrying it are\n * \"managed\" per-page head — removed and re-applied on each navigation. Tags\n * WITHOUT it (e.g. the source `index.html`'s baseline `<meta charset>` /\n * `<title>` and any unmanaged scaffold tags) are left untouched, so a route\n * that drops a field falls back to whatever the document already shipped.\n */\nexport const MANAGED_HEAD_ATTR = 'data-aihu-head'\n\n// ---------------------------------------------------------------------------\n// Keying — the single source of truth shared by both appliers.\n// ---------------------------------------------------------------------------\n\n/**\n * Stable identity for a meta tag, used to upsert (replace-in-place) rather than\n * accumulate duplicates: name wins, then property, else `null` (always inject).\n */\nexport function metaKey(\n attrs: Record<string, string | undefined>,\n): { attr: 'name' | 'property'; value: string } | null {\n if (typeof attrs.name === 'string') return { attr: 'name', value: attrs.name }\n if (typeof attrs.property === 'string') return { attr: 'property', value: attrs.property }\n return null\n}\n\n/** Plain attribute records for each tag class, with `undefined` values dropped. */\nfunction definedAttrs(tag: Record<string, string | undefined>): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(tag)) {\n if (v !== undefined) out[k] = String(v)\n }\n return out\n}\n\ninterface DecomposedHead {\n title: string | undefined\n metas: Array<Record<string, string>>\n links: Array<Record<string, string>>\n scripts: Array<{ type: string; content: string }>\n}\n\n/** Decompose a lowered HeadConfig into plain, escape-free tag records. */\nexport function decomposeHead(head: HeadConfig): DecomposedHead {\n return {\n title: head.title,\n metas: (head.meta ?? []).map((m) => definedAttrs(m)),\n links: (head.links ?? []).map((l) => definedAttrs(l)),\n scripts: (head.scripts ?? []).map((s) => ({ type: s.type, content: s.content })),\n }\n}\n\n// ---------------------------------------------------------------------------\n// String applier (B4 — SSG prerender). Mirrors the DOM applier's keying.\n// ---------------------------------------------------------------------------\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"')\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\nfunction attrsToHtml(attrs: Record<string, string>): string {\n return Object.entries(attrs)\n .map(([k, v]) => `${k}=\"${escapeAttr(v)}\"`)\n .join(' ')\n}\n\n/**\n * Apply a lowered HeadConfig onto a built `index.html` template string.\n *\n * - `<title>` is replaced (or injected when absent).\n * - Each meta is replaced in place when a tag with the same name/property\n * already exists; otherwise injected before `</head>`.\n * - canonical link replaces an existing `rel=\"canonical\"`; other links + all\n * scripts (JSON-LD) are injected before `</head>`.\n *\n * Build-time only — the regex transform is never shipped to the client.\n */\nexport function applyHeadToHtml(html: string, head: HeadConfig): string {\n let out = html\n const inject: string[] = []\n const { title, metas, links, scripts } = decomposeHead(head)\n\n if (title !== undefined) {\n const tag = `<title>${escapeText(title)}</title>`\n if (/<title[^>]*>[\\s\\S]*?<\\/title>/i.test(out)) {\n out = out.replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, tag)\n } else {\n inject.push(tag)\n }\n }\n\n for (const meta of metas) {\n const metaTag = `<meta ${attrsToHtml(meta)}>`\n const key = metaKey(meta)\n if (key) {\n const escaped = key.value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n const re = new RegExp(`<meta\\\\s+[^>]*${key.attr}=\"${escaped}\"[^>]*>`, 'i')\n if (re.test(out)) {\n out = out.replace(re, metaTag)\n continue\n }\n }\n inject.push(metaTag)\n }\n\n for (const link of links) {\n const linkTag = `<link ${attrsToHtml(link)}>`\n if ((link.rel ?? '').toLowerCase() === 'canonical') {\n const re = /<link\\s+[^>]*rel=\"canonical\"[^>]*>/i\n if (re.test(out)) {\n out = out.replace(re, linkTag)\n continue\n }\n }\n inject.push(linkTag)\n }\n\n for (const script of scripts) {\n // Element text — neutralize a literal `</` so injected `</script>` can't\n // break out (matches @aihu/server's buildHead guard).\n const body = script.content.replace(/<\\//g, '<\\\\/')\n inject.push(`<script type=\"${escapeAttr(script.type)}\">${body}</script>`)\n }\n\n if (inject.length === 0) return out\n const block = inject.join('\\n ')\n if (/<\\/head>/i.test(out)) {\n return out.replace(/<\\/head>/i, ` ${block}\\n </head>`)\n }\n return `${out}\\n${block}`\n}\n\n// ---------------------------------------------------------------------------\n// DOM applier (B5 — client-side SPA navigation).\n// ---------------------------------------------------------------------------\n\n/**\n * Remove every per-page head tag previously applied by `applyHeadToDocument`\n * (those stamped with {@link MANAGED_HEAD_ATTR}). Source/baseline tags and any\n * tag the appliers did not create are left in place.\n *\n * Called at the start of every navigation so a route that omits a field (or\n * navigating back to a route with a different head) never leaves a stale\n * title/canonical/OG/JSON-LD behind.\n */\nexport function clearManagedHead(doc: Document = document): void {\n const head = doc.head\n if (!head) return\n for (const el of Array.from(head.querySelectorAll(`[${MANAGED_HEAD_ATTR}]`))) {\n el.remove()\n }\n}\n\n/**\n * Apply a lowered HeadConfig to the LIVE `document.head` for SPA navigation.\n *\n * Idempotent per navigation: first clears the previously-managed per-page tags\n * ({@link clearManagedHead}), then applies the new config, stamping each tag it\n * creates with {@link MANAGED_HEAD_ATTR}. Because the lowered config already\n * folds in the global `app.head` defaults (via `routeHeadToSsrHead`'s\n * `globalHead`), re-applying the full set every navigation keeps those defaults\n * present while dropping route-only tags from the prior page.\n *\n * - `<title>`: the document title is set via `document.title`. (Title is a\n * singleton — never stamped/removed; an absent title leaves the prior one,\n * but the lowered config carries the global default title so it is restored.)\n * - meta: upserted by name/property — an existing managed-or-source tag with\n * the same key is updated in place; otherwise a fresh managed tag is appended.\n * - link: `rel=\"canonical\"` upserts the existing canonical; other links append.\n * - script: JSON-LD upserted by `type`.\n */\nexport function applyHeadToDocument(head: HeadConfig, doc: Document = document): void {\n const headEl = doc.head\n if (!headEl) return\n\n clearManagedHead(doc)\n\n const { title, metas, links, scripts } = decomposeHead(head)\n\n if (title !== undefined) {\n doc.title = title\n }\n\n for (const meta of metas) {\n const key = metaKey(meta)\n let el: HTMLMetaElement | null = null\n if (key) {\n el = headEl.querySelector<HTMLMetaElement>(`meta[${key.attr}=\"${cssEscape(key.value)}\"]`)\n }\n if (!el) {\n el = doc.createElement('meta')\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n headEl.appendChild(el)\n } else {\n // Re-stamp so an upserted source/global tag is cleaned up with the rest.\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n }\n setAttrs(el, meta)\n }\n\n for (const link of links) {\n const isCanonical = (link.rel ?? '').toLowerCase() === 'canonical'\n let el: HTMLLinkElement | null = null\n if (isCanonical) {\n el = headEl.querySelector<HTMLLinkElement>('link[rel=\"canonical\"]')\n }\n if (!el) {\n el = doc.createElement('link')\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n headEl.appendChild(el)\n } else {\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n }\n setAttrs(el, link)\n }\n\n for (const script of scripts) {\n let el = headEl.querySelector<HTMLScriptElement>(`script[type=\"${cssEscape(script.type)}\"]`)\n if (!el) {\n el = doc.createElement('script')\n el.type = script.type\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n headEl.appendChild(el)\n } else {\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n }\n // textContent — the DOM never re-parses script text as HTML, so JSON-LD is\n // safe verbatim (no `</script>` escaping needed, unlike the string path).\n el.textContent = script.content\n }\n}\n\n/** Set/refresh attributes on an element from a plain record. */\nfunction setAttrs(el: Element, attrs: Record<string, string>): void {\n for (const [k, v] of Object.entries(attrs)) {\n el.setAttribute(k, v)\n }\n}\n\n/**\n * Escape a value for use inside a `[attr=\"…\"]` CSS attribute selector. Uses the\n * platform `CSS.escape` when available, with a minimal fallback (quotes +\n * backslashes) for environments without it.\n */\nfunction cssEscape(value: string): string {\n const g = globalThis as { CSS?: { escape?: (v: string) => string } }\n if (g.CSS && typeof g.CSS.escape === 'function') return g.CSS.escape(value)\n return value.replace(/[\"\\\\]/g, '\\\\$&')\n}\n","import layouts from 'virtual:aihu-layouts'\nimport routes from 'virtual:aihu-routes'\nimport { hydrate, mount } from '@aihu/arbor'\nimport type { MatchResult, RouteDefinition, RouteHead } from '@aihu/router'\nimport { createRouter } from '@aihu/router'\nimport { _setHydrate, _setMount, _setSignal } from '@aihu/runtime'\n// Pure subpath — NOT the @aihu/server barrel. The barrel reaches loader.ts +\n// the lazy native loader; importing it would risk dragging node:-bearing code\n// into the browser client bundle and trip check:runtime-purity. head-lowering.ts\n// is side-effect free (only the web-standard URL), so this stays node:-free.\nimport type { HeadConfig } from '@aihu/server/head-lowering'\nimport { routeHeadToSsrHead } from '@aihu/server/head-lowering'\nimport { signal } from '@aihu/signals'\nimport { applyHeadToDocument, clearManagedHead } from './head-apply.ts'\n\n/**\n * Rendering mode passed from the server config into the client bootstrap.\n * Inlined here to avoid importing @aihu/server into the client bundle.\n * Must stay in sync with RenderingMode in @aihu/server.\n */\nexport type AppRenderingMode = 'ssr' | 'spa' | 'hybrid'\n\n/** Inline runtime configuration accepted by createApp(). All fields optional. */\nexport interface AppConfig {\n /** Id of the outlet element in index.html. Default: 'outlet' */\n outletId?: string\n /**\n * App-level values hoisted into globalThis before any component runs.\n * Use this for singletons (db clients, auth helpers, i18n) that are\n * referenced as bare identifiers inside @state blocks.\n *\n * @example\n * createApp({ provide: { supabase, checkAuth } })\n */\n provide?: Record<string, unknown>\n /**\n * Rendering mode from the server config. Controls whether the client\n * wires the hydration function into the runtime.\n *\n * - 'ssr' | 'hybrid' (default): wires _setHydrate so the client can\n * take over from server-rendered HTML without re-creating DOM.\n * - 'spa': skips _setHydrate — no SSR HTML to hydrate, mount-only.\n *\n * Pass `defineAihuConfig(…).rendering?.mode` from your server config.\n * Default: 'ssr' (hydration wired).\n */\n rendering?: { mode?: AppRenderingMode }\n /**\n * Site-level config. `site.url` is the absolute base URL used to resolve\n * relative per-route `canonical` / `og:*` / `twitter:*` values into absolute\n * URLs as the head is applied on client navigation (mirrors the SSG path's\n * `AihuConfig.site.url`). When absent, relative values are emitted unchanged.\n */\n site?: { url?: string }\n /**\n * Global `<head>` defaults (typically `aihu.config.ts`'s `app.head`). On every\n * navigation these defaults are folded under the active route's head\n * (`routeHeadToSsrHead`'s `globalHead`) and re-applied — so a route that omits\n * a field falls back to the global default, and global tags persist across\n * navigations while route-only tags are cleaned up.\n */\n head?: HeadConfig\n}\n\n/**\n * Handle returned by {@link createApp} for driving the running app.\n */\nexport interface AppHandle {\n /**\n * Switch the active layout on the current route without navigating.\n * `setLayout(name)` forces that layout; `setLayout(null)` forces none. The\n * override is reset on the next navigation. Wire it to a UI toggle or expose\n * it to an `@agent` action (e.g. `setLayout(\"compact\")`).\n */\n setLayout(name: string | null): Promise<void>\n}\n\n/**\n * Bootstrap the aihu SPA.\n *\n * - Wires the aihu runtime (mount + signal) — idempotent if called multiple times\n * - Creates the router from virtual:aihu-routes\n * - Renders the current route (wrapped in its `layout`, if any)\n * - Installs SPA click interception and popstate listeners\n *\n * Returns an {@link AppHandle} for runtime control (e.g. dynamic layout switching).\n *\n * @example\n * // src/main.ts\n * import { createApp } from '@aihu/app/client'\n * const app = createApp()\n * app.setLayout('compact') // switch layout on the current route\n */\nexport function createApp(config?: AppConfig): AppHandle {\n // Hoist provided values into globalThis before any component runs so that\n // @state blocks can reference them as bare identifiers.\n if (config?.provide) {\n Object.assign(globalThis, config.provide)\n }\n\n // Wire runtime — null-guarded in @aihu/runtime, safe to call multiple times\n _setMount(mount)\n _setSignal(signal as Parameters<typeof _setSignal>[0])\n if (config?.rendering?.mode !== 'spa') {\n _setHydrate(hydrate as Parameters<typeof _setHydrate>[0])\n }\n\n const outletId = config?.outletId ?? 'outlet'\n const outletEl = document.getElementById(outletId)\n if (!outletEl) {\n throw new Error(\n `@aihu/app: no element with id=\"${outletId}\" found. Add <div id=\"${outletId}\"></div> to your index.html`,\n )\n }\n const outlet: HTMLElement = outletEl\n\n const router = createRouter(routes)\n\n // Per-route <head> wiring (B5, SEO arc). `siteUrl` resolves relative\n // canonical/OG/Twitter URLs to absolute; `globalHead` (app.head) is folded\n // under each route's head so defaults persist across navigations.\n const siteUrl = config?.site?.url\n const globalHead = config?.head\n\n /**\n * Update the live `document.head` for the active route. Lowers the route's\n * head (merged with the global defaults) into a renderable HeadConfig and\n * applies it — `applyHeadToDocument` first removes the prior route's managed\n * tags, so stale title/canonical/OG/JSON-LD never accumulate across nav.\n *\n * When there is no route head AND no global defaults, the previously-managed\n * per-page tags are simply cleared (nothing to re-apply).\n */\n function updateHead(head: RouteHead | undefined): void {\n if (head === undefined && globalHead === undefined) {\n clearManagedHead()\n return\n }\n const lowered = routeHeadToSsrHead(head, {\n ...(siteUrl !== undefined ? { siteUrl } : {}),\n ...(globalHead !== undefined ? { globalHead } : {}),\n })\n applyHeadToDocument(lowered)\n }\n\n // Dynamic layout switching (Step 2). `layoutOverride` lets a human toggle or\n // an `@agent` action swap the active layout WITHOUT navigating:\n // - `undefined` → follow the matched route's declared `layout`\n // - `null` → force NO layout (render at the root outlet)\n // - `\"<name>\"` → force that layout\n // It is transient: navigating resets it so each route shows its declared\n // layout again. `currentMatch` is the last rendered match so `setLayout` can\n // re-render the same route under the new layout.\n let currentMatch: MatchResult | null = null\n let layoutOverride: string | null | undefined\n\n async function render(match: MatchResult | null): Promise<void> {\n currentMatch = match\n if (!match) {\n // Check for a 404/not-found route by convention before falling back inline\n const notFoundRoute = (routes as RouteDefinition[]).find(\n (r) => r.pattern === '*' || r.name === 'not-found',\n )\n if (notFoundRoute) {\n await notFoundRoute.module()\n const tag = notFoundRoute.name\n if (tag?.includes('-')) {\n updateHead(notFoundRoute.head)\n outlet.replaceChildren(document.createElement(tag))\n return\n }\n }\n // Inline fallback 404 — drop the prior route's head, fall back to globals.\n updateHead(undefined)\n const p = document.createElement('p')\n p.style.cssText = 'font-family:system-ui;padding:2rem;color:#888'\n p.textContent = '404 — page not found'\n outlet.replaceChildren(p)\n return\n }\n\n // Reflect the active route's <head> on the live document before rendering\n // its element, so title/meta/canonical/JSON-LD match the page being shown.\n updateHead(match.route.head)\n\n // Import the page module — registers its custom element + auto-wires runtime\n await match.route.module()\n const tag = match.route.name\n if (!tag?.includes('-')) return\n\n const el = document.createElement(tag)\n\n // Flat per-attribute route params (A4 protocol — replaces JSON route attribute)\n if (match.params) {\n for (const [key, val] of Object.entries(match.params)) {\n el.setAttribute(key, String(val))\n }\n }\n\n // Layout wrapping: if the matched route declares a `layout` and that layout\n // exists in the generated map, render the layout into the root outlet and\n // mount the page into the layout's `data-aihu-outlet` marker. Otherwise the\n // page mounts directly into the root outlet (original behavior).\n // Override (dynamic switch) wins over the route's declared layout.\n const layoutName =\n layoutOverride === undefined ? match.route.layout : (layoutOverride ?? undefined)\n const entry = layoutName ? layouts[layoutName] : undefined\n if (entry) {\n // Register the layout's `aihu-layout-<name>` custom element (import side effect).\n await entry.load()\n const layoutEl = document.createElement(entry.tag)\n // Connect the layout first so its template — including the passive outlet\n // marker — mounts synchronously, then place the page inside the marker.\n outlet.replaceChildren(layoutEl)\n const root: ParentNode = layoutEl.shadowRoot ?? layoutEl\n const marker = root.querySelector('[data-aihu-outlet]')\n if (marker) {\n marker.replaceChildren(el)\n } else {\n // Misconfigured layout (no <$outlet>) — keep it visible + surface it\n // rather than silently dropping the page.\n console.warn(`[@aihu/app] layout \"${layoutName}\" has no <$outlet>`)\n root.appendChild(el)\n }\n return\n }\n\n outlet.replaceChildren(el)\n }\n\n /** Navigate-and-render: a real navigation clears any transient layout override. */\n function renderNav(match: MatchResult | null): void {\n layoutOverride = undefined\n void render(match)\n }\n\n /**\n * Switch the active layout on the current route WITHOUT navigating.\n * - `setLayout(\"compact\")` — render the current page under the `compact` layout.\n * - `setLayout(null)` — render the current page with no layout (root outlet).\n * The override is reset on the next navigation. Returns a promise that\n * resolves once the re-render completes.\n */\n function setLayout(name: string | null): Promise<void> {\n layoutOverride = name\n return render(currentMatch)\n }\n\n // Initial render\n renderNav(router.match(location.pathname))\n\n // SPA click interception — handles <a> links within the app\n document.addEventListener('click', (e) => {\n const a = (e.target as Element).closest('a') as HTMLAnchorElement | null\n if (!a) return\n const href = a.getAttribute('href')\n if (!href || href.startsWith('http') || href.startsWith('//') || href.startsWith('mailto:'))\n return\n e.preventDefault()\n history.pushState({}, '', href)\n renderNav(router.match(location.pathname))\n })\n\n // Browser back/forward\n window.addEventListener('popstate', () => {\n renderNav(router.match(location.pathname))\n })\n\n return { setLayout }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../src/head-apply.ts","../src/client.ts"],"sourcesContent":["/**\n * Shared head-application core (SEO arc).\n *\n * A single `HeadConfig` → keyed-tag decomposition feeds BOTH appliers so the\n * server-side (build-time) and client-side (runtime) paths can never diverge on\n * how a meta/link/script is keyed, merged, or escaped:\n *\n * - `applyHeadToHtml` — B4 (SSG prerender, `prerender.ts`): regex-rewrites a\n * built `index.html` string. Build-time only, never shipped to the browser.\n * - `applyHeadToDocument` — B5 (client-nav, `client.ts`): mutates the LIVE\n * `document.head` on SPA navigation and tracks the per-page tags it owns so\n * they can be cleaned up on the next navigation.\n *\n * This module is PURE (no `node:` builtin, no module-level DOM access) so it is\n * safe in the browser-edge `dist/client.js` bundle that `check:runtime-purity`\n * guards. The DOM applier receives its `Document` as an argument (defaulting to\n * the ambient `document` only when called) — there is no top-level `document`\n * reference, so importing this module never assumes a DOM.\n */\n\nimport type { HeadConfig } from '@aihu/server/head-lowering'\n\n/**\n * Attribute stamped on every tag the head appliers own. Tags carrying it are\n * \"managed\" per-page head — removed and re-applied on each navigation. Tags\n * WITHOUT it (e.g. the source `index.html`'s baseline `<meta charset>` /\n * `<title>` and any unmanaged scaffold tags) are left untouched, so a route\n * that drops a field falls back to whatever the document already shipped.\n */\nexport const MANAGED_HEAD_ATTR = 'data-aihu-head'\n\n// ---------------------------------------------------------------------------\n// Keying — the single source of truth shared by both appliers.\n// ---------------------------------------------------------------------------\n\n/**\n * Stable identity for a meta tag, used to upsert (replace-in-place) rather than\n * accumulate duplicates: name wins, then property, else `null` (always inject).\n */\nexport function metaKey(\n attrs: Record<string, string | undefined>,\n): { attr: 'name' | 'property'; value: string } | null {\n if (typeof attrs.name === 'string') return { attr: 'name', value: attrs.name }\n if (typeof attrs.property === 'string') return { attr: 'property', value: attrs.property }\n return null\n}\n\n/** Plain attribute records for each tag class, with `undefined` values dropped. */\nfunction definedAttrs(tag: Record<string, string | undefined>): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [k, v] of Object.entries(tag)) {\n if (v !== undefined) out[k] = String(v)\n }\n return out\n}\n\ninterface DecomposedHead {\n title: string | undefined\n metas: Array<Record<string, string>>\n links: Array<Record<string, string>>\n scripts: Array<{ type: string; content: string }>\n}\n\n/** Decompose a lowered HeadConfig into plain, escape-free tag records. */\nexport function decomposeHead(head: HeadConfig): DecomposedHead {\n return {\n title: head.title,\n metas: (head.meta ?? []).map((m) => definedAttrs(m)),\n links: (head.links ?? []).map((l) => definedAttrs(l)),\n scripts: (head.scripts ?? []).map((s) => ({ type: s.type, content: s.content })),\n }\n}\n\n// ---------------------------------------------------------------------------\n// String applier (B4 — SSG prerender). Mirrors the DOM applier's keying.\n// ---------------------------------------------------------------------------\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"')\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\nfunction attrsToHtml(attrs: Record<string, string>): string {\n return Object.entries(attrs)\n .map(([k, v]) => `${k}=\"${escapeAttr(v)}\"`)\n .join(' ')\n}\n\n/**\n * Apply a lowered HeadConfig onto a built `index.html` template string.\n *\n * - `<title>` is replaced (or injected when absent).\n * - Each meta is replaced in place when a tag with the same name/property\n * already exists; otherwise injected before `</head>`.\n * - canonical link replaces an existing `rel=\"canonical\"`; other links + all\n * scripts (JSON-LD) are injected before `</head>`.\n *\n * Build-time only — the regex transform is never shipped to the client.\n */\nexport function applyHeadToHtml(html: string, head: HeadConfig): string {\n let out = html\n const inject: string[] = []\n const { title, metas, links, scripts } = decomposeHead(head)\n\n if (title !== undefined) {\n const tag = `<title>${escapeText(title)}</title>`\n if (/<title[^>]*>[\\s\\S]*?<\\/title>/i.test(out)) {\n out = out.replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, tag)\n } else {\n inject.push(tag)\n }\n }\n\n for (const meta of metas) {\n const metaTag = `<meta ${attrsToHtml(meta)}>`\n const key = metaKey(meta)\n if (key) {\n const escaped = key.value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n const re = new RegExp(`<meta\\\\s+[^>]*${key.attr}=\"${escaped}\"[^>]*>`, 'i')\n if (re.test(out)) {\n out = out.replace(re, metaTag)\n continue\n }\n }\n inject.push(metaTag)\n }\n\n for (const link of links) {\n const linkTag = `<link ${attrsToHtml(link)}>`\n if ((link.rel ?? '').toLowerCase() === 'canonical') {\n const re = /<link\\s+[^>]*rel=\"canonical\"[^>]*>/i\n if (re.test(out)) {\n out = out.replace(re, linkTag)\n continue\n }\n }\n inject.push(linkTag)\n }\n\n for (const script of scripts) {\n // Element text — neutralize a literal `</` so injected `</script>` can't\n // break out (matches @aihu/server's buildHead guard).\n const body = script.content.replace(/<\\//g, '<\\\\/')\n inject.push(`<script type=\"${escapeAttr(script.type)}\">${body}</script>`)\n }\n\n if (inject.length === 0) return out\n const block = inject.join('\\n ')\n if (/<\\/head>/i.test(out)) {\n return out.replace(/<\\/head>/i, ` ${block}\\n </head>`)\n }\n return `${out}\\n${block}`\n}\n\n// ---------------------------------------------------------------------------\n// DOM applier (B5 — client-side SPA navigation).\n// ---------------------------------------------------------------------------\n\n/**\n * Remove every per-page head tag previously applied by `applyHeadToDocument`\n * (those stamped with {@link MANAGED_HEAD_ATTR}). Source/baseline tags and any\n * tag the appliers did not create are left in place.\n *\n * Called at the start of every navigation so a route that omits a field (or\n * navigating back to a route with a different head) never leaves a stale\n * title/canonical/OG/JSON-LD behind.\n */\nexport function clearManagedHead(doc: Document = document): void {\n const head = doc.head\n if (!head) return\n for (const el of Array.from(head.querySelectorAll(`[${MANAGED_HEAD_ATTR}]`))) {\n el.remove()\n }\n}\n\n/**\n * Apply a lowered HeadConfig to the LIVE `document.head` for SPA navigation.\n *\n * Idempotent per navigation: first clears the previously-managed per-page tags\n * ({@link clearManagedHead}), then applies the new config, stamping each tag it\n * creates with {@link MANAGED_HEAD_ATTR}. Because the lowered config already\n * folds in the global `app.head` defaults (via `routeHeadToSsrHead`'s\n * `globalHead`), re-applying the full set every navigation keeps those defaults\n * present while dropping route-only tags from the prior page.\n *\n * - `<title>`: the document title is set via `document.title`. (Title is a\n * singleton — never stamped/removed; an absent title leaves the prior one,\n * but the lowered config carries the global default title so it is restored.)\n * - meta: upserted by name/property — an existing managed-or-source tag with\n * the same key is updated in place; otherwise a fresh managed tag is appended.\n * - link: `rel=\"canonical\"` upserts the existing canonical; other links append.\n * - script: JSON-LD upserted by `type`.\n */\nexport function applyHeadToDocument(head: HeadConfig, doc: Document = document): void {\n const headEl = doc.head\n if (!headEl) return\n\n clearManagedHead(doc)\n\n const { title, metas, links, scripts } = decomposeHead(head)\n\n if (title !== undefined) {\n doc.title = title\n }\n\n for (const meta of metas) {\n const key = metaKey(meta)\n let el: HTMLMetaElement | null = null\n if (key) {\n el = headEl.querySelector<HTMLMetaElement>(`meta[${key.attr}=\"${cssEscape(key.value)}\"]`)\n }\n if (!el) {\n el = doc.createElement('meta')\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n headEl.appendChild(el)\n } else {\n // Re-stamp so an upserted source/global tag is cleaned up with the rest.\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n }\n setAttrs(el, meta)\n }\n\n for (const link of links) {\n const isCanonical = (link.rel ?? '').toLowerCase() === 'canonical'\n let el: HTMLLinkElement | null = null\n if (isCanonical) {\n el = headEl.querySelector<HTMLLinkElement>('link[rel=\"canonical\"]')\n }\n if (!el) {\n el = doc.createElement('link')\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n headEl.appendChild(el)\n } else {\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n }\n setAttrs(el, link)\n }\n\n for (const script of scripts) {\n let el = headEl.querySelector<HTMLScriptElement>(`script[type=\"${cssEscape(script.type)}\"]`)\n if (!el) {\n el = doc.createElement('script')\n el.type = script.type\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n headEl.appendChild(el)\n } else {\n el.setAttribute(MANAGED_HEAD_ATTR, '')\n }\n // textContent — the DOM never re-parses script text as HTML, so JSON-LD is\n // safe verbatim (no `</script>` escaping needed, unlike the string path).\n el.textContent = script.content\n }\n}\n\n/** Set/refresh attributes on an element from a plain record. */\nfunction setAttrs(el: Element, attrs: Record<string, string>): void {\n for (const [k, v] of Object.entries(attrs)) {\n el.setAttribute(k, v)\n }\n}\n\n/**\n * Escape a value for use inside a `[attr=\"…\"]` CSS attribute selector. Uses the\n * platform `CSS.escape` when available, with a minimal fallback (quotes +\n * backslashes) for environments without it.\n */\nfunction cssEscape(value: string): string {\n const g = globalThis as { CSS?: { escape?: (v: string) => string } }\n if (g.CSS && typeof g.CSS.escape === 'function') return g.CSS.escape(value)\n return value.replace(/[\"\\\\]/g, '\\\\$&')\n}\n","import layouts from 'virtual:aihu-layouts'\nimport routes from 'virtual:aihu-routes'\nimport { hydrate, mount } from '@aihu/arbor'\nimport type { MatchResult, RouteDefinition, RouteHead } from '@aihu/router'\nimport { createRouter } from '@aihu/router'\nimport { _setHydrate, _setMount, _setSignal } from '@aihu/runtime'\n// Pure subpath — NOT the @aihu/server barrel. The barrel reaches loader.ts +\n// the lazy native loader; importing it would risk dragging node:-bearing code\n// into the browser client bundle and trip check:runtime-purity. head-lowering.ts\n// is side-effect free (only the web-standard URL), so this stays node:-free.\nimport type { HeadConfig } from '@aihu/server/head-lowering'\nimport { routeHeadToSsrHead } from '@aihu/server/head-lowering'\nimport { signal } from '@aihu/signals'\nimport { applyHeadToDocument, clearManagedHead } from './head-apply.ts'\n\n/**\n * Rendering mode passed from the server config into the client bootstrap.\n * Inlined here to avoid importing @aihu/server into the client bundle.\n * Must stay in sync with RenderingMode in @aihu/server.\n */\nexport type AppRenderingMode = 'ssr' | 'spa' | 'hybrid'\n\n/** Inline runtime configuration accepted by createApp(). All fields optional. */\nexport interface AppConfig {\n /** Id of the outlet element in index.html. Default: 'outlet' */\n outletId?: string\n /**\n * App-level values hoisted into globalThis before any component runs.\n * Use this for singletons (db clients, auth helpers, i18n) that are\n * referenced as bare identifiers inside @state blocks.\n *\n * @example\n * createApp({ provide: { supabase, checkAuth } })\n */\n provide?: Record<string, unknown>\n /**\n * Rendering mode from the server config. Controls whether the client\n * wires the hydration function into the runtime.\n *\n * - 'ssr' | 'hybrid' (default): wires _setHydrate so the client can\n * take over from server-rendered HTML without re-creating DOM.\n * - 'spa': skips _setHydrate — no SSR HTML to hydrate, mount-only.\n *\n * Pass `defineAihuConfig(…).rendering?.mode` from your server config.\n * Default: 'ssr' (hydration wired).\n */\n rendering?: { mode?: AppRenderingMode }\n /**\n * Site-level config. `site.url` is the absolute base URL used to resolve\n * relative per-route `canonical` / `og:*` / `twitter:*` values into absolute\n * URLs as the head is applied on client navigation (mirrors the SSG path's\n * `AihuConfig.site.url`). When absent, relative values are emitted unchanged.\n */\n site?: { url?: string }\n /**\n * Global `<head>` defaults (typically `aihu.config.ts`'s `app.head`). On every\n * navigation these defaults are folded under the active route's head\n * (`routeHeadToSsrHead`'s `globalHead`) and re-applied — so a route that omits\n * a field falls back to the global default, and global tags persist across\n * navigations while route-only tags are cleaned up.\n */\n head?: HeadConfig\n}\n\n/**\n * Handle returned by {@link createApp} for driving the running app.\n */\nexport interface AppHandle {\n /**\n * Switch the active layout on the current route without navigating.\n * `setLayout(name)` forces that layout; `setLayout(null)` forces none. The\n * override is reset on the next navigation. Wire it to a UI toggle or expose\n * it to an `@agent` action (e.g. `setLayout(\"compact\")`).\n */\n setLayout(name: string | null): Promise<void>\n}\n\n/**\n * Bootstrap the aihu SPA.\n *\n * - Wires the aihu runtime (mount + signal) — idempotent if called multiple times\n * - Creates the router from virtual:aihu-routes\n * - Renders the current route (wrapped in its `layout`, if any)\n * - Installs SPA click interception and popstate listeners\n *\n * Returns an {@link AppHandle} for runtime control (e.g. dynamic layout switching).\n *\n * @example\n * // src/main.ts\n * import { createApp } from '@aihu/app/client'\n * const app = createApp()\n * app.setLayout('compact') // switch layout on the current route\n */\nexport function createApp(config?: AppConfig): AppHandle {\n // Hoist provided values into globalThis before any component runs so that\n // @state blocks can reference them as bare identifiers.\n if (config?.provide) {\n Object.assign(globalThis, config.provide)\n }\n\n // Wire runtime — null-guarded in @aihu/runtime, safe to call multiple times\n _setMount(mount)\n _setSignal(signal as Parameters<typeof _setSignal>[0])\n if (config?.rendering?.mode !== 'spa') {\n _setHydrate(hydrate as Parameters<typeof _setHydrate>[0])\n }\n\n const outletId = config?.outletId ?? 'outlet'\n const outletEl = document.getElementById(outletId)\n if (!outletEl) {\n throw new Error(\n `@aihu/app: no element with id=\"${outletId}\" found. Add <div id=\"${outletId}\"></div> to your index.html`,\n )\n }\n const outlet: HTMLElement = outletEl\n\n const router = createRouter(routes)\n\n // Per-route <head> wiring (B5, SEO arc). `siteUrl` resolves relative\n // canonical/OG/Twitter URLs to absolute; `globalHead` (app.head) is folded\n // under each route's head so defaults persist across navigations.\n const siteUrl = config?.site?.url\n const globalHead = config?.head\n\n /**\n * Update the live `document.head` for the active route. Lowers the route's\n * head (merged with the global defaults) into a renderable HeadConfig and\n * applies it — `applyHeadToDocument` first removes the prior route's managed\n * tags, so stale title/canonical/OG/JSON-LD never accumulate across nav.\n *\n * When there is no route head AND no global defaults, the previously-managed\n * per-page tags are simply cleared (nothing to re-apply).\n */\n function updateHead(head: RouteHead | undefined): void {\n if (head === undefined && globalHead === undefined) {\n clearManagedHead()\n return\n }\n const lowered = routeHeadToSsrHead(head, {\n ...(siteUrl !== undefined ? { siteUrl } : {}),\n ...(globalHead !== undefined ? { globalHead } : {}),\n })\n applyHeadToDocument(lowered)\n }\n\n // Dynamic layout switching (Step 2). `layoutOverride` lets a human toggle or\n // an `@agent` action swap the active layout WITHOUT navigating:\n // - `undefined` → follow the matched route's declared `layout`\n // - `null` → force NO layout (render at the root outlet)\n // - `\"<name>\"` → force that layout\n // It is transient: navigating resets it so each route shows its declared\n // layout again. `currentMatch` is the last rendered match so `setLayout` can\n // re-render the same route under the new layout.\n let currentMatch: MatchResult | null = null\n let layoutOverride: string | null | undefined\n\n async function render(match: MatchResult | null): Promise<void> {\n currentMatch = match\n if (!match) {\n // Check for a 404/not-found route by convention before falling back inline\n const notFoundRoute = (routes as RouteDefinition[]).find(\n (r) => r.pattern === '*' || r.name === 'not-found',\n )\n if (notFoundRoute) {\n await notFoundRoute.module()\n const tag = notFoundRoute.name\n if (tag?.includes('-')) {\n updateHead(notFoundRoute.head)\n outlet.replaceChildren(document.createElement(tag))\n return\n }\n }\n // Inline fallback 404 — drop the prior route's head, fall back to globals.\n updateHead(undefined)\n const p = document.createElement('p')\n p.style.cssText = 'font-family:system-ui;padding:2rem;color:#888'\n p.textContent = '404 — page not found'\n outlet.replaceChildren(p)\n return\n }\n\n // Reflect the active route's <head> on the live document before rendering\n // its element, so title/meta/canonical/JSON-LD match the page being shown.\n updateHead(match.route.head)\n\n // Import the page module — registers its custom element + auto-wires runtime\n await match.route.module()\n const tag = match.route.name\n if (!tag?.includes('-')) return\n\n const el = document.createElement(tag)\n\n // Flat per-attribute route params (A4 protocol — replaces JSON route attribute)\n if (match.params) {\n for (const [key, val] of Object.entries(match.params)) {\n el.setAttribute(key, String(val))\n }\n }\n\n // Layout wrapping: if the matched route declares a `layout` and that layout\n // exists in the generated map, render the layout into the root outlet and\n // mount the page into the layout's `data-aihu-outlet` marker. Otherwise the\n // page mounts directly into the root outlet (original behavior).\n // Override (dynamic switch) wins over the route's declared layout.\n const layoutName =\n layoutOverride === undefined ? match.route.layout : (layoutOverride ?? undefined)\n const entry = layoutName ? layouts[layoutName] : undefined\n if (entry) {\n // Register the layout's `aihu-layout-<name>` custom element (import side effect).\n await entry.load()\n const layoutEl = document.createElement(entry.tag)\n // Connect the layout first so its template — including the passive outlet\n // marker — mounts synchronously, then place the page inside the marker.\n outlet.replaceChildren(layoutEl)\n const root: ParentNode = layoutEl.shadowRoot ?? layoutEl\n const marker = root.querySelector('[data-aihu-outlet]')\n if (marker) {\n marker.replaceChildren(el)\n } else {\n // Misconfigured layout (no <$outlet>) — keep it visible + surface it\n // rather than silently dropping the page.\n console.warn(`[@aihu/app] layout \"${layoutName}\" has no <$outlet>`)\n root.appendChild(el)\n }\n return\n }\n\n outlet.replaceChildren(el)\n }\n\n /** Navigate-and-render: a real navigation clears any transient layout override. */\n function renderNav(match: MatchResult | null): void {\n layoutOverride = undefined\n void render(match)\n }\n\n /**\n * Switch the active layout on the current route WITHOUT navigating.\n * - `setLayout(\"compact\")` — render the current page under the `compact` layout.\n * - `setLayout(null)` — render the current page with no layout (root outlet).\n * The override is reset on the next navigation. Returns a promise that\n * resolves once the re-render completes.\n */\n function setLayout(name: string | null): Promise<void> {\n layoutOverride = name\n return render(currentMatch)\n }\n\n // Initial render\n renderNav(router.match(location.pathname))\n\n // SPA click interception — handles <a> links within the app\n document.addEventListener('click', (e) => {\n // Resolve the anchor across shadow boundaries. A click inside a shadow root\n // (e.g. a layout shell rendered with the default shadow mode) is retargeted\n // at the host, so `e.target` is the host element and `closest('a')` misses\n // the real <a>. `composedPath()` includes nodes inside shadow trees, ordered\n // target→root, so the first anchor in it is the innermost one clicked.\n const a = e.composedPath().find((n): n is HTMLAnchorElement => n instanceof HTMLAnchorElement)\n if (!a) return\n const href = a.getAttribute('href')\n if (!href || href.startsWith('http') || href.startsWith('//') || href.startsWith('mailto:'))\n return\n e.preventDefault()\n history.pushState({}, '', href)\n renderNav(router.match(location.pathname))\n })\n\n // Browser back/forward\n window.addEventListener('popstate', () => {\n renderNav(router.match(location.pathname))\n })\n\n return { setLayout }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aihu/app",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@aihu-plugin/agent-readiness": "2.0.3",
|
|
40
|
-
"@aihu/compiler": "0.7.
|
|
40
|
+
"@aihu/compiler": "0.7.1"
|
|
41
41
|
},
|
|
42
42
|
"description": "Top-level app integration — wires runtime, router, and adapters into a Vite app.",
|
|
43
43
|
"repository": {
|