@aihu/app 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/client.d.ts +19 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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@
|
|
24
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.0`.</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** | `
|
|
35
|
+
| **Version** | `2.0.0` |
|
|
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@
|
|
40
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.0`.</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@
|
|
54
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.0`.</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@
|
|
72
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.0`.</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@
|
|
86
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.0`.</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@
|
|
97
|
+
<sub><i>Auto-generated against `@aihu/app@2.0.0`.</i></sub>
|
|
98
98
|
|
|
99
99
|
<!-- END_AUTOGEN: license -->
|
package/dist/client.d.ts
CHANGED
|
@@ -52,20 +52,35 @@ interface AppConfig {
|
|
|
52
52
|
*/
|
|
53
53
|
head?: HeadConfig;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Handle returned by {@link createApp} for driving the running app.
|
|
57
|
+
*/
|
|
58
|
+
interface AppHandle {
|
|
59
|
+
/**
|
|
60
|
+
* Switch the active layout on the current route without navigating.
|
|
61
|
+
* `setLayout(name)` forces that layout; `setLayout(null)` forces none. The
|
|
62
|
+
* override is reset on the next navigation. Wire it to a UI toggle or expose
|
|
63
|
+
* it to an `@agent` action (e.g. `setLayout("compact")`).
|
|
64
|
+
*/
|
|
65
|
+
setLayout(name: string | null): Promise<void>;
|
|
66
|
+
}
|
|
55
67
|
/**
|
|
56
68
|
* Bootstrap the aihu SPA.
|
|
57
69
|
*
|
|
58
70
|
* - Wires the aihu runtime (mount + signal) — idempotent if called multiple times
|
|
59
71
|
* - Creates the router from virtual:aihu-routes
|
|
60
|
-
* - Renders the current route
|
|
72
|
+
* - Renders the current route (wrapped in its `layout`, if any)
|
|
61
73
|
* - Installs SPA click interception and popstate listeners
|
|
62
74
|
*
|
|
75
|
+
* Returns an {@link AppHandle} for runtime control (e.g. dynamic layout switching).
|
|
76
|
+
*
|
|
63
77
|
* @example
|
|
64
78
|
* // src/main.ts
|
|
65
79
|
* import { createApp } from '@aihu/app/client'
|
|
66
|
-
* createApp()
|
|
80
|
+
* const app = createApp()
|
|
81
|
+
* app.setLayout('compact') // switch layout on the current route
|
|
67
82
|
*/
|
|
68
|
-
declare function createApp(config?: AppConfig):
|
|
83
|
+
declare function createApp(config?: AppConfig): AppHandle;
|
|
69
84
|
//#endregion
|
|
70
|
-
export { AppConfig, AppRenderingMode, createApp };
|
|
85
|
+
export { AppConfig, AppHandle, AppRenderingMode, createApp };
|
|
71
86
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;AAoBA;;;KAAY,gBAAA;;UAGK,SAAA;EAAS;EAExB,QAAA;EASU;;;;;;;;EAAV,OAAA,GAAU,MAAA;EAYI;;;;;;;;AAqBhB;;;EArBE,SAAA;IAAc,IAAA,GAAO,gBAAA;EAAA;EA4BW;;;AAmBlC;;;EAxCE,IAAA;IAAS,GAAA;EAAA;EAwCoC;;;;;;;EAhC7C,IAAA,GAAO,UAAA;AAAA;;;;UAMQ,SAAA;;;;;;;EAOf,SAAA,CAAU,IAAA,kBAAsB,OAAA;AAAA;;;;;;;;;;;;;;;;;iBAmBlB,SAAA,CAAU,MAAA,GAAS,SAAA,GAAY,SAAA"}
|
package/dist/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"virtual:aihu-routes";import{hydrate as
|
|
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.target.closest(`a`);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 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 * 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\n * - Installs SPA click interception and popstate listeners\n *\n * @example\n * // src/main.ts\n * import { createApp } from '@aihu/app/client'\n * createApp()\n */\nexport function createApp(config?: AppConfig): void {\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 async function render(match: MatchResult | null): Promise<void> {\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 outlet.replaceChildren(el)\n }\n\n // Initial render\n render(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 render(router.match(location.pathname))\n })\n\n // Browser back/forward\n window.addEventListener('popstate', () => {\n render(router.match(location.pathname))\n })\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 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":"sVA6BA,MAAa,EAAoB,iBAUjC,SAAgB,EACd,EACqD,CAGrD,OAFI,OAAO,EAAM,MAAS,SAAiB,CAAE,KAAM,OAAQ,MAAO,EAAM,KAAM,CAC1E,OAAO,EAAM,UAAa,SAAiB,CAAE,KAAM,WAAY,MAAO,EAAM,SAAU,CACnF,KAIT,SAAS,EAAa,EAAiE,CACrF,IAAM,EAA8B,EAAE,CACtC,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,EAAI,CAClC,IAAM,IAAA,KAAW,EAAI,GAAK,OAAO,EAAE,EAEzC,OAAO,EAWT,SAAgB,EAAc,EAAkC,CAC9D,MAAO,CACL,MAAO,EAAK,MACZ,OAAQ,EAAK,MAAQ,EAAE,EAAE,IAAK,GAAM,EAAa,EAAE,CAAC,CACpD,OAAQ,EAAK,OAAS,EAAE,EAAE,IAAK,GAAM,EAAa,EAAE,CAAC,CACrD,SAAU,EAAK,SAAW,EAAE,EAAE,IAAK,IAAO,CAAE,KAAM,EAAE,KAAM,QAAS,EAAE,QAAS,EAAE,CACjF,CAoGH,SAAgB,EAAiB,EAAgB,SAAgB,CAC/D,IAAM,EAAO,EAAI,KACZ,KACL,IAAK,IAAM,KAAM,MAAM,KAAK,EAAK,iBAAiB,IAAI,EAAkB,GAAG,CAAC,CAC1E,EAAG,QAAQ,CAsBf,SAAgB,EAAoB,EAAkB,EAAgB,SAAgB,CACpF,IAAM,EAAS,EAAI,KACnB,GAAI,CAAC,EAAQ,OAEb,EAAiB,EAAI,CAErB,GAAM,CAAE,QAAO,QAAO,QAAO,WAAY,EAAc,EAAK,CAExD,IAAU,IAAA,KACZ,EAAI,MAAQ,GAGd,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAM,EAAQ,EAAK,CACrB,EAA6B,KAC7B,IACF,EAAK,EAAO,cAA+B,QAAQ,EAAI,KAAK,IAAI,EAAU,EAAI,MAAM,CAAC,IAAI,EAEtF,EAMH,EAAG,aAAa,EAAmB,GAAG,EALtC,EAAK,EAAI,cAAc,OAAO,CAC9B,EAAG,aAAa,EAAmB,GAAG,CACtC,EAAO,YAAY,EAAG,EAKxB,EAAS,EAAI,EAAK,CAGpB,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,GAAe,EAAK,KAAO,IAAI,aAAa,GAAK,YACnD,EAA6B,KAC7B,IACF,EAAK,EAAO,cAA+B,wBAAwB,EAEhE,EAKH,EAAG,aAAa,EAAmB,GAAG,EAJtC,EAAK,EAAI,cAAc,OAAO,CAC9B,EAAG,aAAa,EAAmB,GAAG,CACtC,EAAO,YAAY,EAAG,EAIxB,EAAS,EAAI,EAAK,CAGpB,IAAK,IAAM,KAAU,EAAS,CAC5B,IAAI,EAAK,EAAO,cAAiC,gBAAgB,EAAU,EAAO,KAAK,CAAC,IAAI,CACvF,EAMH,EAAG,aAAa,EAAmB,GAAG,EALtC,EAAK,EAAI,cAAc,SAAS,CAChC,EAAG,KAAO,EAAO,KACjB,EAAG,aAAa,EAAmB,GAAG,CACtC,EAAO,YAAY,EAAG,EAMxB,EAAG,YAAc,EAAO,SAK5B,SAAS,EAAS,EAAa,EAAqC,CAClE,IAAK,GAAM,CAAC,EAAG,KAAM,OAAO,QAAQ,EAAM,CACxC,EAAG,aAAa,EAAG,EAAE,CASzB,SAAS,EAAU,EAAuB,CACxC,IAAM,EAAI,WAEV,OADI,EAAE,KAAO,OAAO,EAAE,IAAI,QAAW,WAAmB,EAAE,IAAI,OAAO,EAAM,CACpE,EAAM,QAAQ,SAAU,OAAO,CCnLxC,SAAgB,EAAU,EAA+B,CAGnD,GAAQ,SACV,OAAO,OAAO,WAAY,EAAO,QAAQ,CAI3C,EAAU,EAAM,CAChB,EAAW,EAA2C,CAClD,GAAQ,WAAW,OAAS,OAC9B,EAAY,EAA6C,CAG3D,IAAM,EAAW,GAAQ,UAAY,SAC/B,EAAW,SAAS,eAAe,EAAS,CAClD,GAAI,CAAC,EACH,MAAU,MACR,kCAAkC,EAAS,wBAAwB,EAAS,6BAC7E,CAEH,IAAM,EAAsB,EAEtB,EAAS,EAAa,EAAO,CAK7B,EAAU,GAAQ,MAAM,IACxB,EAAa,GAAQ,KAW3B,SAAS,EAAW,EAAmC,CACrD,GAAI,IAAS,IAAA,IAAa,IAAe,IAAA,GAAW,CAClD,GAAkB,CAClB,OAMF,EAJgB,EAAmB,EAAM,CACvC,GAAI,IAAY,IAAA,GAA0B,EAAE,CAAhB,CAAE,UAAS,CACvC,GAAI,IAAe,IAAA,GAA6B,EAAE,CAAnB,CAAE,aAAY,CAC9C,CAC0B,CAAC,CAW9B,IAAI,EAAmC,KACnC,EAEJ,eAAe,EAAO,EAA0C,CAE9D,GADA,EAAe,EACX,CAAC,EAAO,CAEV,IAAM,EAAiB,EAA6B,KACjD,GAAM,EAAE,UAAY,KAAO,EAAE,OAAS,YACxC,CACD,GAAI,EAAe,CACjB,MAAM,EAAc,QAAQ,CAC5B,IAAM,EAAM,EAAc,KAC1B,GAAI,GAAK,SAAS,IAAI,CAAE,CACtB,EAAW,EAAc,KAAK,CAC9B,EAAO,gBAAgB,SAAS,cAAc,EAAI,CAAC,CACnD,QAIJ,EAAW,IAAA,GAAU,CACrB,IAAM,EAAI,SAAS,cAAc,IAAI,CACrC,EAAE,MAAM,QAAU,gDAClB,EAAE,YAAc,uBAChB,EAAO,gBAAgB,EAAE,CACzB,OAKF,EAAW,EAAM,MAAM,KAAK,CAG5B,MAAM,EAAM,MAAM,QAAQ,CAC1B,IAAM,EAAM,EAAM,MAAM,KACxB,GAAI,CAAC,GAAK,SAAS,IAAI,CAAE,OAEzB,IAAM,EAAK,SAAS,cAAc,EAAI,CAGtC,GAAI,EAAM,OACR,IAAK,GAAM,CAAC,EAAK,KAAQ,OAAO,QAAQ,EAAM,OAAO,CACnD,EAAG,aAAa,EAAK,OAAO,EAAI,CAAC,CASrC,IAAM,EACJ,IAAmB,IAAA,GAAY,EAAM,MAAM,OAAU,GAAkB,IAAA,GACnE,EAAQ,EAAa,EAAQ,GAAc,IAAA,GACjD,GAAI,EAAO,CAET,MAAM,EAAM,MAAM,CAClB,IAAM,EAAW,SAAS,cAAc,EAAM,IAAI,CAGlD,EAAO,gBAAgB,EAAS,CAChC,IAAM,EAAmB,EAAS,YAAc,EAC1C,EAAS,EAAK,cAAc,qBAAqB,CACnD,EACF,EAAO,gBAAgB,EAAG,EAI1B,QAAQ,KAAK,uBAAuB,EAAW,oBAAoB,CACnE,EAAK,YAAY,EAAG,EAEtB,OAGF,EAAO,gBAAgB,EAAG,CAI5B,SAAS,EAAU,EAAiC,CAClD,EAAiB,IAAA,GACjB,EAAY,EAAM,CAUpB,SAAS,EAAU,EAAoC,CAErD,MADA,GAAiB,EACV,EAAO,EAAa,CAuB7B,OAnBA,EAAU,EAAO,MAAM,SAAS,SAAS,CAAC,CAG1C,SAAS,iBAAiB,QAAU,GAAM,CACxC,IAAM,EAAK,EAAE,OAAmB,QAAQ,IAAI,CAC5C,GAAI,CAAC,EAAG,OACR,IAAM,EAAO,EAAE,aAAa,OAAO,CAC/B,CAAC,GAAQ,EAAK,WAAW,OAAO,EAAI,EAAK,WAAW,KAAK,EAAI,EAAK,WAAW,UAAU,GAE3F,EAAE,gBAAgB,CAClB,QAAQ,UAAU,EAAE,CAAE,GAAI,EAAK,CAC/B,EAAU,EAAO,MAAM,SAAS,SAAS,CAAC,GAC1C,CAGF,OAAO,iBAAiB,eAAkB,CACxC,EAAU,EAAO,MAAM,SAAS,SAAS,CAAC,EAC1C,CAEK,CAAE,YAAW"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{cp as e,mkdir as t,readFile as n,writeFile as r}from"node:fs/promises";import{dirname as i,join as a,resolve as o}from"node:path";import{aihuCompilerPlugin as s}from"@aihu/compiler";import{readRouteSidecar as c,scanPages as l,viteRouterIntegration as u}from"@aihu/router/plugin";import{renderToString as d,routeHeadToSsrHead as f}from"@aihu/server";var p=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")}),m=class extends Error{code;field;constructor(e,t,n){super(e),this.code=t,this.field=n,this.name=`AihuConfigError`}};function h(e){if(e.output&&e.output!==`spa`&&e.output!==`static`)throw new m(`output mode '${e.output}' is not supported (use 'spa' or 'static')`,`INVALID_OUTPUT_MODE`,`output`);if(e.dir?.pages!==void 0&&typeof e.dir.pages!=`string`)throw new m(`dir.pages must be a string`,`INVALID_DIR`,`dir.pages`);if(e.dir?.layouts!==void 0&&typeof e.dir.layouts!=`string`)throw new m(`dir.layouts must be a string`,`INVALID_DIR`,`dir.layouts`);if(e.css?.shadowMode!==void 0&&e.css.shadowMode!==`open`&&e.css.shadowMode!==`closed`&&e.css.shadowMode!==`none`)throw new m(`css.shadowMode '${e.css.shadowMode}' is not supported (use 'open', 'closed', or 'none')`,`INVALID_CSS_SHADOW_MODE`,`css.shadowMode`);return e}function g(e){return e.replace(/&/g,`&`).replace(/"/g,`"`).replace(/</g,`<`).replace(/>/g,`>`)}function _(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`)}function v(e,t){if(!t)return e;let n=e,r=[];if(t.title!==void 0){let e=`<title>${_(t.title)}</title>`;/<title[^>]*>[\s\S]*?<\/title>/i.test(n)?n=n.replace(/<title[^>]*>[\s\S]*?<\/title>/i,e):r.push(e)}if(t.charset!==void 0){let e=`<meta charset="${g(t.charset)}">`;/<meta\s+[^>]*charset\s*=\s*["'][^"']*["'][^>]*>/i.test(n)?n=n.replace(/<meta\s+[^>]*charset\s*=\s*["'][^"']*["'][^>]*>/i,e):r.push(e)}if(t.viewport!==void 0){let e=`<meta name="viewport" content="${g(t.viewport)}">`,i=/<meta\s+[^>]*name\s*=\s*["']viewport["'][^>]*>/i;i.test(n)?n=n.replace(i,e):r.push(e)}for(let e of t.meta??[]){let t=e.name===void 0?e.property===void 0?null:`property`:`name`,i=t?e[t]:void 0,a=`<meta ${Object.entries(e).map(([e,t])=>`${e}="${g(String(t))}"`).join(` `)}>`;if(t&&i!==void 0){let e=RegExp(`<meta\\s+[^>]*${t}\\s*=\\s*["']${i.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}["'][^>]*>`,`i`);if(e.test(n)){n=n.replace(e,a);continue}}r.push(a)}if(r.length===0)return n;let i=r.join(`
|
|
2
2
|
`);return/<\/head>/i.test(n)?n.replace(/<\/head>/i,` ${i}\n </head>`):`${n}\n${i}`}function y(e){return typeof e.name==`string`?{attr:`name`,value:e.name}:typeof e.property==`string`?{attr:`property`,value:e.property}:null}function b(e){let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n]=String(r));return t}function x(e){return{title:e.title,metas:(e.meta??[]).map(e=>b(e)),links:(e.links??[]).map(e=>b(e)),scripts:(e.scripts??[]).map(e=>({type:e.type,content:e.content}))}}function S(e){return e.replace(/&/g,`&`).replace(/"/g,`"`)}function C(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`)}function w(e){return Object.entries(e).map(([e,t])=>`${e}="${S(t)}"`).join(` `)}function T(e,t){let n=e,r=[],{title:i,metas:a,links:o,scripts:s}=x(t);if(i!==void 0){let e=`<title>${C(i)}</title>`;/<title[^>]*>[\s\S]*?<\/title>/i.test(n)?n=n.replace(/<title[^>]*>[\s\S]*?<\/title>/i,e):r.push(e)}for(let e of a){let t=`<meta ${w(e)}>`,i=y(e);if(i){let e=i.value.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),r=RegExp(`<meta\\s+[^>]*${i.attr}="${e}"[^>]*>`,`i`);if(r.test(n)){n=n.replace(r,t);continue}}r.push(t)}for(let e of o){let t=`<link ${w(e)}>`;if((e.rel??``).toLowerCase()===`canonical`){let e=/<link\s+[^>]*rel="canonical"[^>]*>/i;if(e.test(n)){n=n.replace(e,t);continue}}r.push(t)}for(let e of s){let t=e.content.replace(/<\//g,`<\\/`);r.push(`<script type="${S(e.type)}">${t}<\/script>`)}if(r.length===0)return n;let c=r.join(`
|
|
3
3
|
`);return/<\/head>/i.test(n)?n.replace(/<\/head>/i,` ${c}\n </head>`):`${n}\n${c}`}function E(e){let t=e.replace(/\\/g,`/`).replace(/\.[^/.]+$/,``).split(`/`).filter(Boolean).map(e=>e.startsWith(`[...`)&&e.endsWith(`]`)?{kind:`catchall`}:e.startsWith(`[`)&&e.endsWith(`]`)?{kind:`param`,name:e.slice(1,-1)}:e.startsWith(`:`)?{kind:`param`,name:e.slice(1)}:{kind:`static`,path:e});if(t.length>0){let e=t[t.length-1];e.kind===`static`&&e.path===`index`&&t.pop()}return t}function D(e){return e.length===0?`/`:`/${e.map(e=>e.kind===`static`?e.path:e.kind===`param`?`:${e.name}`:`*`).join(`/`)}`}function O(e,t){let{routes:n}=l(e,t),r=o(e,t).replace(/\\/g,`/`);return n.map(e=>{let t=e.replace(/\\/g,`/`),n=E(t.startsWith(`${r}/`)?t.slice(r.length+1):t);return{file:e,pattern:D(n),segments:n,dynamic:n.some(e=>e.kind===`param`||e.kind===`catchall`)}})}function k(e,t){return e.length===0?`/`:`/${e.map(e=>e.kind===`static`?e.path:e.kind===`param`?t[e.name]??``:t[`*`]??``).filter(e=>e!==``).join(`/`)}`}function A(e){return e&&typeof e==`object`&&`params`in e&&e.params?e.params:e}function j(e,t,n){let r=n.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),i=RegExp(`(<[a-zA-Z]+\\b[^>]*\\bid="${r}"[^>]*>)(\\s*)(</[a-zA-Z]+>)`,`i`);if(i.test(e))return e.replace(i,`$1${t}$3`);let a=RegExp(`(<[a-zA-Z]+\\b[^>]*\\bid="${r}"[^>]*>)`,`i`);return a.test(e)?e.replace(a,`$1${t}`):e}function M(e){let t=e.default;return typeof t==`function`||t&&typeof t==`object`&&typeof t.toHtml==`function`?t:null}function N(e){return e===`/`?`index.html`:a(e.replace(/^\//,``).replace(/\/$/,``),`index.html`)}async function P(e){let{resolvedViteConfig:a,config:s,loadModule:l,warn:u}=e,p=a.root,m=o(p,a.build.outDir),h=s?.dir?.pages??`pages`,g=s?.site?.url,_=s?.app?.head,v={written:[],warnings:[]},y=e=>{v.warnings.push(e),u(e)},b=o(m,`index.html`),x;try{x=await n(b,`utf8`)}catch{return y(`[@aihu/app] static output: no index.html in ${m} — cannot prerender. Ensure the SPA build produced an index.html.`),v}let S=O(p,h);for(let e of S){let n;try{n=await l(e.file)}catch(t){let n=t instanceof Error?t.message:String(t);y(`[@aihu/app] static output: failed to load route ${e.pattern}: ${n}`);continue}let a=c(e.file)?.head,s=M(n);if(!s){y(`[@aihu/app] static output: route ${e.pattern} has no renderable default export — skipping content prerender (the SPA shell still ships).`);continue}let u;if(e.dynamic){if(typeof n.getStaticPaths!=`function`){y(`[@aihu/app] static output: dynamic route ${e.pattern} has no getStaticPaths() — skipped. Export getStaticPaths() to prerender its paths.`);continue}u=(await n.getStaticPaths()??[]).map(A),u.length===0&&y(`[@aihu/app] static output: dynamic route ${e.pattern} getStaticPaths() returned no paths — nothing prerendered for this route.`)}else u=[{}];for(let n of u){let c=e.dynamic?k(e.segments,n):e.pattern,l;try{l=await d(s)}catch(e){y(`[@aihu/app] static output: render failed for ${c}: ${e instanceof Error?e.message:String(e)}`);continue}let u=f(a,{...g===void 0?{}:{siteUrl:g},..._===void 0?{}:{globalHead:_}}),p=T(x,u);p=j(p,l,`outlet`);let h=N(c),b=o(m,h);await t(i(b),{recursive:!0}),await r(b,p,`utf8`),v.written.push(h.replace(/\\/g,`/`))}}return v}async function F(e,t,n){let{createServer:r}=await import(`vite`),i=(e.plugins??[]).filter(e=>e?.name!==`aihu-ssg`&&e?.name!==`aihu-adapter`),a=await r({root:e.root,configFile:!1,appType:`custom`,logLevel:`silent`,server:{middlewareMode:!0,hmr:!1},plugins:i});try{return await P({resolvedViteConfig:e,config:t,loadModule:async e=>await a.ssrLoadModule(e),warn:n})}finally{await a.close()}}function I(e,t,n){let r=e.replace(/\\/g,`/`).replace(RegExp(`^.*?${n}/`),``).replace(/\.[^.]+$/,``).split(`/`).filter(Boolean);r.length>0&&r[r.length-1]===`index`&&r.pop();let i=r.map(e=>e.startsWith(`[...`)&&e.endsWith(`]`)?{kind:`catchall`}:e.startsWith(`[`)&&e.endsWith(`]`)?{kind:`param`,name:e.slice(1,-1)}:{kind:`static`,path:e});return{pattern:i.length===0?`/`:`/`+i.map(e=>e.kind===`static`?e.path:e.kind===`param`?`:${e.name}`:`*`).join(`/`),segments:i,module:()=>Promise.resolve({default:null})}}function L(n,a,s){let c=o(n.root,n.build.outDir),l=n.root,u=s?.dir?.pages??`pages`;return{outDir:c,root:l,routes:a.map(e=>I(e,l,u)),config:s??{},async emitFile(e,n){let a=o(c,e);await t(i(a),{recursive:!0}),await r(a,n,`utf8`)},async copy(n,r){await t(i(r),{recursive:!0}),await e(n,r,{recursive:!0,force:!0})},async writeFile(e,n){await t(i(e),{recursive:!0}),await r(e,n,`utf8`)},createHandlerSource(e){let t=e?.routesSpecifier??`./routes-manifest.js`;return[`// AUTO-GENERATED — do not edit`,`import { createRequestRouter } from '${e?.serverSpecifier??`@aihu/server`}'`,`import routes from '${t}'`,`const _manifest = { routes }`,`const _handler = createRequestRouter(_manifest)`,`export { _handler as handler }`].join(`
|
|
4
|
-
`)}}}function R(e){let t={pagesDir:e?.dir?.pages??`pages`,layoutsDir:e?.dir?.layouts??`src/layouts`},n,r=e?.agentReadiness;if(r){let{viteAgentReadinessIntegration:e}=p(`@aihu-plugin/agent-readiness`);n=e(r)}else n={name:`aihu-agent-readiness-disabled`};let i={name:`aihu-head`,transformIndexHtml:{order:`post`,handler(t){return v(t,e?.app?.head)}}},a={name:`aihu-vite-passthrough`,config(){return e?.vite??{}}},o=null,c={name:`aihu-adapter`,apply:`build`,configResolved(e){o=e},async closeBundle(){let t=e?.adapter;if(!t||!o)return;let n=e?.dir?.pages??`pages`,{routes:r}=l(o.root,n),i=L(o,r,e);try{await t.adapt(i)}catch(e){let n=e instanceof Error?e.message:String(e);this.error(`[@aihu/app] Adapter '${t.name}' failed: ${n}`)}}},d=null,f={name:`aihu-ssg`,apply:`build`,configResolved(e){d=e},async closeBundle(){if(!(e?.output!==`static`||!d))try{await F(d,e,e=>this.warn(e))}catch(e){let t=e instanceof Error?e.message:String(e);this.error(`[@aihu/app] static (SSG) prerender failed: ${t}`)}}};return[s({islands:!1,...e?.css?.shadowMode==null?{}:{shadowMode:e.css.shadowMode}}),u(t),n,i,...e?.plugins??[],a,f,c]}export{m as AihuConfigError,h as defineConfig,R as viteAihuPlugin};
|
|
4
|
+
`)}}}function R(e){let t={pagesDir:e?.dir?.pages??`pages`,layoutsDir:e?.dir?.layouts??`src/layouts`},n,r=e?.agentReadiness;if(r){let{viteAgentReadinessIntegration:e}=p(`@aihu-plugin/agent-readiness`);n=e(r)}else n={name:`aihu-agent-readiness-disabled`};let i={name:`aihu-head`,transformIndexHtml:{order:`post`,handler(t){return v(t,e?.app?.head)}}},a={name:`aihu-vite-passthrough`,config(){return e?.vite??{}}},o=null,c={name:`aihu-adapter`,apply:`build`,configResolved(e){o=e},async closeBundle(){let t=e?.adapter;if(!t||!o)return;let n=e?.dir?.pages??`pages`,{routes:r}=l(o.root,n),i=L(o,r,e);try{await t.adapt(i)}catch(e){let n=e instanceof Error?e.message:String(e);this.error(`[@aihu/app] Adapter '${t.name}' failed: ${n}`)}}},d=null,f={name:`aihu-ssg`,apply:`build`,configResolved(e){d=e},async closeBundle(){if(!(e?.output!==`static`||!d))try{await F(d,e,e=>this.warn(e))}catch(e){let t=e instanceof Error?e.message:String(e);this.error(`[@aihu/app] static (SSG) prerender failed: ${t}`)}}};return[s({islands:!1,layoutsDir:t.layoutsDir,...e?.css?.shadowMode==null?{}:{shadowMode:e.css.shadowMode}}),u(t),n,i,...e?.plugins??[],a,f,c]}export{m as AihuConfigError,h as defineConfig,R as viteAihuPlugin};
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["escapeAttr","escapeText","resolvePath","resolvePath","fsWriteFile"],"sources":["../src/config.ts","../src/head.ts","../src/head-apply.ts","../src/prerender.ts","../src/vite-plugin.ts"],"sourcesContent":["import type { Plugin, UserConfig } from 'vite'\nimport type { AihuAdapter } from './adapter.ts'\n\n/**\n * Build output mode.\n *\n * - `'spa'` (default): a single empty-shell `index.html` that boots the client\n * SPA. No per-route HTML, no prerendered content.\n * - `'static'` (SSG): prerenders every static route to a content-ful\n * `<pattern>/index.html` with a per-page `<head>`, then hydrates into the SPA\n * on load (progressive enhancement). Ideal for content sites on static hosts\n * (e.g. Cloudflare Pages) — crawlers and non-JS agents see real content.\n *\n * Other rendering modes (`ssr`, `hybrid`) are tracked separately under\n * @aihu/server's RenderingMode and are not part of the app build OutputMode.\n */\nexport type OutputMode = 'spa' | 'static'\n\n/** Site-level configuration. */\nexport interface SiteConfig {\n /**\n * Absolute base URL of the deployed site (e.g. `https://example.com`).\n * Used by the `'static'` (SSG) output mode to resolve relative per-route\n * `canonical` / `og:*` / `twitter:*` URLs into absolute URLs (passed as\n * `siteUrl` to @aihu/server's `routeHeadToSsrHead`). When absent, relative\n * URLs are emitted unchanged.\n */\n readonly url?: string\n}\n\nexport interface DirConfig {\n /** Directory to scan for page routes. Default: 'pages' */\n readonly pages?: string\n /** Directory to scan for layout files. Default: 'src/layouts' */\n readonly layouts?: string\n /** Public static assets directory. Default: 'public' */\n readonly public?: string\n}\n\n/** Runtime configuration split. Public values are safe to expose to the client. */\nexport interface RuntimeConfig {\n readonly public?: Record<string, unknown>\n /** V0: accepted but ignored at runtime (server-side enforcement deferred to V1). */\n readonly private?: Record<string, unknown>\n}\n\nexport interface HeadConfig {\n readonly title?: string\n /** Default: 'UTF-8' */\n readonly charset?: string\n /** Default: 'width=device-width, initial-scale=1' */\n readonly viewport?: string\n readonly meta?: ReadonlyArray<Record<string, string>>\n}\n\nexport interface AppHeadConfig {\n readonly head?: HeadConfig\n}\n\n/** Vite config fields that can be safely merged (excludes plugins — use AihuConfig.plugins). */\nexport type VitePassthrough = Omit<UserConfig, 'plugins'>\n\n/** A Aihu plugin is structurally identical to a Vite plugin (V0). */\nexport type AihuPlugin = Plugin\n\n/** Type-only import — not bundled when agentReadiness is absent. */\nexport type AgentReadinessConfig = import('@aihu-plugin/agent-readiness').AgentReadinessConfig\n\n/**\n * CSS / styling options forwarded to the compiler's Vite plugin.\n *\n * Today this surfaces the shadow-DOM mode the compiler injects into every\n * `defineElement(...)` call. The default (`'open'`) keeps shadow-encapsulated\n * component styles. `@aihu/css-engine` is scoped by design and works in any\n * mode — its utilities fold into each component's shadow style. Global-cascade\n * frameworks (Tailwind, UnoCSS, Pico) — or styling light-DOM / external\n * (slotted) children — need `'none'`.\n *\n * When set, `viteAihuPlugin` forwards this to its internal\n * `aihuCompilerPlugin({ shadowMode })` call. When absent, behaviour is\n * unchanged (compiler default applies).\n */\nexport interface CssConfig {\n /**\n * Project-wide shadow-DOM mode for every `.aihu` SFC compiled by\n * `viteAihuPlugin`.\n *\n * - `'open'` — default browser behaviour (shadow root, externally readable).\n * - `'closed'` — shadow root, externally hidden.\n * - `'none'` — **no shadow root.** Use for global-cascade CSS frameworks,\n * or when you explicitly want light-DOM / global CSS (e.g. to\n * style external / slotted child elements). NOT required for\n * `@aihu/css-engine`, which is scoped and works in any mode.\n */\n readonly shadowMode?: 'open' | 'closed' | 'none'\n}\n\n/** Router-related app config (arch-5 M1, RFC-A5-012). */\nexport interface RouterConfig {\n /**\n * When `true`, `<$link>` navigation wraps in `document.startViewTransition()`\n * if the browser supports the View Transitions API. No-op in unsupported\n * browsers (graceful degradation). Default: `false`.\n *\n * SSR safety: the wrapping is browser-only — server-rendered HTML is\n * unchanged, and hydration is unaffected.\n */\n readonly viewTransitions?: boolean\n}\n\nexport interface AihuConfig {\n /** Directory layout overrides. */\n readonly dir?: DirConfig\n /**\n * Output mode. Supports `'spa'` (default) and `'static'` (SSG prerender).\n * defineConfig throws AihuConfigError for any other value.\n */\n readonly output?: OutputMode\n /**\n * Site-level configuration. `site.url` is the absolute base URL used by the\n * `'static'` output mode to resolve relative canonical/OG/Twitter URLs.\n */\n readonly site?: SiteConfig\n /**\n * Aihu plugins. Order is preserved.\n * Appended after the three framework plugins (compiler, router, agent-readiness).\n */\n readonly plugins?: ReadonlyArray<AihuPlugin>\n /** Runtime configuration split — public values are inlined in the client bundle. */\n readonly runtimeConfig?: RuntimeConfig\n /**\n * App-level values made available to all components as bare identifiers.\n * Declared here for documentation and future build-time validation; the\n * values are hoisted into globalThis by createApp() at runtime.\n *\n * @example\n * export default defineConfig({ provide: { supabase, checkAuth } })\n */\n readonly provide?: Record<string, unknown>\n /** HTML <head> metadata. */\n readonly app?: AppHeadConfig\n /** Passthrough to Vite's UserConfig. Merged via Vite's config() hook. */\n readonly vite?: VitePassthrough\n /**\n * Opt-in agent-readiness integration.\n * Requires { name: string } at minimum.\n * When absent or false, a no-op plugin is substituted.\n */\n readonly agentReadiness?: AgentReadinessConfig | false\n /**\n * Deployment adapter. Transforms the Vite build output into the target\n * platform's required format. Called after vite build completes.\n * When absent, no post-build transformation is applied (manual deployment).\n */\n readonly adapter?: AihuAdapter\n /**\n * Router-related app config (arch-5 M1).\n * Currently exposes the `viewTransitions` opt-in for `<$link>`.\n */\n readonly router?: RouterConfig\n /**\n * CSS / styling integration. Currently surfaces the project-wide\n * `shadowMode` forwarded to the compiler. Set to `{ shadowMode: 'none' }`\n * when using `@aihu/css-engine` utility classes or any other cascade-\n * dependent CSS framework.\n */\n readonly css?: CssConfig\n}\n\n/** Thrown by defineConfig when configuration validation fails. */\nexport class AihuConfigError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'INVALID_OUTPUT_MODE'\n | 'INVALID_DIR'\n | 'UNKNOWN_FIELD'\n | 'INVALID_CSS_SHADOW_MODE',\n readonly field?: string,\n ) {\n super(message)\n this.name = 'AihuConfigError'\n }\n}\n\n/**\n * Define the aihu application configuration.\n *\n * Validates the config at call time and throws AihuConfigError for invalid values.\n * Returns the config unchanged (typed identity function).\n *\n * @example\n * // aihu.config.ts\n * import { defineConfig } from '@aihu/app'\n * export default defineConfig({\n * app: { head: { title: 'My App' } },\n * })\n */\nexport function defineConfig(config: AihuConfig): AihuConfig {\n if (config.output && config.output !== 'spa' && config.output !== 'static') {\n throw new AihuConfigError(\n `output mode '${config.output}' is not supported (use 'spa' or 'static')`,\n 'INVALID_OUTPUT_MODE',\n 'output',\n )\n }\n if (config.dir?.pages !== undefined && typeof config.dir.pages !== 'string') {\n throw new AihuConfigError('dir.pages must be a string', 'INVALID_DIR', 'dir.pages')\n }\n if (config.dir?.layouts !== undefined && typeof config.dir.layouts !== 'string') {\n throw new AihuConfigError('dir.layouts must be a string', 'INVALID_DIR', 'dir.layouts')\n }\n if (\n config.css?.shadowMode !== undefined &&\n config.css.shadowMode !== 'open' &&\n config.css.shadowMode !== 'closed' &&\n config.css.shadowMode !== 'none'\n ) {\n throw new AihuConfigError(\n `css.shadowMode '${config.css.shadowMode}' is not supported (use 'open', 'closed', or 'none')`,\n 'INVALID_CSS_SHADOW_MODE',\n 'css.shadowMode',\n )\n }\n return config\n}\n","import type { HeadConfig } from './config.ts'\n\n/** Escape a string for safe inclusion in a double-quoted HTML attribute value. */\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/** Escape text for safe inclusion in element text content (e.g. <title>). */\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/**\n * Transform a built index.html, applying the app-level <head> config.\n *\n * Precedence rule: **config overrides source.** When the source index.html\n * already declares a tag that `app.head` also configures (title, charset,\n * viewport, or a meta with a matching name/property), the configured value\n * replaces the source value in place — no duplicates are emitted. Tags present\n * only in the source are left untouched; tags present only in config are\n * injected just before `</head>` (or appended if no `</head>` exists).\n *\n * This is the sensible precedence because `app.head` is the explicit,\n * type-checked intent of the application author in aihu.config.ts, whereas the\n * source index.html is typically a Vite scaffold default.\n */\nexport function applyHeadConfig(html: string, head: HeadConfig | undefined): string {\n if (!head) return html\n\n let out = html\n const inject: string[] = []\n\n // title → set/replace <title>\n if (head.title !== undefined) {\n const tag = `<title>${escapeText(head.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 // charset → <meta charset>\n if (head.charset !== undefined) {\n const tag = `<meta charset=\"${escapeAttr(head.charset)}\">`\n if (/<meta\\s+[^>]*charset\\s*=\\s*[\"'][^\"']*[\"'][^>]*>/i.test(out)) {\n out = out.replace(/<meta\\s+[^>]*charset\\s*=\\s*[\"'][^\"']*[\"'][^>]*>/i, tag)\n } else {\n inject.push(tag)\n }\n }\n\n // viewport → <meta name=\"viewport\">\n if (head.viewport !== undefined) {\n const tag = `<meta name=\"viewport\" content=\"${escapeAttr(head.viewport)}\">`\n const viewportRe = /<meta\\s+[^>]*name\\s*=\\s*[\"']viewport[\"'][^>]*>/i\n if (viewportRe.test(out)) {\n out = out.replace(viewportRe, tag)\n } else {\n inject.push(tag)\n }\n }\n\n // meta[] → one <meta> per entry, keyed by name/property (config overrides\n // any matching source meta; unkeyed metas are always injected).\n for (const entry of head.meta ?? []) {\n const key = entry.name !== undefined ? 'name' : entry.property !== undefined ? 'property' : null\n const keyVal = key ? entry[key] : undefined\n\n const attrs = Object.entries(entry)\n .map(([k, v]) => `${k}=\"${escapeAttr(String(v))}\"`)\n .join(' ')\n const tag = `<meta ${attrs}>`\n\n if (key && keyVal !== undefined) {\n const re = new RegExp(\n `<meta\\\\s+[^>]*${key}\\\\s*=\\\\s*[\"']${keyVal.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}[\"'][^>]*>`,\n 'i',\n )\n if (re.test(out)) {\n out = out.replace(re, tag)\n continue\n }\n }\n inject.push(tag)\n }\n\n if (inject.length === 0) return out\n\n const block = inject.join('\\n ')\n if (/<\\/head>/i.test(out)) {\n return out.replace(/<\\/head>/i, ` ${block}\\n </head>`)\n }\n // No </head> in source — append the tags so they are not silently dropped.\n return `${out}\\n${block}`\n}\n","/**\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","/**\n * SSG prerender (B4, SEO arc) — build-time only.\n *\n * When `output: 'static'`, this module runs in `viteAihuPlugin`'s `closeBundle`\n * after Vite has written the SPA build. For every STATIC route it:\n *\n * 1. Loads the route's REAL module by file path (via a short-lived Vite SSR\n * module loader — this compiles `.aihu`/`.ts` exactly like the dev/prod\n * pipeline). The `AdapterContext.routes` stub is NOT used here.\n * 2. Renders the route's component to content HTML with @aihu/server's\n * `renderToString`.\n * 3. Folds the route's `<head>` (from the `.route.json` sidecar) into a\n * renderable `HeadConfig` via `routeHeadToSsrHead`, resolving relative\n * canonical/OG/Twitter URLs against `site.url`.\n * 4. Uses the built `index.html` as a template — injecting the per-page head\n * into `<head>` and the rendered content into the SPA outlet — so the page\n * ships content-ful HTML for crawlers/agents AND keeps the client bundle\n * `<script>` tags that hydrate it into the live SPA (progressive\n * enhancement).\n * 5. Writes `<pattern>/index.html` (and `index.html` for `/`).\n *\n * Dynamic routes (`:param` / `[param]`) are prerendered only when their module\n * exports `getStaticPaths()`; otherwise they are SKIPPED with a build warning.\n *\n * This module is build-time only (no DOM, never shipped to the client) — it\n * does NOT get a `.size-limit.json` row.\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join, resolve as resolvePath } from 'node:path'\nimport type { RouteSegment } from '@aihu/router'\nimport { readRouteSidecar, scanPages } from '@aihu/router/plugin'\nimport type { HeadConfig } from '@aihu/server'\nimport { renderToString, routeHeadToSsrHead } from '@aihu/server'\nimport type { ResolvedConfig } from 'vite'\nimport type { AihuConfig } from './config.ts'\nimport { applyHeadToHtml } from './head-apply.ts'\n\n/**\n * One param set for a dynamic route, as returned by `getStaticPaths()`.\n * Either a flat record of params, or `{ params: {...} }` (the latter mirrors\n * the common framework shape and is accepted for ergonomics).\n */\ntype StaticPathEntry = Record<string, string> | { params: Record<string, string> }\n\n/** The subset of a route module shape that the prerender consumes. */\ninterface PrerenderRouteModule {\n /** The renderable component — `() => arbor-tree` or `{ toHtml() }`. */\n default?: unknown\n /** Dynamic-route param sets to prerender. Absent → route is skipped + warned. */\n getStaticPaths?: () => StaticPathEntry[] | Promise<StaticPathEntry[]>\n}\n\n/** A loader that resolves a route file path to its evaluated module. */\nexport type SsrModuleLoader = (filePath: string) => Promise<PrerenderRouteModule>\n\n/** Derived route info for a single scanned page file. */\ninterface ScannedRoute {\n /** Absolute file path to the route module. */\n file: string\n /** URL pattern, e.g. `/`, `/about`, `/posts/:slug`. */\n pattern: string\n segments: RouteSegment[]\n /** Whether the pattern contains a `:param` / catchall segment. */\n dynamic: boolean\n}\n\n/** Result of a prerender run — surfaced for tests + logging. */\nexport interface PrerenderResult {\n /** outDir-relative HTML paths that were written. */\n written: string[]\n /** Human-readable warnings (e.g. skipped dynamic routes). */\n warnings: string[]\n}\n\n// ---------------------------------------------------------------------------\n// Route derivation (mirrors @aihu/router's file-router conventions)\n// ---------------------------------------------------------------------------\n\nfunction fileToSegments(rel: string): RouteSegment[] {\n const parts = rel\n .replace(/\\\\/g, '/')\n .replace(/\\.[^/.]+$/, '')\n .split('/')\n .filter(Boolean)\n .map(\n (p): RouteSegment =>\n p.startsWith('[...') && p.endsWith(']')\n ? { kind: 'catchall' }\n : p.startsWith('[') && p.endsWith(']')\n ? { kind: 'param', name: p.slice(1, -1) }\n : p.startsWith(':')\n ? { kind: 'param', name: p.slice(1) }\n : { kind: 'static', path: p },\n )\n // File-router convention: a trailing `index` segment maps to its parent dir.\n if (parts.length > 0) {\n const last = parts[parts.length - 1]!\n if (last.kind === 'static' && last.path === 'index') parts.pop()\n }\n return parts\n}\n\nfunction segmentsToPattern(segs: RouteSegment[]): string {\n if (segs.length === 0) return '/'\n return `/${segs\n .map((s) => (s.kind === 'static' ? s.path : s.kind === 'param' ? `:${s.name}` : '*'))\n .join('/')}`\n}\n\nfunction deriveRoutes(root: string, pagesDir: string): ScannedRoute[] {\n const { routes } = scanPages(root, pagesDir)\n const pagesAbs = resolvePath(root, pagesDir).replace(/\\\\/g, '/')\n return routes.map((file) => {\n const norm = file.replace(/\\\\/g, '/')\n const rel = norm.startsWith(`${pagesAbs}/`) ? norm.slice(pagesAbs.length + 1) : norm\n const segments = fileToSegments(rel)\n const pattern = segmentsToPattern(segments)\n const dynamic = segments.some((s) => s.kind === 'param' || s.kind === 'catchall')\n return { file, pattern, segments, dynamic }\n })\n}\n\n/** Substitute `:param` segments with concrete values to form a concrete path. */\nfunction fillPattern(segments: RouteSegment[], params: Record<string, string>): string {\n if (segments.length === 0) return '/'\n const parts = segments.map((s) => {\n if (s.kind === 'static') return s.path\n if (s.kind === 'param') return params[s.name] ?? ''\n // catchall: accept either '*' or a named param for the rest\n return params['*'] ?? ''\n })\n return `/${parts.filter((p) => p !== '').join('/')}`\n}\n\nfunction normalizeStaticPathEntry(entry: StaticPathEntry): Record<string, string> {\n if (\n entry &&\n typeof entry === 'object' &&\n 'params' in entry &&\n (entry as { params?: unknown }).params\n ) {\n return (entry as { params: Record<string, string> }).params\n }\n return entry as Record<string, string>\n}\n\n// ---------------------------------------------------------------------------\n// HTML templating\n//\n// The HeadConfig→template head transform (`applyHeadToHtml`) lives in the\n// shared `./head-apply.ts` module so the SSG path (here) and the client-nav\n// path (client.ts, B5) key/merge/escape tags identically and can never diverge.\n// ---------------------------------------------------------------------------\n\n/** Inject rendered route content into the outlet element of the template. */\nfunction injectContent(html: string, content: string, outletId: string): string {\n const escaped = outletId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n // Match an empty outlet `<div id=\"outlet\"></div>` (the SPA scaffold shape).\n const emptyRe = new RegExp(\n `(<[a-zA-Z]+\\\\b[^>]*\\\\bid=\"${escaped}\"[^>]*>)(\\\\s*)(</[a-zA-Z]+>)`,\n 'i',\n )\n if (emptyRe.test(html)) {\n return html.replace(emptyRe, `$1${content}$3`)\n }\n // Fallback: open-tag only — insert content right after it.\n const openRe = new RegExp(`(<[a-zA-Z]+\\\\b[^>]*\\\\bid=\"${escaped}\"[^>]*>)`, 'i')\n if (openRe.test(html)) {\n return html.replace(openRe, `$1${content}`)\n }\n return html\n}\n\n// ---------------------------------------------------------------------------\n// Prerender driver\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the renderable component from a loaded route module.\n *\n * The compiled-`.aihu` happy path registers a custom element as an import\n * side-effect and may not expose a `default`. Hand-authored route modules and\n * SSG-targeted pages export a `default` renderable (`() => arbor-tree` or\n * `{ toHtml() }`) — the same contract @aihu/server's router `handle()` uses.\n * Returns `null` when no renderable is present.\n */\nfunction resolveComponent(\n mod: PrerenderRouteModule,\n): (() => unknown) | { toHtml(): string } | null {\n const d = mod.default\n if (typeof d === 'function') return d as () => unknown\n if (d && typeof d === 'object' && typeof (d as { toHtml?: unknown }).toHtml === 'function') {\n return d as { toHtml(): string }\n }\n return null\n}\n\n/** Convert a route pattern to its `index.html` output path under outDir. */\nfunction patternToHtmlPath(pattern: string): string {\n if (pattern === '/') return 'index.html'\n const clean = pattern.replace(/^\\//, '').replace(/\\/$/, '')\n return join(clean, 'index.html')\n}\n\nexport interface RunPrerenderOptions {\n resolvedViteConfig: ResolvedConfig\n config: AihuConfig | undefined\n /**\n * Loads a route module by absolute file path. The default driver\n * (`prerenderClose`) wires this to a short-lived Vite SSR loader.\n */\n loadModule: SsrModuleLoader\n /** Emits a warning (skipped dynamic routes, missing renderables). */\n warn: (msg: string) => void\n}\n\n/**\n * Run the SSG prerender. Enumerates routes, renders each static route (and any\n * dynamic route that exports `getStaticPaths`), and writes per-route HTML into\n * the Vite build's outDir using the built `index.html` as the template.\n */\nexport async function runPrerender(opts: RunPrerenderOptions): Promise<PrerenderResult> {\n const { resolvedViteConfig, config, loadModule, warn } = opts\n const root = resolvedViteConfig.root\n const outDir = resolvePath(root, resolvedViteConfig.build.outDir)\n const pagesDir = config?.dir?.pages ?? 'pages'\n const siteUrl = config?.site?.url\n const globalHead = config?.app?.head as HeadConfig | undefined\n const outletId = 'outlet'\n\n const result: PrerenderResult = { written: [], warnings: [] }\n const pushWarn = (msg: string): void => {\n result.warnings.push(msg)\n warn(msg)\n }\n\n // The built index.html is our template — it already carries the hashed client\n // bundle <script> tags + base <head>, so reusing it gives free hydration.\n const templatePath = resolvePath(outDir, 'index.html')\n let template: string\n try {\n template = await readFile(templatePath, 'utf8')\n } catch {\n pushWarn(\n `[@aihu/app] static output: no index.html in ${outDir} — cannot prerender. ` +\n `Ensure the SPA build produced an index.html.`,\n )\n return result\n }\n\n const routes = deriveRoutes(root, pagesDir)\n\n for (const route of routes) {\n let mod: PrerenderRouteModule\n try {\n mod = await loadModule(route.file)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n pushWarn(`[@aihu/app] static output: failed to load route ${route.pattern}: ${msg}`)\n continue\n }\n\n const sidecar = readRouteSidecar(route.file)\n const head = sidecar?.head\n\n const component = resolveComponent(mod)\n if (!component) {\n pushWarn(\n `[@aihu/app] static output: route ${route.pattern} has no renderable default export — ` +\n `skipping content prerender (the SPA shell still ships).`,\n )\n continue\n }\n\n // Build the param-path list to render: static routes render once with no\n // params; dynamic routes require getStaticPaths().\n let paramSets: Array<Record<string, string>>\n if (route.dynamic) {\n if (typeof mod.getStaticPaths !== 'function') {\n pushWarn(\n `[@aihu/app] static output: dynamic route ${route.pattern} has no getStaticPaths() — ` +\n `skipped. Export getStaticPaths() to prerender its paths.`,\n )\n continue\n }\n const raw = await mod.getStaticPaths()\n paramSets = (raw ?? []).map(normalizeStaticPathEntry)\n if (paramSets.length === 0) {\n pushWarn(\n `[@aihu/app] static output: dynamic route ${route.pattern} getStaticPaths() returned ` +\n `no paths — nothing prerendered for this route.`,\n )\n }\n } else {\n paramSets = [{}]\n }\n\n for (const params of paramSets) {\n const concretePath = route.dynamic ? fillPattern(route.segments, params) : route.pattern\n\n let content: string\n try {\n content = await renderToString(component)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n pushWarn(`[@aihu/app] static output: render failed for ${concretePath}: ${msg}`)\n continue\n }\n\n const lowered = routeHeadToSsrHead(head, {\n ...(siteUrl !== undefined ? { siteUrl } : {}),\n ...(globalHead !== undefined ? { globalHead } : {}),\n })\n let html = applyHeadToHtml(template, lowered)\n html = injectContent(html, content, outletId)\n\n const relPath = patternToHtmlPath(concretePath)\n const absPath = resolvePath(outDir, relPath)\n await mkdir(dirname(absPath), { recursive: true })\n await writeFile(absPath, html, 'utf8')\n result.written.push(relPath.replace(/\\\\/g, '/'))\n }\n }\n\n return result\n}\n\n/**\n * `closeBundle` driver for the SSG prerender. Spins up a short-lived Vite SSR\n * module loader (middleware mode) so route files compile exactly like the dev\n * pipeline (`.aihu`, TS, virtual modules), runs `runPrerender`, then tears the\n * loader down.\n */\nexport async function prerenderClose(\n resolvedViteConfig: ResolvedConfig,\n config: AihuConfig | undefined,\n warn: (msg: string) => void,\n): Promise<PrerenderResult> {\n // Lazy import of Vite so this stays out of any non-build path.\n const { createServer } = await import('vite')\n // Reuse the resolved plugin chain so route files compile exactly like the\n // build (compiler for `.aihu`, router for virtual modules), but drop our own\n // build-only sentinels — they have no role in a module-loading dev server and\n // would only add noise. (closeBundle does not fire in middleware mode, so the\n // SSG plugin cannot re-enter even if left in.)\n const plugins = (\n (resolvedViteConfig.plugins as ReadonlyArray<{ name?: string }> | undefined) ?? []\n ).filter((p) => p?.name !== 'aihu-ssg' && p?.name !== 'aihu-adapter')\n const server = await createServer({\n root: resolvedViteConfig.root,\n configFile: false,\n appType: 'custom',\n logLevel: 'silent',\n server: { middlewareMode: true, hmr: false },\n plugins: plugins as never,\n })\n try {\n const loadModule: SsrModuleLoader = async (filePath) =>\n (await server.ssrLoadModule(filePath)) as PrerenderRouteModule\n return await runPrerender({ resolvedViteConfig, config, loadModule, warn })\n } finally {\n await server.close()\n }\n}\n","import { cp, writeFile as fsWriteFile, mkdir } from 'node:fs/promises'\nimport { dirname, resolve as resolvePath } from 'node:path'\n// Build-time sub-plugin imports. These are devDependencies of @aihu/app and\n// are marked external in rolldown.config.ts — they are never bundled.\nimport { aihuCompilerPlugin } from '@aihu/compiler'\nimport type { RouteDefinition } from '@aihu/router'\nimport { scanPages, viteRouterIntegration } from '@aihu/router/plugin'\nimport type { Plugin, ResolvedConfig } from 'vite'\nimport type { AdapterContext, CreateHandlerSourceOptions } from './adapter.ts'\nimport type { AihuConfig } from './config.ts'\nimport { applyHeadConfig } from './head.ts'\nimport { prerenderClose } from './prerender.ts'\n\n/** Map a pages-dir file path to a minimal RouteDefinition for adapter context. */\nfunction fileToRouteDefinition(filePath: string, _root: string, pagesDir: string): RouteDefinition {\n // Derive a URL pattern from the file path relative to the pages directory.\n const rel = filePath\n .replace(/\\\\/g, '/')\n .replace(new RegExp(`^.*?${pagesDir}/`), '')\n .replace(/\\.[^.]+$/, '') // strip extension\n const parts = rel.split('/').filter(Boolean)\n // Strip trailing 'index' segment (file-router convention)\n if (parts.length > 0 && parts[parts.length - 1] === 'index') parts.pop()\n\n const segments = parts.map((p) =>\n p.startsWith('[...') && p.endsWith(']')\n ? { kind: 'catchall' as const }\n : p.startsWith('[') && p.endsWith(']')\n ? { kind: 'param' as const, name: p.slice(1, -1) }\n : { kind: 'static' as const, path: p },\n )\n\n const pattern =\n segments.length === 0\n ? '/'\n : '/' +\n segments\n .map((s) => (s.kind === 'static' ? s.path : s.kind === 'param' ? `:${s.name}` : '*'))\n .join('/')\n\n return {\n pattern,\n segments,\n module: () => Promise.resolve({ default: null }),\n }\n}\n\n/** Build the AdapterContext object passed to adapter.adapt(). */\nfunction buildAdapterContext(\n resolvedViteConfig: ResolvedConfig,\n routeFiles: string[],\n config: AihuConfig | undefined,\n): AdapterContext {\n const outDir = resolvePath(resolvedViteConfig.root, resolvedViteConfig.build.outDir)\n const root = resolvedViteConfig.root\n const pagesDir = config?.dir?.pages ?? 'pages'\n\n const routes: RouteDefinition[] = routeFiles.map((f) => fileToRouteDefinition(f, root, pagesDir))\n\n return {\n outDir,\n root,\n routes,\n config: config ?? {},\n\n async emitFile(path: string, content: string): Promise<void> {\n const abs = resolvePath(outDir, path)\n await mkdir(dirname(abs), { recursive: true })\n await fsWriteFile(abs, content, 'utf8')\n },\n\n async copy(src: string, dest: string): Promise<void> {\n await mkdir(dirname(dest), { recursive: true })\n await cp(src, dest, { recursive: true, force: true })\n },\n\n async writeFile(absolutePath: string, content: string): Promise<void> {\n await mkdir(dirname(absolutePath), { recursive: true })\n await fsWriteFile(absolutePath, content, 'utf8')\n },\n\n createHandlerSource(opts?: CreateHandlerSourceOptions): string {\n const routesSpec = opts?.routesSpecifier ?? './routes-manifest.js'\n const serverSpec = opts?.serverSpecifier ?? '@aihu/server'\n return [\n `// AUTO-GENERATED — do not edit`,\n `import { createRequestRouter } from '${serverSpec}'`,\n `import routes from '${routesSpec}'`,\n `const _manifest = { routes }`,\n `const _handler = createRequestRouter(_manifest)`,\n `export { _handler as handler }`,\n ].join('\\n')\n },\n }\n}\n\n/**\n * viteAihuPlugin() — composed Vite plugin for aihu SPA projects.\n *\n * Returns Plugin[] composing:\n * [0] aihuCompilerPlugin (enforce:'pre') — transforms .aihu SFCs\n * [1] viteRouterIntegration — serves virtual:aihu-routes + virtual:aihu-layouts\n * [2] aihu-agent-readiness (opt-in) or no-op\n * [3] aihu-head (injects config.app.head into index.html <head>)\n * [4..n] user plugins from config.plugins\n * [n+1] aihu-vite-passthrough (merges config.vite into Vite's resolved config)\n * [n+2] aihu-adapter (adapter.adapt() on closeBundle, build mode only)\n *\n * @example\n * // vite.config.ts\n * import { defineConfig } from 'vite'\n * import { viteAihuPlugin } from '@aihu/app'\n * export default defineConfig({ plugins: [viteAihuPlugin()] })\n *\n * @example\n * // With adapter\n * import { cloudflare } from '@aihu/adapter-cloudflare'\n * export default defineConfig({\n * plugins: [viteAihuPlugin({\n * dir: { pages: 'src/pages' },\n * adapter: cloudflare({ name: 'my-worker' }),\n * })]\n * })\n */\nexport function viteAihuPlugin(config?: AihuConfig): Plugin[] {\n const routerOpts = {\n pagesDir: config?.dir?.pages ?? 'pages',\n layoutsDir: config?.dir?.layouts ?? 'src/layouts',\n }\n\n // Agent readiness: opt-in only. No safe default for `name`.\n let agentPlugin: Plugin\n const ar = config?.agentReadiness\n if (ar) {\n // Dynamic import to avoid pulling @aihu-plugin/agent-readiness into the bundle\n // when it is not configured. The `require` below is evaluated at runtime\n // in Node.js (vite.config.ts execution context), not in the browser.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { viteAgentReadinessIntegration } =\n require('@aihu-plugin/agent-readiness') as typeof import('@aihu-plugin/agent-readiness')\n agentPlugin = viteAgentReadinessIntegration(ar) as unknown as Plugin\n } else {\n // Stable no-op so plugin-inspector shows a meaningful entry\n agentPlugin = { name: 'aihu-agent-readiness-disabled' }\n }\n\n // Head injection — applies config.app.head into the built index.html <head>.\n // Without this hook the configured global head (title/charset/viewport/meta)\n // is silently dropped from SPA/static output, hurting SEO and non-JS agents.\n const headPlugin: Plugin = {\n name: 'aihu-head',\n transformIndexHtml: {\n // Run after Vite's core HTML processing so our config wins over the\n // scaffold defaults present in the source index.html.\n order: 'post',\n handler(html: string): string {\n return applyHeadConfig(html, config?.app?.head)\n },\n },\n }\n\n // Vite config passthrough — deep-merged by Vite via the config() hook return value.\n const passthroughPlugin: Plugin = {\n name: 'aihu-vite-passthrough',\n config() {\n return (config?.vite ?? {}) as import('vite').UserConfig\n },\n }\n\n // Adapter sentinel — calls adapter.adapt() after build completes.\n // Registered unconditionally; short-circuits immediately if no adapter is set.\n let resolvedViteConfig: ResolvedConfig | null = null\n\n const adapterPlugin: Plugin = {\n name: 'aihu-adapter',\n apply: 'build',\n configResolved(rc) {\n resolvedViteConfig = rc\n },\n async closeBundle() {\n const adapter = config?.adapter\n if (!adapter || !resolvedViteConfig) return\n\n const pagesDir = config?.dir?.pages ?? 'pages'\n const { routes: routeFiles } = scanPages(resolvedViteConfig.root, pagesDir)\n const context = buildAdapterContext(resolvedViteConfig, routeFiles, config)\n\n try {\n await adapter.adapt(context)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n this.error(`[@aihu/app] Adapter '${adapter.name}' failed: ${msg}`)\n }\n },\n }\n\n // SSG prerender — active only when `output: 'static'`. Runs after Vite writes\n // the SPA build and before the adapter (so an adapter, if present, sees the\n // per-route HTML). Prerenders every static route to a content-ful\n // `<pattern>/index.html` that hydrates into the SPA. `output: 'spa'` is a\n // no-op here, preserving the existing empty-shell behavior.\n let ssgResolvedConfig: ResolvedConfig | null = null\n const ssgPlugin: Plugin = {\n name: 'aihu-ssg',\n apply: 'build',\n configResolved(rc) {\n ssgResolvedConfig = rc\n },\n // Run before the adapter's closeBundle (plugin order in the array is honored\n // for sequential closeBundle hooks).\n async closeBundle() {\n if (config?.output !== 'static' || !ssgResolvedConfig) return\n try {\n await prerenderClose(ssgResolvedConfig, config, (msg) => this.warn(msg))\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n this.error(`[@aihu/app] static (SSG) prerender failed: ${msg}`)\n }\n },\n }\n\n return [\n // SPA mode: route components are top-level mounts that frequently use\n // lifecycle hooks (onMount/onCleanup) and rely on the runtime/signals\n // owner context regardless of whether they call signal() directly. The\n // static-island optimization is unsafe to apply silently here — it strips\n // defineComponent and breaks `no owner` for any module touching lifecycle.\n // It also saves ~0 B in practice because the runtime already ships in the\n // main bundle. Default islands off; opt back in via the compiler plugin\n // directly if you genuinely have an MPA-style mixed-island layout.\n // `css.shadowMode` (when set) forwards to the compiler's per-plugin\n // shadowMode injection — required for consumers using `@aihu/css-engine`\n // utility classes (or any cascade-dependent CSS framework) which need\n // `'none'` so styles aren't trapped in shadow roots.\n aihuCompilerPlugin({\n islands: false,\n ...(config?.css?.shadowMode != null ? { shadowMode: config.css.shadowMode } : {}),\n }) as unknown as Plugin,\n viteRouterIntegration(routerOpts) as unknown as Plugin,\n agentPlugin,\n headPlugin,\n ...((config?.plugins ?? []) as Plugin[]),\n passthroughPlugin,\n ssgPlugin,\n adapterPlugin,\n ]\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["escapeAttr","escapeText","resolvePath","resolvePath","fsWriteFile"],"sources":["../src/config.ts","../src/head.ts","../src/head-apply.ts","../src/prerender.ts","../src/vite-plugin.ts"],"sourcesContent":["import type { Plugin, UserConfig } from 'vite'\nimport type { AihuAdapter } from './adapter.ts'\n\n/**\n * Build output mode.\n *\n * - `'spa'` (default): a single empty-shell `index.html` that boots the client\n * SPA. No per-route HTML, no prerendered content.\n * - `'static'` (SSG): prerenders every static route to a content-ful\n * `<pattern>/index.html` with a per-page `<head>`, then hydrates into the SPA\n * on load (progressive enhancement). Ideal for content sites on static hosts\n * (e.g. Cloudflare Pages) — crawlers and non-JS agents see real content.\n *\n * Other rendering modes (`ssr`, `hybrid`) are tracked separately under\n * @aihu/server's RenderingMode and are not part of the app build OutputMode.\n */\nexport type OutputMode = 'spa' | 'static'\n\n/** Site-level configuration. */\nexport interface SiteConfig {\n /**\n * Absolute base URL of the deployed site (e.g. `https://example.com`).\n * Used by the `'static'` (SSG) output mode to resolve relative per-route\n * `canonical` / `og:*` / `twitter:*` URLs into absolute URLs (passed as\n * `siteUrl` to @aihu/server's `routeHeadToSsrHead`). When absent, relative\n * URLs are emitted unchanged.\n */\n readonly url?: string\n}\n\nexport interface DirConfig {\n /** Directory to scan for page routes. Default: 'pages' */\n readonly pages?: string\n /** Directory to scan for layout files. Default: 'src/layouts' */\n readonly layouts?: string\n /** Public static assets directory. Default: 'public' */\n readonly public?: string\n}\n\n/** Runtime configuration split. Public values are safe to expose to the client. */\nexport interface RuntimeConfig {\n readonly public?: Record<string, unknown>\n /** V0: accepted but ignored at runtime (server-side enforcement deferred to V1). */\n readonly private?: Record<string, unknown>\n}\n\nexport interface HeadConfig {\n readonly title?: string\n /** Default: 'UTF-8' */\n readonly charset?: string\n /** Default: 'width=device-width, initial-scale=1' */\n readonly viewport?: string\n readonly meta?: ReadonlyArray<Record<string, string>>\n}\n\nexport interface AppHeadConfig {\n readonly head?: HeadConfig\n}\n\n/** Vite config fields that can be safely merged (excludes plugins — use AihuConfig.plugins). */\nexport type VitePassthrough = Omit<UserConfig, 'plugins'>\n\n/** A Aihu plugin is structurally identical to a Vite plugin (V0). */\nexport type AihuPlugin = Plugin\n\n/** Type-only import — not bundled when agentReadiness is absent. */\nexport type AgentReadinessConfig = import('@aihu-plugin/agent-readiness').AgentReadinessConfig\n\n/**\n * CSS / styling options forwarded to the compiler's Vite plugin.\n *\n * Today this surfaces the shadow-DOM mode the compiler injects into every\n * `defineElement(...)` call. The default (`'open'`) keeps shadow-encapsulated\n * component styles. `@aihu/css-engine` is scoped by design and works in any\n * mode — its utilities fold into each component's shadow style. Global-cascade\n * frameworks (Tailwind, UnoCSS, Pico) — or styling light-DOM / external\n * (slotted) children — need `'none'`.\n *\n * When set, `viteAihuPlugin` forwards this to its internal\n * `aihuCompilerPlugin({ shadowMode })` call. When absent, behaviour is\n * unchanged (compiler default applies).\n */\nexport interface CssConfig {\n /**\n * Project-wide shadow-DOM mode for every `.aihu` SFC compiled by\n * `viteAihuPlugin`.\n *\n * - `'open'` — default browser behaviour (shadow root, externally readable).\n * - `'closed'` — shadow root, externally hidden.\n * - `'none'` — **no shadow root.** Use for global-cascade CSS frameworks,\n * or when you explicitly want light-DOM / global CSS (e.g. to\n * style external / slotted child elements). NOT required for\n * `@aihu/css-engine`, which is scoped and works in any mode.\n */\n readonly shadowMode?: 'open' | 'closed' | 'none'\n}\n\n/** Router-related app config (arch-5 M1, RFC-A5-012). */\nexport interface RouterConfig {\n /**\n * When `true`, `<$link>` navigation wraps in `document.startViewTransition()`\n * if the browser supports the View Transitions API. No-op in unsupported\n * browsers (graceful degradation). Default: `false`.\n *\n * SSR safety: the wrapping is browser-only — server-rendered HTML is\n * unchanged, and hydration is unaffected.\n */\n readonly viewTransitions?: boolean\n}\n\nexport interface AihuConfig {\n /** Directory layout overrides. */\n readonly dir?: DirConfig\n /**\n * Output mode. Supports `'spa'` (default) and `'static'` (SSG prerender).\n * defineConfig throws AihuConfigError for any other value.\n */\n readonly output?: OutputMode\n /**\n * Site-level configuration. `site.url` is the absolute base URL used by the\n * `'static'` output mode to resolve relative canonical/OG/Twitter URLs.\n */\n readonly site?: SiteConfig\n /**\n * Aihu plugins. Order is preserved.\n * Appended after the three framework plugins (compiler, router, agent-readiness).\n */\n readonly plugins?: ReadonlyArray<AihuPlugin>\n /** Runtime configuration split — public values are inlined in the client bundle. */\n readonly runtimeConfig?: RuntimeConfig\n /**\n * App-level values made available to all components as bare identifiers.\n * Declared here for documentation and future build-time validation; the\n * values are hoisted into globalThis by createApp() at runtime.\n *\n * @example\n * export default defineConfig({ provide: { supabase, checkAuth } })\n */\n readonly provide?: Record<string, unknown>\n /** HTML <head> metadata. */\n readonly app?: AppHeadConfig\n /** Passthrough to Vite's UserConfig. Merged via Vite's config() hook. */\n readonly vite?: VitePassthrough\n /**\n * Opt-in agent-readiness integration.\n * Requires { name: string } at minimum.\n * When absent or false, a no-op plugin is substituted.\n */\n readonly agentReadiness?: AgentReadinessConfig | false\n /**\n * Deployment adapter. Transforms the Vite build output into the target\n * platform's required format. Called after vite build completes.\n * When absent, no post-build transformation is applied (manual deployment).\n */\n readonly adapter?: AihuAdapter\n /**\n * Router-related app config (arch-5 M1).\n * Currently exposes the `viewTransitions` opt-in for `<$link>`.\n */\n readonly router?: RouterConfig\n /**\n * CSS / styling integration. Currently surfaces the project-wide\n * `shadowMode` forwarded to the compiler. Set to `{ shadowMode: 'none' }`\n * when using `@aihu/css-engine` utility classes or any other cascade-\n * dependent CSS framework.\n */\n readonly css?: CssConfig\n}\n\n/** Thrown by defineConfig when configuration validation fails. */\nexport class AihuConfigError extends Error {\n constructor(\n message: string,\n readonly code:\n | 'INVALID_OUTPUT_MODE'\n | 'INVALID_DIR'\n | 'UNKNOWN_FIELD'\n | 'INVALID_CSS_SHADOW_MODE',\n readonly field?: string,\n ) {\n super(message)\n this.name = 'AihuConfigError'\n }\n}\n\n/**\n * Define the aihu application configuration.\n *\n * Validates the config at call time and throws AihuConfigError for invalid values.\n * Returns the config unchanged (typed identity function).\n *\n * @example\n * // aihu.config.ts\n * import { defineConfig } from '@aihu/app'\n * export default defineConfig({\n * app: { head: { title: 'My App' } },\n * })\n */\nexport function defineConfig(config: AihuConfig): AihuConfig {\n if (config.output && config.output !== 'spa' && config.output !== 'static') {\n throw new AihuConfigError(\n `output mode '${config.output}' is not supported (use 'spa' or 'static')`,\n 'INVALID_OUTPUT_MODE',\n 'output',\n )\n }\n if (config.dir?.pages !== undefined && typeof config.dir.pages !== 'string') {\n throw new AihuConfigError('dir.pages must be a string', 'INVALID_DIR', 'dir.pages')\n }\n if (config.dir?.layouts !== undefined && typeof config.dir.layouts !== 'string') {\n throw new AihuConfigError('dir.layouts must be a string', 'INVALID_DIR', 'dir.layouts')\n }\n if (\n config.css?.shadowMode !== undefined &&\n config.css.shadowMode !== 'open' &&\n config.css.shadowMode !== 'closed' &&\n config.css.shadowMode !== 'none'\n ) {\n throw new AihuConfigError(\n `css.shadowMode '${config.css.shadowMode}' is not supported (use 'open', 'closed', or 'none')`,\n 'INVALID_CSS_SHADOW_MODE',\n 'css.shadowMode',\n )\n }\n return config\n}\n","import type { HeadConfig } from './config.ts'\n\n/** Escape a string for safe inclusion in a double-quoted HTML attribute value. */\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\n/** Escape text for safe inclusion in element text content (e.g. <title>). */\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/**\n * Transform a built index.html, applying the app-level <head> config.\n *\n * Precedence rule: **config overrides source.** When the source index.html\n * already declares a tag that `app.head` also configures (title, charset,\n * viewport, or a meta with a matching name/property), the configured value\n * replaces the source value in place — no duplicates are emitted. Tags present\n * only in the source are left untouched; tags present only in config are\n * injected just before `</head>` (or appended if no `</head>` exists).\n *\n * This is the sensible precedence because `app.head` is the explicit,\n * type-checked intent of the application author in aihu.config.ts, whereas the\n * source index.html is typically a Vite scaffold default.\n */\nexport function applyHeadConfig(html: string, head: HeadConfig | undefined): string {\n if (!head) return html\n\n let out = html\n const inject: string[] = []\n\n // title → set/replace <title>\n if (head.title !== undefined) {\n const tag = `<title>${escapeText(head.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 // charset → <meta charset>\n if (head.charset !== undefined) {\n const tag = `<meta charset=\"${escapeAttr(head.charset)}\">`\n if (/<meta\\s+[^>]*charset\\s*=\\s*[\"'][^\"']*[\"'][^>]*>/i.test(out)) {\n out = out.replace(/<meta\\s+[^>]*charset\\s*=\\s*[\"'][^\"']*[\"'][^>]*>/i, tag)\n } else {\n inject.push(tag)\n }\n }\n\n // viewport → <meta name=\"viewport\">\n if (head.viewport !== undefined) {\n const tag = `<meta name=\"viewport\" content=\"${escapeAttr(head.viewport)}\">`\n const viewportRe = /<meta\\s+[^>]*name\\s*=\\s*[\"']viewport[\"'][^>]*>/i\n if (viewportRe.test(out)) {\n out = out.replace(viewportRe, tag)\n } else {\n inject.push(tag)\n }\n }\n\n // meta[] → one <meta> per entry, keyed by name/property (config overrides\n // any matching source meta; unkeyed metas are always injected).\n for (const entry of head.meta ?? []) {\n const key = entry.name !== undefined ? 'name' : entry.property !== undefined ? 'property' : null\n const keyVal = key ? entry[key] : undefined\n\n const attrs = Object.entries(entry)\n .map(([k, v]) => `${k}=\"${escapeAttr(String(v))}\"`)\n .join(' ')\n const tag = `<meta ${attrs}>`\n\n if (key && keyVal !== undefined) {\n const re = new RegExp(\n `<meta\\\\s+[^>]*${key}\\\\s*=\\\\s*[\"']${keyVal.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}[\"'][^>]*>`,\n 'i',\n )\n if (re.test(out)) {\n out = out.replace(re, tag)\n continue\n }\n }\n inject.push(tag)\n }\n\n if (inject.length === 0) return out\n\n const block = inject.join('\\n ')\n if (/<\\/head>/i.test(out)) {\n return out.replace(/<\\/head>/i, ` ${block}\\n </head>`)\n }\n // No </head> in source — append the tags so they are not silently dropped.\n return `${out}\\n${block}`\n}\n","/**\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","/**\n * SSG prerender (B4, SEO arc) — build-time only.\n *\n * When `output: 'static'`, this module runs in `viteAihuPlugin`'s `closeBundle`\n * after Vite has written the SPA build. For every STATIC route it:\n *\n * 1. Loads the route's REAL module by file path (via a short-lived Vite SSR\n * module loader — this compiles `.aihu`/`.ts` exactly like the dev/prod\n * pipeline). The `AdapterContext.routes` stub is NOT used here.\n * 2. Renders the route's component to content HTML with @aihu/server's\n * `renderToString`.\n * 3. Folds the route's `<head>` (from the `.route.json` sidecar) into a\n * renderable `HeadConfig` via `routeHeadToSsrHead`, resolving relative\n * canonical/OG/Twitter URLs against `site.url`.\n * 4. Uses the built `index.html` as a template — injecting the per-page head\n * into `<head>` and the rendered content into the SPA outlet — so the page\n * ships content-ful HTML for crawlers/agents AND keeps the client bundle\n * `<script>` tags that hydrate it into the live SPA (progressive\n * enhancement).\n * 5. Writes `<pattern>/index.html` (and `index.html` for `/`).\n *\n * Dynamic routes (`:param` / `[param]`) are prerendered only when their module\n * exports `getStaticPaths()`; otherwise they are SKIPPED with a build warning.\n *\n * This module is build-time only (no DOM, never shipped to the client) — it\n * does NOT get a `.size-limit.json` row.\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join, resolve as resolvePath } from 'node:path'\nimport type { RouteSegment } from '@aihu/router'\nimport { readRouteSidecar, scanPages } from '@aihu/router/plugin'\nimport type { HeadConfig } from '@aihu/server'\nimport { renderToString, routeHeadToSsrHead } from '@aihu/server'\nimport type { ResolvedConfig } from 'vite'\nimport type { AihuConfig } from './config.ts'\nimport { applyHeadToHtml } from './head-apply.ts'\n\n/**\n * One param set for a dynamic route, as returned by `getStaticPaths()`.\n * Either a flat record of params, or `{ params: {...} }` (the latter mirrors\n * the common framework shape and is accepted for ergonomics).\n */\ntype StaticPathEntry = Record<string, string> | { params: Record<string, string> }\n\n/** The subset of a route module shape that the prerender consumes. */\ninterface PrerenderRouteModule {\n /** The renderable component — `() => arbor-tree` or `{ toHtml() }`. */\n default?: unknown\n /** Dynamic-route param sets to prerender. Absent → route is skipped + warned. */\n getStaticPaths?: () => StaticPathEntry[] | Promise<StaticPathEntry[]>\n}\n\n/** A loader that resolves a route file path to its evaluated module. */\nexport type SsrModuleLoader = (filePath: string) => Promise<PrerenderRouteModule>\n\n/** Derived route info for a single scanned page file. */\ninterface ScannedRoute {\n /** Absolute file path to the route module. */\n file: string\n /** URL pattern, e.g. `/`, `/about`, `/posts/:slug`. */\n pattern: string\n segments: RouteSegment[]\n /** Whether the pattern contains a `:param` / catchall segment. */\n dynamic: boolean\n}\n\n/** Result of a prerender run — surfaced for tests + logging. */\nexport interface PrerenderResult {\n /** outDir-relative HTML paths that were written. */\n written: string[]\n /** Human-readable warnings (e.g. skipped dynamic routes). */\n warnings: string[]\n}\n\n// ---------------------------------------------------------------------------\n// Route derivation (mirrors @aihu/router's file-router conventions)\n// ---------------------------------------------------------------------------\n\nfunction fileToSegments(rel: string): RouteSegment[] {\n const parts = rel\n .replace(/\\\\/g, '/')\n .replace(/\\.[^/.]+$/, '')\n .split('/')\n .filter(Boolean)\n .map(\n (p): RouteSegment =>\n p.startsWith('[...') && p.endsWith(']')\n ? { kind: 'catchall' }\n : p.startsWith('[') && p.endsWith(']')\n ? { kind: 'param', name: p.slice(1, -1) }\n : p.startsWith(':')\n ? { kind: 'param', name: p.slice(1) }\n : { kind: 'static', path: p },\n )\n // File-router convention: a trailing `index` segment maps to its parent dir.\n if (parts.length > 0) {\n const last = parts[parts.length - 1]!\n if (last.kind === 'static' && last.path === 'index') parts.pop()\n }\n return parts\n}\n\nfunction segmentsToPattern(segs: RouteSegment[]): string {\n if (segs.length === 0) return '/'\n return `/${segs\n .map((s) => (s.kind === 'static' ? s.path : s.kind === 'param' ? `:${s.name}` : '*'))\n .join('/')}`\n}\n\nfunction deriveRoutes(root: string, pagesDir: string): ScannedRoute[] {\n const { routes } = scanPages(root, pagesDir)\n const pagesAbs = resolvePath(root, pagesDir).replace(/\\\\/g, '/')\n return routes.map((file) => {\n const norm = file.replace(/\\\\/g, '/')\n const rel = norm.startsWith(`${pagesAbs}/`) ? norm.slice(pagesAbs.length + 1) : norm\n const segments = fileToSegments(rel)\n const pattern = segmentsToPattern(segments)\n const dynamic = segments.some((s) => s.kind === 'param' || s.kind === 'catchall')\n return { file, pattern, segments, dynamic }\n })\n}\n\n/** Substitute `:param` segments with concrete values to form a concrete path. */\nfunction fillPattern(segments: RouteSegment[], params: Record<string, string>): string {\n if (segments.length === 0) return '/'\n const parts = segments.map((s) => {\n if (s.kind === 'static') return s.path\n if (s.kind === 'param') return params[s.name] ?? ''\n // catchall: accept either '*' or a named param for the rest\n return params['*'] ?? ''\n })\n return `/${parts.filter((p) => p !== '').join('/')}`\n}\n\nfunction normalizeStaticPathEntry(entry: StaticPathEntry): Record<string, string> {\n if (\n entry &&\n typeof entry === 'object' &&\n 'params' in entry &&\n (entry as { params?: unknown }).params\n ) {\n return (entry as { params: Record<string, string> }).params\n }\n return entry as Record<string, string>\n}\n\n// ---------------------------------------------------------------------------\n// HTML templating\n//\n// The HeadConfig→template head transform (`applyHeadToHtml`) lives in the\n// shared `./head-apply.ts` module so the SSG path (here) and the client-nav\n// path (client.ts, B5) key/merge/escape tags identically and can never diverge.\n// ---------------------------------------------------------------------------\n\n/** Inject rendered route content into the outlet element of the template. */\nfunction injectContent(html: string, content: string, outletId: string): string {\n const escaped = outletId.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n // Match an empty outlet `<div id=\"outlet\"></div>` (the SPA scaffold shape).\n const emptyRe = new RegExp(\n `(<[a-zA-Z]+\\\\b[^>]*\\\\bid=\"${escaped}\"[^>]*>)(\\\\s*)(</[a-zA-Z]+>)`,\n 'i',\n )\n if (emptyRe.test(html)) {\n return html.replace(emptyRe, `$1${content}$3`)\n }\n // Fallback: open-tag only — insert content right after it.\n const openRe = new RegExp(`(<[a-zA-Z]+\\\\b[^>]*\\\\bid=\"${escaped}\"[^>]*>)`, 'i')\n if (openRe.test(html)) {\n return html.replace(openRe, `$1${content}`)\n }\n return html\n}\n\n// ---------------------------------------------------------------------------\n// Prerender driver\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the renderable component from a loaded route module.\n *\n * The compiled-`.aihu` happy path registers a custom element as an import\n * side-effect and may not expose a `default`. Hand-authored route modules and\n * SSG-targeted pages export a `default` renderable (`() => arbor-tree` or\n * `{ toHtml() }`) — the same contract @aihu/server's router `handle()` uses.\n * Returns `null` when no renderable is present.\n */\nfunction resolveComponent(\n mod: PrerenderRouteModule,\n): (() => unknown) | { toHtml(): string } | null {\n const d = mod.default\n if (typeof d === 'function') return d as () => unknown\n if (d && typeof d === 'object' && typeof (d as { toHtml?: unknown }).toHtml === 'function') {\n return d as { toHtml(): string }\n }\n return null\n}\n\n/** Convert a route pattern to its `index.html` output path under outDir. */\nfunction patternToHtmlPath(pattern: string): string {\n if (pattern === '/') return 'index.html'\n const clean = pattern.replace(/^\\//, '').replace(/\\/$/, '')\n return join(clean, 'index.html')\n}\n\nexport interface RunPrerenderOptions {\n resolvedViteConfig: ResolvedConfig\n config: AihuConfig | undefined\n /**\n * Loads a route module by absolute file path. The default driver\n * (`prerenderClose`) wires this to a short-lived Vite SSR loader.\n */\n loadModule: SsrModuleLoader\n /** Emits a warning (skipped dynamic routes, missing renderables). */\n warn: (msg: string) => void\n}\n\n/**\n * Run the SSG prerender. Enumerates routes, renders each static route (and any\n * dynamic route that exports `getStaticPaths`), and writes per-route HTML into\n * the Vite build's outDir using the built `index.html` as the template.\n */\nexport async function runPrerender(opts: RunPrerenderOptions): Promise<PrerenderResult> {\n const { resolvedViteConfig, config, loadModule, warn } = opts\n const root = resolvedViteConfig.root\n const outDir = resolvePath(root, resolvedViteConfig.build.outDir)\n const pagesDir = config?.dir?.pages ?? 'pages'\n const siteUrl = config?.site?.url\n const globalHead = config?.app?.head as HeadConfig | undefined\n const outletId = 'outlet'\n\n const result: PrerenderResult = { written: [], warnings: [] }\n const pushWarn = (msg: string): void => {\n result.warnings.push(msg)\n warn(msg)\n }\n\n // The built index.html is our template — it already carries the hashed client\n // bundle <script> tags + base <head>, so reusing it gives free hydration.\n const templatePath = resolvePath(outDir, 'index.html')\n let template: string\n try {\n template = await readFile(templatePath, 'utf8')\n } catch {\n pushWarn(\n `[@aihu/app] static output: no index.html in ${outDir} — cannot prerender. ` +\n `Ensure the SPA build produced an index.html.`,\n )\n return result\n }\n\n const routes = deriveRoutes(root, pagesDir)\n\n for (const route of routes) {\n let mod: PrerenderRouteModule\n try {\n mod = await loadModule(route.file)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n pushWarn(`[@aihu/app] static output: failed to load route ${route.pattern}: ${msg}`)\n continue\n }\n\n const sidecar = readRouteSidecar(route.file)\n const head = sidecar?.head\n\n const component = resolveComponent(mod)\n if (!component) {\n pushWarn(\n `[@aihu/app] static output: route ${route.pattern} has no renderable default export — ` +\n `skipping content prerender (the SPA shell still ships).`,\n )\n continue\n }\n\n // Build the param-path list to render: static routes render once with no\n // params; dynamic routes require getStaticPaths().\n let paramSets: Array<Record<string, string>>\n if (route.dynamic) {\n if (typeof mod.getStaticPaths !== 'function') {\n pushWarn(\n `[@aihu/app] static output: dynamic route ${route.pattern} has no getStaticPaths() — ` +\n `skipped. Export getStaticPaths() to prerender its paths.`,\n )\n continue\n }\n const raw = await mod.getStaticPaths()\n paramSets = (raw ?? []).map(normalizeStaticPathEntry)\n if (paramSets.length === 0) {\n pushWarn(\n `[@aihu/app] static output: dynamic route ${route.pattern} getStaticPaths() returned ` +\n `no paths — nothing prerendered for this route.`,\n )\n }\n } else {\n paramSets = [{}]\n }\n\n for (const params of paramSets) {\n const concretePath = route.dynamic ? fillPattern(route.segments, params) : route.pattern\n\n let content: string\n try {\n content = await renderToString(component)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n pushWarn(`[@aihu/app] static output: render failed for ${concretePath}: ${msg}`)\n continue\n }\n\n const lowered = routeHeadToSsrHead(head, {\n ...(siteUrl !== undefined ? { siteUrl } : {}),\n ...(globalHead !== undefined ? { globalHead } : {}),\n })\n let html = applyHeadToHtml(template, lowered)\n html = injectContent(html, content, outletId)\n\n const relPath = patternToHtmlPath(concretePath)\n const absPath = resolvePath(outDir, relPath)\n await mkdir(dirname(absPath), { recursive: true })\n await writeFile(absPath, html, 'utf8')\n result.written.push(relPath.replace(/\\\\/g, '/'))\n }\n }\n\n return result\n}\n\n/**\n * `closeBundle` driver for the SSG prerender. Spins up a short-lived Vite SSR\n * module loader (middleware mode) so route files compile exactly like the dev\n * pipeline (`.aihu`, TS, virtual modules), runs `runPrerender`, then tears the\n * loader down.\n */\nexport async function prerenderClose(\n resolvedViteConfig: ResolvedConfig,\n config: AihuConfig | undefined,\n warn: (msg: string) => void,\n): Promise<PrerenderResult> {\n // Lazy import of Vite so this stays out of any non-build path.\n const { createServer } = await import('vite')\n // Reuse the resolved plugin chain so route files compile exactly like the\n // build (compiler for `.aihu`, router for virtual modules), but drop our own\n // build-only sentinels — they have no role in a module-loading dev server and\n // would only add noise. (closeBundle does not fire in middleware mode, so the\n // SSG plugin cannot re-enter even if left in.)\n const plugins = (\n (resolvedViteConfig.plugins as ReadonlyArray<{ name?: string }> | undefined) ?? []\n ).filter((p) => p?.name !== 'aihu-ssg' && p?.name !== 'aihu-adapter')\n const server = await createServer({\n root: resolvedViteConfig.root,\n configFile: false,\n appType: 'custom',\n logLevel: 'silent',\n server: { middlewareMode: true, hmr: false },\n plugins: plugins as never,\n })\n try {\n const loadModule: SsrModuleLoader = async (filePath) =>\n (await server.ssrLoadModule(filePath)) as PrerenderRouteModule\n return await runPrerender({ resolvedViteConfig, config, loadModule, warn })\n } finally {\n await server.close()\n }\n}\n","import { cp, writeFile as fsWriteFile, mkdir } from 'node:fs/promises'\nimport { dirname, resolve as resolvePath } from 'node:path'\n// Build-time sub-plugin imports. These are devDependencies of @aihu/app and\n// are marked external in rolldown.config.ts — they are never bundled.\nimport { aihuCompilerPlugin } from '@aihu/compiler'\nimport type { RouteDefinition } from '@aihu/router'\nimport { scanPages, viteRouterIntegration } from '@aihu/router/plugin'\nimport type { Plugin, ResolvedConfig } from 'vite'\nimport type { AdapterContext, CreateHandlerSourceOptions } from './adapter.ts'\nimport type { AihuConfig } from './config.ts'\nimport { applyHeadConfig } from './head.ts'\nimport { prerenderClose } from './prerender.ts'\n\n/** Map a pages-dir file path to a minimal RouteDefinition for adapter context. */\nfunction fileToRouteDefinition(filePath: string, _root: string, pagesDir: string): RouteDefinition {\n // Derive a URL pattern from the file path relative to the pages directory.\n const rel = filePath\n .replace(/\\\\/g, '/')\n .replace(new RegExp(`^.*?${pagesDir}/`), '')\n .replace(/\\.[^.]+$/, '') // strip extension\n const parts = rel.split('/').filter(Boolean)\n // Strip trailing 'index' segment (file-router convention)\n if (parts.length > 0 && parts[parts.length - 1] === 'index') parts.pop()\n\n const segments = parts.map((p) =>\n p.startsWith('[...') && p.endsWith(']')\n ? { kind: 'catchall' as const }\n : p.startsWith('[') && p.endsWith(']')\n ? { kind: 'param' as const, name: p.slice(1, -1) }\n : { kind: 'static' as const, path: p },\n )\n\n const pattern =\n segments.length === 0\n ? '/'\n : '/' +\n segments\n .map((s) => (s.kind === 'static' ? s.path : s.kind === 'param' ? `:${s.name}` : '*'))\n .join('/')\n\n return {\n pattern,\n segments,\n module: () => Promise.resolve({ default: null }),\n }\n}\n\n/** Build the AdapterContext object passed to adapter.adapt(). */\nfunction buildAdapterContext(\n resolvedViteConfig: ResolvedConfig,\n routeFiles: string[],\n config: AihuConfig | undefined,\n): AdapterContext {\n const outDir = resolvePath(resolvedViteConfig.root, resolvedViteConfig.build.outDir)\n const root = resolvedViteConfig.root\n const pagesDir = config?.dir?.pages ?? 'pages'\n\n const routes: RouteDefinition[] = routeFiles.map((f) => fileToRouteDefinition(f, root, pagesDir))\n\n return {\n outDir,\n root,\n routes,\n config: config ?? {},\n\n async emitFile(path: string, content: string): Promise<void> {\n const abs = resolvePath(outDir, path)\n await mkdir(dirname(abs), { recursive: true })\n await fsWriteFile(abs, content, 'utf8')\n },\n\n async copy(src: string, dest: string): Promise<void> {\n await mkdir(dirname(dest), { recursive: true })\n await cp(src, dest, { recursive: true, force: true })\n },\n\n async writeFile(absolutePath: string, content: string): Promise<void> {\n await mkdir(dirname(absolutePath), { recursive: true })\n await fsWriteFile(absolutePath, content, 'utf8')\n },\n\n createHandlerSource(opts?: CreateHandlerSourceOptions): string {\n const routesSpec = opts?.routesSpecifier ?? './routes-manifest.js'\n const serverSpec = opts?.serverSpecifier ?? '@aihu/server'\n return [\n `// AUTO-GENERATED — do not edit`,\n `import { createRequestRouter } from '${serverSpec}'`,\n `import routes from '${routesSpec}'`,\n `const _manifest = { routes }`,\n `const _handler = createRequestRouter(_manifest)`,\n `export { _handler as handler }`,\n ].join('\\n')\n },\n }\n}\n\n/**\n * viteAihuPlugin() — composed Vite plugin for aihu SPA projects.\n *\n * Returns Plugin[] composing:\n * [0] aihuCompilerPlugin (enforce:'pre') — transforms .aihu SFCs\n * [1] viteRouterIntegration — serves virtual:aihu-routes + virtual:aihu-layouts\n * [2] aihu-agent-readiness (opt-in) or no-op\n * [3] aihu-head (injects config.app.head into index.html <head>)\n * [4..n] user plugins from config.plugins\n * [n+1] aihu-vite-passthrough (merges config.vite into Vite's resolved config)\n * [n+2] aihu-adapter (adapter.adapt() on closeBundle, build mode only)\n *\n * @example\n * // vite.config.ts\n * import { defineConfig } from 'vite'\n * import { viteAihuPlugin } from '@aihu/app'\n * export default defineConfig({ plugins: [viteAihuPlugin()] })\n *\n * @example\n * // With adapter\n * import { cloudflare } from '@aihu/adapter-cloudflare'\n * export default defineConfig({\n * plugins: [viteAihuPlugin({\n * dir: { pages: 'src/pages' },\n * adapter: cloudflare({ name: 'my-worker' }),\n * })]\n * })\n */\nexport function viteAihuPlugin(config?: AihuConfig): Plugin[] {\n const routerOpts = {\n pagesDir: config?.dir?.pages ?? 'pages',\n layoutsDir: config?.dir?.layouts ?? 'src/layouts',\n }\n\n // Agent readiness: opt-in only. No safe default for `name`.\n let agentPlugin: Plugin\n const ar = config?.agentReadiness\n if (ar) {\n // Dynamic import to avoid pulling @aihu-plugin/agent-readiness into the bundle\n // when it is not configured. The `require` below is evaluated at runtime\n // in Node.js (vite.config.ts execution context), not in the browser.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { viteAgentReadinessIntegration } =\n require('@aihu-plugin/agent-readiness') as typeof import('@aihu-plugin/agent-readiness')\n agentPlugin = viteAgentReadinessIntegration(ar) as unknown as Plugin\n } else {\n // Stable no-op so plugin-inspector shows a meaningful entry\n agentPlugin = { name: 'aihu-agent-readiness-disabled' }\n }\n\n // Head injection — applies config.app.head into the built index.html <head>.\n // Without this hook the configured global head (title/charset/viewport/meta)\n // is silently dropped from SPA/static output, hurting SEO and non-JS agents.\n const headPlugin: Plugin = {\n name: 'aihu-head',\n transformIndexHtml: {\n // Run after Vite's core HTML processing so our config wins over the\n // scaffold defaults present in the source index.html.\n order: 'post',\n handler(html: string): string {\n return applyHeadConfig(html, config?.app?.head)\n },\n },\n }\n\n // Vite config passthrough — deep-merged by Vite via the config() hook return value.\n const passthroughPlugin: Plugin = {\n name: 'aihu-vite-passthrough',\n config() {\n return (config?.vite ?? {}) as import('vite').UserConfig\n },\n }\n\n // Adapter sentinel — calls adapter.adapt() after build completes.\n // Registered unconditionally; short-circuits immediately if no adapter is set.\n let resolvedViteConfig: ResolvedConfig | null = null\n\n const adapterPlugin: Plugin = {\n name: 'aihu-adapter',\n apply: 'build',\n configResolved(rc) {\n resolvedViteConfig = rc\n },\n async closeBundle() {\n const adapter = config?.adapter\n if (!adapter || !resolvedViteConfig) return\n\n const pagesDir = config?.dir?.pages ?? 'pages'\n const { routes: routeFiles } = scanPages(resolvedViteConfig.root, pagesDir)\n const context = buildAdapterContext(resolvedViteConfig, routeFiles, config)\n\n try {\n await adapter.adapt(context)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n this.error(`[@aihu/app] Adapter '${adapter.name}' failed: ${msg}`)\n }\n },\n }\n\n // SSG prerender — active only when `output: 'static'`. Runs after Vite writes\n // the SPA build and before the adapter (so an adapter, if present, sees the\n // per-route HTML). Prerenders every static route to a content-ful\n // `<pattern>/index.html` that hydrates into the SPA. `output: 'spa'` is a\n // no-op here, preserving the existing empty-shell behavior.\n let ssgResolvedConfig: ResolvedConfig | null = null\n const ssgPlugin: Plugin = {\n name: 'aihu-ssg',\n apply: 'build',\n configResolved(rc) {\n ssgResolvedConfig = rc\n },\n // Run before the adapter's closeBundle (plugin order in the array is honored\n // for sequential closeBundle hooks).\n async closeBundle() {\n if (config?.output !== 'static' || !ssgResolvedConfig) return\n try {\n await prerenderClose(ssgResolvedConfig, config, (msg) => this.warn(msg))\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n this.error(`[@aihu/app] static (SSG) prerender failed: ${msg}`)\n }\n },\n }\n\n return [\n // SPA mode: route components are top-level mounts that frequently use\n // lifecycle hooks (onMount/onCleanup) and rely on the runtime/signals\n // owner context regardless of whether they call signal() directly. The\n // static-island optimization is unsafe to apply silently here — it strips\n // defineComponent and breaks `no owner` for any module touching lifecycle.\n // It also saves ~0 B in practice because the runtime already ships in the\n // main bundle. Default islands off; opt back in via the compiler plugin\n // directly if you genuinely have an MPA-style mixed-island layout.\n // `css.shadowMode` (when set) forwards to the compiler's per-plugin\n // shadowMode injection — required for consumers using `@aihu/css-engine`\n // utility classes (or any cascade-dependent CSS framework) which need\n // `'none'` so styles aren't trapped in shadow roots.\n aihuCompilerPlugin({\n islands: false,\n // Compile layouts (under the same dir the router scans) in layout mode:\n // namespaced tag + passive <$outlet> marker the client renderer fills.\n layoutsDir: routerOpts.layoutsDir,\n ...(config?.css?.shadowMode != null ? { shadowMode: config.css.shadowMode } : {}),\n }) as unknown as Plugin,\n viteRouterIntegration(routerOpts) as unknown as Plugin,\n agentPlugin,\n headPlugin,\n ...((config?.plugins ?? []) as Plugin[]),\n passthroughPlugin,\n ssgPlugin,\n adapterPlugin,\n ]\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aihu/app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@aihu/arbor": "1.0.0",
|
|
32
|
-
"@aihu/router": "0.
|
|
32
|
+
"@aihu/router": "0.2.0",
|
|
33
33
|
"@aihu/runtime": "1.0.0",
|
|
34
34
|
"@aihu/server": "0.2.0",
|
|
35
35
|
"@aihu/signals": "0.2.0",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@aihu-plugin/agent-readiness": "2.0.3",
|
|
40
|
-
"@aihu/compiler": "0.
|
|
40
|
+
"@aihu/compiler": "0.7.0"
|
|
41
41
|
},
|
|
42
42
|
"description": "Top-level app integration — wires runtime, router, and adapters into a Vite app.",
|
|
43
43
|
"repository": {
|