@aihu/app 0.1.9 → 0.2.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 +9 -7
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
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@0.
|
|
24
|
+
<sub><i>Auto-generated against `@aihu/app@0.2.0`.</i></sub>
|
|
25
25
|
|
|
26
26
|
<!-- END_AUTOGEN: install -->
|
|
27
27
|
|
|
@@ -32,12 +32,13 @@ bun add @aihu/app
|
|
|
32
32
|
|
|
33
33
|
| | |
|
|
34
34
|
|---|---|
|
|
35
|
-
| **Version** | `0.
|
|
35
|
+
| **Version** | `0.2.0` |
|
|
36
36
|
| **Tier** | B — Meta-framework — top-level integration of runtime, router, adapter |
|
|
37
|
+
| **Bundle size** | 1.38 kB (gz) — limit 1500 B |
|
|
37
38
|
| **Published files** | 3 entries |
|
|
38
39
|
| **License** | MIT |
|
|
39
40
|
|
|
40
|
-
<sub><i>Auto-generated against `@aihu/app@0.
|
|
41
|
+
<sub><i>Auto-generated against `@aihu/app@0.2.0`.</i></sub>
|
|
41
42
|
|
|
42
43
|
<!-- END_AUTOGEN: stats -->
|
|
43
44
|
|
|
@@ -51,7 +52,7 @@ bun add @aihu/app
|
|
|
51
52
|
| `.` | `./dist/index.js` | `—` |
|
|
52
53
|
| `./client` | `./dist/client.js` | `—` |
|
|
53
54
|
|
|
54
|
-
<sub><i>Auto-generated against `@aihu/app@0.
|
|
55
|
+
<sub><i>Auto-generated against `@aihu/app@0.2.0`.</i></sub>
|
|
55
56
|
|
|
56
57
|
<!-- END_AUTOGEN: exports -->
|
|
57
58
|
|
|
@@ -65,10 +66,11 @@ bun add @aihu/app
|
|
|
65
66
|
- `@aihu/arbor` — `workspace:*`
|
|
66
67
|
- `@aihu/router` — `workspace:*`
|
|
67
68
|
- `@aihu/runtime` — `workspace:*`
|
|
69
|
+
- `@aihu/server` — `workspace:*`
|
|
68
70
|
- `@aihu/signals` — `workspace:*`
|
|
69
71
|
- `vite` — `>=5.0.0`
|
|
70
72
|
|
|
71
|
-
<sub><i>Auto-generated against `@aihu/app@0.
|
|
73
|
+
<sub><i>Auto-generated against `@aihu/app@0.2.0`.</i></sub>
|
|
72
74
|
|
|
73
75
|
<!-- END_AUTOGEN: deps -->
|
|
74
76
|
|
|
@@ -82,7 +84,7 @@ bun add @aihu/app
|
|
|
82
84
|
- [@aihu/adapter-cloudflare](../adapter-cloudflare)
|
|
83
85
|
- [Aihu framework root](../../README.md)
|
|
84
86
|
|
|
85
|
-
<sub><i>Auto-generated against `@aihu/app@0.
|
|
87
|
+
<sub><i>Auto-generated against `@aihu/app@0.2.0`.</i></sub>
|
|
86
88
|
|
|
87
89
|
<!-- END_AUTOGEN: see-also -->
|
|
88
90
|
|
|
@@ -93,6 +95,6 @@ bun add @aihu/app
|
|
|
93
95
|
|
|
94
96
|
MIT — see [LICENSE](../../LICENSE).
|
|
95
97
|
|
|
96
|
-
<sub><i>Auto-generated against `@aihu/app@0.
|
|
98
|
+
<sub><i>Auto-generated against `@aihu/app@0.2.0`.</i></sub>
|
|
97
99
|
|
|
98
100
|
<!-- END_AUTOGEN: license -->
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HeadConfig } from "@aihu/server/head-lowering";
|
|
2
|
+
|
|
1
3
|
//#region src/client.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Rendering mode passed from the server config into the client bootstrap.
|
|
@@ -32,6 +34,23 @@ interface AppConfig {
|
|
|
32
34
|
rendering?: {
|
|
33
35
|
mode?: AppRenderingMode;
|
|
34
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Site-level config. `site.url` is the absolute base URL used to resolve
|
|
39
|
+
* relative per-route `canonical` / `og:*` / `twitter:*` values into absolute
|
|
40
|
+
* URLs as the head is applied on client navigation (mirrors the SSG path's
|
|
41
|
+
* `AihuConfig.site.url`). When absent, relative values are emitted unchanged.
|
|
42
|
+
*/
|
|
43
|
+
site?: {
|
|
44
|
+
url?: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Global `<head>` defaults (typically `aihu.config.ts`'s `app.head`). On every
|
|
48
|
+
* navigation these defaults are folded under the active route's head
|
|
49
|
+
* (`routeHeadToSsrHead`'s `globalHead`) and re-applied — so a route that omits
|
|
50
|
+
* a field falls back to the global default, and global tags persist across
|
|
51
|
+
* navigations while route-only tags are cleaned up.
|
|
52
|
+
*/
|
|
53
|
+
head?: HeadConfig;
|
|
35
54
|
}
|
|
36
55
|
/**
|
|
37
56
|
* Bootstrap the aihu SPA.
|
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":";;;;;AAmBA;;;KAAY,gBAAA;;UAGK,SAAA;EAAS;EAExB,QAAA;EASU;;;;;;;;EAAV,OAAA,GAAU,MAAA;EAYI;;;;;;;;AA+BhB;;;EA/BE,SAAA;IAAc,IAAA,GAAO,gBAAA;EAAA;;;;;;;EAOrB,IAAA;IAAS,GAAA;EAAA;;;;;;;;EAQT,IAAA,GAAO,UAAA;AAAA;;;;;;;;;;;;;;iBAgBO,SAAA,CAAU,MAAA,GAAS,SAAA"}
|
package/dist/client.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"virtual:aihu-routes";import{hydrate as t,mount as n}from"@aihu/arbor";import{createRouter as r}from"@aihu/router";import{_setHydrate as i,_setMount as a,_setSignal as o}from"@aihu/runtime";import{
|
|
1
|
+
import e from"virtual:aihu-routes";import{hydrate as t,mount as n}from"@aihu/arbor";import{createRouter as r}from"@aihu/router";import{_setHydrate as i,_setMount as a,_setSignal as o}from"@aihu/runtime";import{routeHeadToSsrHead as s}from"@aihu/server/head-lowering";import{signal as c}from"@aihu/signals";const l=`data-aihu-head`;function u(e){return typeof e.name==`string`?{attr:`name`,value:e.name}:typeof e.property==`string`?{attr:`property`,value:e.property}:null}function d(e){let t={};for(let[n,r]of Object.entries(e))r!==void 0&&(t[n]=String(r));return t}function f(e){return{title:e.title,metas:(e.meta??[]).map(e=>d(e)),links:(e.links??[]).map(e=>d(e)),scripts:(e.scripts??[]).map(e=>({type:e.type,content:e.content}))}}function p(e=document){let t=e.head;if(t)for(let e of Array.from(t.querySelectorAll(`[${l}]`)))e.remove()}function m(e,t=document){let n=t.head;if(!n)return;p(t);let{title:r,metas:i,links:a,scripts:o}=f(e);r!==void 0&&(t.title=r);for(let e of i){let r=u(e),i=null;r&&(i=n.querySelector(`meta[${r.attr}="${g(r.value)}"]`)),i?i.setAttribute(l,``):(i=t.createElement(`meta`),i.setAttribute(l,``),n.appendChild(i)),h(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(l,``):(i=t.createElement(`link`),i.setAttribute(l,``),n.appendChild(i)),h(i,e)}for(let e of o){let r=n.querySelector(`script[type="${g(e.type)}"]`);r?r.setAttribute(l,``):(r=t.createElement(`script`),r.type=e.type,r.setAttribute(l,``),n.appendChild(r)),r.textContent=e.content}}function h(e,t){for(let[n,r]of Object.entries(t))e.setAttribute(n,r)}function g(e){let t=globalThis;return t.CSS&&typeof t.CSS.escape==`function`?t.CSS.escape(e):e.replace(/["\\]/g,`\\$&`)}function _(l){l?.provide&&Object.assign(globalThis,l.provide),a(n),o(c),l?.rendering?.mode!==`spa`&&i(t);let u=l?.outletId??`outlet`,d=document.getElementById(u);if(!d)throw Error(`@aihu/app: no element with id="${u}" found. Add <div id="${u}"></div> to your index.html`);let f=d,h=r(e),g=l?.site?.url,_=l?.head;function v(e){if(e===void 0&&_===void 0){p();return}m(s(e,{...g===void 0?{}:{siteUrl:g},..._===void 0?{}:{globalHead:_}}))}async function y(t){if(!t){let t=e.find(e=>e.pattern===`*`||e.name===`not-found`);if(t){await t.module();let e=t.name;if(e?.includes(`-`)){v(t.head),f.replaceChildren(document.createElement(e));return}}v(void 0);let n=document.createElement(`p`);n.style.cssText=`font-family:system-ui;padding:2rem;color:#888`,n.textContent=`404 — page not found`,f.replaceChildren(n);return}v(t.route.head),await t.route.module();let n=t.route.name;if(!n?.includes(`-`))return;let r=document.createElement(n);if(t.params)for(let[e,n]of Object.entries(t.params))r.setAttribute(e,String(n));f.replaceChildren(r)}y(h.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),y(h.match(location.pathname)))}),window.addEventListener(`popstate`,()=>{y(h.match(location.pathname))})}export{_ 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/client.ts"],"sourcesContent":["import routes from 'virtual:aihu-routes'\nimport { hydrate, mount } from '@aihu/arbor'\nimport type { MatchResult, RouteDefinition } from '@aihu/router'\nimport { createRouter } from '@aihu/router'\nimport { _setHydrate, _setMount, _setSignal } from '@aihu/runtime'\nimport { signal } from '@aihu/signals'\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\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 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 outlet.replaceChildren(document.createElement(tag))\n return\n }\n }\n // Inline fallback 404\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 // 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":"kPAsDA,SAAgB,EAAU,EAA0B,CAG9C,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,CAEnC,eAAe,EAAO,EAA0C,CAC9D,GAAI,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,EAAO,gBAAgB,SAAS,cAAc,EAAI,CAAC,CACnD,QAIJ,IAAM,EAAI,SAAS,cAAc,IAAI,CACrC,EAAE,MAAM,QAAU,gDAClB,EAAE,YAAc,uBAChB,EAAO,gBAAgB,EAAE,CACzB,OAIF,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,CAIrC,EAAO,gBAAgB,EAAG,CAI5B,EAAO,EAAO,MAAM,SAAS,SAAS,CAAC,CAGvC,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,EAAO,EAAO,MAAM,SAAS,SAAS,CAAC,GACvC,CAGF,OAAO,iBAAiB,eAAkB,CACxC,EAAO,EAAO,MAAM,SAAS,SAAS,CAAC,EACvC"}
|
|
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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,31 @@ import { Plugin, UserConfig } from "vite";
|
|
|
3
3
|
import { RouteDefinition } from "@aihu/router";
|
|
4
4
|
|
|
5
5
|
//#region src/config.d.ts
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Build output mode.
|
|
8
|
+
*
|
|
9
|
+
* - `'spa'` (default): a single empty-shell `index.html` that boots the client
|
|
10
|
+
* SPA. No per-route HTML, no prerendered content.
|
|
11
|
+
* - `'static'` (SSG): prerenders every static route to a content-ful
|
|
12
|
+
* `<pattern>/index.html` with a per-page `<head>`, then hydrates into the SPA
|
|
13
|
+
* on load (progressive enhancement). Ideal for content sites on static hosts
|
|
14
|
+
* (e.g. Cloudflare Pages) — crawlers and non-JS agents see real content.
|
|
15
|
+
*
|
|
16
|
+
* Other rendering modes (`ssr`, `hybrid`) are tracked separately under
|
|
17
|
+
* @aihu/server's RenderingMode and are not part of the app build OutputMode.
|
|
18
|
+
*/
|
|
19
|
+
type OutputMode = 'spa' | 'static';
|
|
20
|
+
/** Site-level configuration. */
|
|
21
|
+
interface SiteConfig {
|
|
22
|
+
/**
|
|
23
|
+
* Absolute base URL of the deployed site (e.g. `https://example.com`).
|
|
24
|
+
* Used by the `'static'` (SSG) output mode to resolve relative per-route
|
|
25
|
+
* `canonical` / `og:*` / `twitter:*` URLs into absolute URLs (passed as
|
|
26
|
+
* `siteUrl` to @aihu/server's `routeHeadToSsrHead`). When absent, relative
|
|
27
|
+
* URLs are emitted unchanged.
|
|
28
|
+
*/
|
|
29
|
+
readonly url?: string;
|
|
30
|
+
}
|
|
8
31
|
interface DirConfig {
|
|
9
32
|
/** Directory to scan for page routes. Default: 'pages' */
|
|
10
33
|
readonly pages?: string;
|
|
@@ -52,10 +75,15 @@ interface AihuConfig {
|
|
|
52
75
|
/** Directory layout overrides. */
|
|
53
76
|
readonly dir?: DirConfig;
|
|
54
77
|
/**
|
|
55
|
-
* Output mode.
|
|
78
|
+
* Output mode. Supports `'spa'` (default) and `'static'` (SSG prerender).
|
|
56
79
|
* defineConfig throws AihuConfigError for any other value.
|
|
57
80
|
*/
|
|
58
81
|
readonly output?: OutputMode;
|
|
82
|
+
/**
|
|
83
|
+
* Site-level configuration. `site.url` is the absolute base URL used by the
|
|
84
|
+
* `'static'` output mode to resolve relative canonical/OG/Twitter URLs.
|
|
85
|
+
*/
|
|
86
|
+
readonly site?: SiteConfig;
|
|
59
87
|
/**
|
|
60
88
|
* Aihu plugins. Order is preserved.
|
|
61
89
|
* Appended after the three framework plugins (compiler, router, agent-readiness).
|
|
@@ -204,7 +232,8 @@ interface AihuAdapter {
|
|
|
204
232
|
* [0] aihuCompilerPlugin (enforce:'pre') — transforms .aihu SFCs
|
|
205
233
|
* [1] viteRouterIntegration — serves virtual:aihu-routes + virtual:aihu-layouts
|
|
206
234
|
* [2] aihu-agent-readiness (opt-in) or no-op
|
|
207
|
-
* [3
|
|
235
|
+
* [3] aihu-head (injects config.app.head into index.html <head>)
|
|
236
|
+
* [4..n] user plugins from config.plugins
|
|
208
237
|
* [n+1] aihu-vite-passthrough (merges config.vite into Vite's resolved config)
|
|
209
238
|
* [n+2] aihu-adapter (adapter.adapt() on closeBundle, build mode only)
|
|
210
239
|
*
|
|
@@ -226,5 +255,5 @@ interface AihuAdapter {
|
|
|
226
255
|
*/
|
|
227
256
|
declare function viteAihuPlugin(config?: AihuConfig): Plugin[];
|
|
228
257
|
//#endregion
|
|
229
|
-
export { type AdapterContext, type AgentReadinessConfig, type AihuAdapter, type AihuConfig, AihuConfigError, type AihuPlugin, type AppHeadConfig, type CreateHandlerSourceOptions, type DirConfig, type HeadConfig, type OutputMode, type RuntimeConfig, type VitePassthrough, defineConfig, viteAihuPlugin };
|
|
258
|
+
export { type AdapterContext, type AgentReadinessConfig, type AihuAdapter, type AihuConfig, AihuConfigError, type AihuPlugin, type AppHeadConfig, type CreateHandlerSourceOptions, type DirConfig, type HeadConfig, type OutputMode, type RouterConfig, type RuntimeConfig, type SiteConfig, type VitePassthrough, defineConfig, viteAihuPlugin };
|
|
230
259
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/config.ts","../src/adapter.ts","../src/vite-plugin.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/config.ts","../src/adapter.ts","../src/vite-plugin.ts"],"mappings":";;;;;;;;AAgBA;;;;;AAGA;;;;;KAHY,UAAA;;UAGK,UAAA;EAWS;;;;;;AAU1B;EAV0B,SAHf,GAAA;AAAA;AAAA,UAGM,SAAA;EAWN;EAAA,SATA,KAAA;EAWA;EAAA,SATA,OAAA;EASgB;EAAA,SAPhB,MAAA;AAAA;;UAIM,aAAA;EAAA,SACN,MAAA,GAAS,MAAA;EAMT;EAAA,SAJA,OAAA,GAAU,MAAA;AAAA;AAAA,UAGJ,UAAA;EAAA,SACN,KAAA;EAKqB;EAAA,SAHrB,OAAA;EAG2B;EAAA,SAD3B,QAAA;EAAA,SACA,IAAA,GAAO,aAAA,CAAc,MAAA;AAAA;AAAA,UAGf,aAAA;EAAA,SACN,IAAA,GAAO,UAAA;AAAA;;KAIN,eAAA,GAAkB,IAAA,CAAK,UAAA;;KAGvB,UAAA,GAAa,MAAA;AAAzB;AAAA,KAGY,oBAAA,GAAoB,+BAAA,CAA0C,oBAAA;;UAGzD,YAAA;EANc;AAG/B;;;;;AAGA;;EAN+B,SAepB,eAAA;AAAA;AAAA,UAGM,UAAA;EAAA;EAAA,SAEN,GAAA,GAAM,SAAA;;;;;WAKN,MAAA,GAAS,UAAA;EAUC;;;;EAAA,SALV,IAAA,GAAO,UAAA;EA0BU;;;;EAAA,SArBjB,OAAA,GAAU,aAAA,CAAc,UAAA;EAfxB;EAAA,SAiBA,aAAA,GAAgB,aAAA;EAZhB;;;;;;;;EAAA,SAqBA,OAAA,GAAU,MAAA;EAAV;EAAA,SAEA,GAAA,GAAM,aAAA;EAAN;EAAA,SAEA,IAAA,GAAO,eAAA;EAAP;;;;;EAAA,SAMA,cAAA,GAAiB,oBAAA;EAWjB;;;;AAIX;EAJW,SALA,OAAA,GAAU,WAAA;;;;;WAKV,MAAA,GAAS,YAAA;AAAA;;cAIP,eAAA,SAAwB,KAAA;EAAA,SAGxB,IAAA;EAAA,SACA,KAAA;cAFT,OAAA,UACS,IAAA,2DACA,KAAA;AAAA;;;;;;;;;;;;ACvIb;;iBD2JgB,YAAA,CAAa,MAAA,EAAQ,UAAA,GAAa,UAAA;;;;UC3JjC,0BAAA;;;ADajB;;ECRE,eAAA;EDQoB;;AAGtB;;;ECLE,eAAA;AAAA;ADgBF;;;;AAAA,UCTiB,cAAA;EDaN;EAAA,SCXA,MAAA;EDaM;EAAA,SCXN,IAAA;EDeM;;;;EAAA,SCVN,MAAA,EAAQ,aAAA,CAAc,eAAA;EDWb;EAAA,SCTT,MAAA,EAFqB,UAAA;EDaX;;;AAGrB;ECRE,QAAA,CAAS,IAAA,UAAc,OAAA,WAAkB,OAAA;;;;EAKzC,IAAA,CAAK,GAAA,UAAa,IAAA,WAAe,OAAA;EDQxB;;;ECHT,SAAA,CAAU,YAAA,UAAsB,OAAA,WAAkB,OAAA;EDId;;AAGtC;;;;;ECEE,mBAAA,CAAoB,OAAA,GAAU,0BAAA;AAAA;;;;ADMhC;;;;;AAGA;;;;;UCOiB,WAAA;EDJY;;;;EAAA,SCSlB,IAAA;EDGgB;;;;;;ECKzB,KAAA,CAAM,OAAA,EAAS,cAAA,GAAiB,OAAA;AAAA;;;;;;ADtElC;;;;;AAGA;;;;;AAWA;;;;;;;;;AAUA;;;;;;iBEoFgB,cAAA,CAAe,MAAA,GAAS,UAAA,GAAa,MAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import{cp as e,mkdir as t,writeFile as
|
|
2
|
-
`)}}}function
|
|
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`);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
|
+
`);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
|
+
`);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}),u(t),n,i,...e?.plugins??[],a,f,c]}export{m as AihuConfigError,h as defineConfig,R as viteAihuPlugin};
|
|
3
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["resolvePath","fsWriteFile"],"sources":["../src/config.ts","../src/vite-plugin.ts"],"sourcesContent":["import type { Plugin, UserConfig } from 'vite'\nimport type { AihuAdapter } from './adapter.ts'\n\n/** V0: SPA output only. More modes (ssr, static, hybrid) in V1+. */\nexport type OutputMode = 'spa'\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/** 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. V0 supports 'spa' only.\n * defineConfig throws AihuConfigError for any other value.\n */\n readonly output?: OutputMode\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\n/** Thrown by defineConfig when configuration validation fails. */\nexport class AihuConfigError extends Error {\n constructor(\n message: string,\n readonly code: 'INVALID_OUTPUT_MODE' | 'INVALID_DIR' | 'UNKNOWN_FIELD',\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') {\n throw new AihuConfigError(\n `output mode '${config.output}' is not supported in V0 (only 'spa')`,\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 return config\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'\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..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 // 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 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 aihuCompilerPlugin({ islands: false }) as unknown as Plugin,\n viteRouterIntegration(routerOpts) as unknown as Plugin,\n agentPlugin,\n ...((config?.plugins ?? []) as Plugin[]),\n passthroughPlugin,\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/** 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\n/** Thrown by defineConfig when configuration validation fails. */\nexport class AihuConfigError extends Error {\n constructor(\n message: string,\n readonly code: 'INVALID_OUTPUT_MODE' | 'INVALID_DIR' | 'UNKNOWN_FIELD',\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 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 aihuCompilerPlugin({ islands: false }) 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":"uuBAsIa,EAAb,cAAqC,KAAM,CAG9B,KACA,MAHX,YACE,EACA,EACA,EACA,CACA,MAAM,EAAQ,CAHL,KAAA,KAAA,EACA,KAAA,MAAA,EAGT,KAAK,KAAO,oBAiBhB,SAAgB,EAAa,EAAgC,CAC3D,GAAI,EAAO,QAAU,EAAO,SAAW,OAAS,EAAO,SAAW,SAChE,MAAM,IAAI,EACR,gBAAgB,EAAO,OAAO,4CAC9B,sBACA,SACD,CAEH,GAAI,EAAO,KAAK,QAAU,IAAA,IAAa,OAAO,EAAO,IAAI,OAAU,SACjE,MAAM,IAAI,EAAgB,6BAA8B,cAAe,YAAY,CAErF,GAAI,EAAO,KAAK,UAAY,IAAA,IAAa,OAAO,EAAO,IAAI,SAAY,SACrE,MAAM,IAAI,EAAgB,+BAAgC,cAAe,cAAc,CAEzF,OAAO,ECzKT,SAASA,EAAW,EAAuB,CACzC,OAAO,EACJ,QAAQ,KAAM,QAAQ,CACtB,QAAQ,KAAM,SAAS,CACvB,QAAQ,KAAM,OAAO,CACrB,QAAQ,KAAM,OAAO,CAI1B,SAASC,EAAW,EAAuB,CACzC,OAAO,EAAM,QAAQ,KAAM,QAAQ,CAAC,QAAQ,KAAM,OAAO,CAAC,QAAQ,KAAM,OAAO,CAiBjF,SAAgB,EAAgB,EAAc,EAAsC,CAClF,GAAI,CAAC,EAAM,OAAO,EAElB,IAAI,EAAM,EACJ,EAAmB,EAAE,CAG3B,GAAI,EAAK,QAAU,IAAA,GAAW,CAC5B,IAAM,EAAM,UAAUA,EAAW,EAAK,MAAM,CAAC,UACzC,iCAAiC,KAAK,EAAI,CAC5C,EAAM,EAAI,QAAQ,iCAAkC,EAAI,CAExD,EAAO,KAAK,EAAI,CAKpB,GAAI,EAAK,UAAY,IAAA,GAAW,CAC9B,IAAM,EAAM,kBAAkBD,EAAW,EAAK,QAAQ,CAAC,IACnD,mDAAmD,KAAK,EAAI,CAC9D,EAAM,EAAI,QAAQ,mDAAoD,EAAI,CAE1E,EAAO,KAAK,EAAI,CAKpB,GAAI,EAAK,WAAa,IAAA,GAAW,CAC/B,IAAM,EAAM,kCAAkCA,EAAW,EAAK,SAAS,CAAC,IAClE,EAAa,kDACf,EAAW,KAAK,EAAI,CACtB,EAAM,EAAI,QAAQ,EAAY,EAAI,CAElC,EAAO,KAAK,EAAI,CAMpB,IAAK,IAAM,KAAS,EAAK,MAAQ,EAAE,CAAE,CACnC,IAAM,EAAM,EAAM,OAAS,IAAA,GAAqB,EAAM,WAAa,IAAA,GAAyB,KAAb,WAAxC,OACjC,EAAS,EAAM,EAAM,GAAO,IAAA,GAK5B,EAAM,SAHE,OAAO,QAAQ,EAAM,CAChC,KAAK,CAAC,EAAG,KAAO,GAAG,EAAE,IAAIA,EAAW,OAAO,EAAE,CAAC,CAAC,GAAG,CAClD,KAAK,IACkB,CAAC,GAE3B,GAAI,GAAO,IAAW,IAAA,GAAW,CAC/B,IAAM,EAAS,OACb,iBAAiB,EAAI,eAAe,EAAO,QAAQ,sBAAuB,OAAO,CAAC,YAClF,IACD,CACD,GAAI,EAAG,KAAK,EAAI,CAAE,CAChB,EAAM,EAAI,QAAQ,EAAI,EAAI,CAC1B,UAGJ,EAAO,KAAK,EAAI,CAGlB,GAAI,EAAO,SAAW,EAAG,OAAO,EAEhC,IAAM,EAAQ,EAAO,KAAK;MAAS,CAKnC,MAJI,YAAY,KAAK,EAAI,CAChB,EAAI,QAAQ,YAAa,OAAO,EAAM,aAAa,CAGrD,GAAG,EAAI,IAAI,IC3DpB,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,CAOH,SAAS,EAAW,EAAuB,CACzC,OAAO,EAAM,QAAQ,KAAM,QAAQ,CAAC,QAAQ,KAAM,SAAS,CAG7D,SAAS,EAAW,EAAuB,CACzC,OAAO,EAAM,QAAQ,KAAM,QAAQ,CAAC,QAAQ,KAAM,OAAO,CAAC,QAAQ,KAAM,OAAO,CAGjF,SAAS,EAAY,EAAuC,CAC1D,OAAO,OAAO,QAAQ,EAAM,CACzB,KAAK,CAAC,EAAG,KAAO,GAAG,EAAE,IAAI,EAAW,EAAE,CAAC,GAAG,CAC1C,KAAK,IAAI,CAcd,SAAgB,EAAgB,EAAc,EAA0B,CACtE,IAAI,EAAM,EACJ,EAAmB,EAAE,CACrB,CAAE,QAAO,QAAO,QAAO,WAAY,EAAc,EAAK,CAE5D,GAAI,IAAU,IAAA,GAAW,CACvB,IAAM,EAAM,UAAU,EAAW,EAAM,CAAC,UACpC,iCAAiC,KAAK,EAAI,CAC5C,EAAM,EAAI,QAAQ,iCAAkC,EAAI,CAExD,EAAO,KAAK,EAAI,CAIpB,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAU,SAAS,EAAY,EAAK,CAAC,GACrC,EAAM,EAAQ,EAAK,CACzB,GAAI,EAAK,CACP,IAAM,EAAU,EAAI,MAAM,QAAQ,sBAAuB,OAAO,CAC1D,EAAS,OAAO,iBAAiB,EAAI,KAAK,IAAI,EAAQ,SAAU,IAAI,CAC1E,GAAI,EAAG,KAAK,EAAI,CAAE,CAChB,EAAM,EAAI,QAAQ,EAAI,EAAQ,CAC9B,UAGJ,EAAO,KAAK,EAAQ,CAGtB,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAU,SAAS,EAAY,EAAK,CAAC,GAC3C,IAAK,EAAK,KAAO,IAAI,aAAa,GAAK,YAAa,CAClD,IAAM,EAAK,sCACX,GAAI,EAAG,KAAK,EAAI,CAAE,CAChB,EAAM,EAAI,QAAQ,EAAI,EAAQ,CAC9B,UAGJ,EAAO,KAAK,EAAQ,CAGtB,IAAK,IAAM,KAAU,EAAS,CAG5B,IAAM,EAAO,EAAO,QAAQ,QAAQ,OAAQ,OAAO,CACnD,EAAO,KAAK,iBAAiB,EAAW,EAAO,KAAK,CAAC,IAAI,EAAK,YAAW,CAG3E,GAAI,EAAO,SAAW,EAAG,OAAO,EAChC,IAAM,EAAQ,EAAO,KAAK;MAAS,CAInC,MAHI,YAAY,KAAK,EAAI,CAChB,EAAI,QAAQ,YAAa,OAAO,EAAM,aAAa,CAErD,GAAG,EAAI,IAAI,IC3EpB,SAAS,EAAe,EAA6B,CACnD,IAAM,EAAQ,EACX,QAAQ,MAAO,IAAI,CACnB,QAAQ,YAAa,GAAG,CACxB,MAAM,IAAI,CACV,OAAO,QAAQ,CACf,IACE,GACC,EAAE,WAAW,OAAO,EAAI,EAAE,SAAS,IAAI,CACnC,CAAE,KAAM,WAAY,CACpB,EAAE,WAAW,IAAI,EAAI,EAAE,SAAS,IAAI,CAClC,CAAE,KAAM,QAAS,KAAM,EAAE,MAAM,EAAG,GAAG,CAAE,CACvC,EAAE,WAAW,IAAI,CACf,CAAE,KAAM,QAAS,KAAM,EAAE,MAAM,EAAE,CAAE,CACnC,CAAE,KAAM,SAAU,KAAM,EAAG,CACtC,CAEH,GAAI,EAAM,OAAS,EAAG,CACpB,IAAM,EAAO,EAAM,EAAM,OAAS,GAC9B,EAAK,OAAS,UAAY,EAAK,OAAS,SAAS,EAAM,KAAK,CAElE,OAAO,EAGT,SAAS,EAAkB,EAA8B,CAEvD,OADI,EAAK,SAAW,EAAU,IACvB,IAAI,EACR,IAAK,GAAO,EAAE,OAAS,SAAW,EAAE,KAAO,EAAE,OAAS,QAAU,IAAI,EAAE,OAAS,IAAK,CACpF,KAAK,IAAI,GAGd,SAAS,EAAa,EAAc,EAAkC,CACpE,GAAM,CAAE,UAAW,EAAU,EAAM,EAAS,CACtC,EAAWE,EAAY,EAAM,EAAS,CAAC,QAAQ,MAAO,IAAI,CAChE,OAAO,EAAO,IAAK,GAAS,CAC1B,IAAM,EAAO,EAAK,QAAQ,MAAO,IAAI,CAE/B,EAAW,EADL,EAAK,WAAW,GAAG,EAAS,GAAG,CAAG,EAAK,MAAM,EAAS,OAAS,EAAE,CAAG,EAC5C,CAGpC,MAAO,CAAE,OAAM,QAFC,EAAkB,EAEZ,CAAE,WAAU,QADlB,EAAS,KAAM,GAAM,EAAE,OAAS,SAAW,EAAE,OAAS,WAC7B,CAAE,EAC3C,CAIJ,SAAS,EAAY,EAA0B,EAAwC,CAQrF,OAPI,EAAS,SAAW,EAAU,IAO3B,IANO,EAAS,IAAK,GACtB,EAAE,OAAS,SAAiB,EAAE,KAC9B,EAAE,OAAS,QAAgB,EAAO,EAAE,OAAS,GAE1C,EAAO,MAAQ,GAER,CAAC,OAAQ,GAAM,IAAM,GAAG,CAAC,KAAK,IAAI,GAGpD,SAAS,EAAyB,EAAgD,CAShF,OAPE,GACA,OAAO,GAAU,UACjB,WAAY,GACX,EAA+B,OAExB,EAA6C,OAEhD,EAYT,SAAS,EAAc,EAAc,EAAiB,EAA0B,CAC9E,IAAM,EAAU,EAAS,QAAQ,sBAAuB,OAAO,CAEzD,EAAc,OAClB,6BAA6B,EAAQ,8BACrC,IACD,CACD,GAAI,EAAQ,KAAK,EAAK,CACpB,OAAO,EAAK,QAAQ,EAAS,KAAK,EAAQ,IAAI,CAGhD,IAAM,EAAa,OAAO,6BAA6B,EAAQ,UAAW,IAAI,CAI9E,OAHI,EAAO,KAAK,EAAK,CACZ,EAAK,QAAQ,EAAQ,KAAK,IAAU,CAEtC,EAgBT,SAAS,EACP,EAC+C,CAC/C,IAAM,EAAI,EAAI,QAKd,OAJI,OAAO,GAAM,YACb,GAAK,OAAO,GAAM,UAAY,OAAQ,EAA2B,QAAW,WACvE,EAEF,KAIT,SAAS,EAAkB,EAAyB,CAGlD,OAFI,IAAY,IAAY,aAErB,EADO,EAAQ,QAAQ,MAAO,GAAG,CAAC,QAAQ,MAAO,GACvC,CAAE,aAAa,CAoBlC,eAAsB,EAAa,EAAqD,CACtF,GAAM,CAAE,qBAAoB,SAAQ,aAAY,QAAS,EACnD,EAAO,EAAmB,KAC1B,EAASA,EAAY,EAAM,EAAmB,MAAM,OAAO,CAC3D,EAAW,GAAQ,KAAK,OAAS,QACjC,EAAU,GAAQ,MAAM,IACxB,EAAa,GAAQ,KAAK,KAG1B,EAA0B,CAAE,QAAS,EAAE,CAAE,SAAU,EAAE,CAAE,CACvD,EAAY,GAAsB,CACtC,EAAO,SAAS,KAAK,EAAI,CACzB,EAAK,EAAI,EAKL,EAAeA,EAAY,EAAQ,aAAa,CAClD,EACJ,GAAI,CACF,EAAW,MAAM,EAAS,EAAc,OAAO,MACzC,CAKN,OAJA,EACE,+CAA+C,EAAO,mEAEvD,CACM,EAGT,IAAM,EAAS,EAAa,EAAM,EAAS,CAE3C,IAAK,IAAM,KAAS,EAAQ,CAC1B,IAAI,EACJ,GAAI,CACF,EAAM,MAAM,EAAW,EAAM,KAAK,OAC3B,EAAK,CACZ,IAAM,EAAM,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAC5D,EAAS,mDAAmD,EAAM,QAAQ,IAAI,IAAM,CACpF,SAIF,IAAM,EADU,EAAiB,EAAM,KACnB,EAAE,KAEhB,EAAY,EAAiB,EAAI,CACvC,GAAI,CAAC,EAAW,CACd,EACE,oCAAoC,EAAM,QAAQ,6FAEnD,CACD,SAKF,IAAI,EACJ,GAAI,EAAM,QAAS,CACjB,GAAI,OAAO,EAAI,gBAAmB,WAAY,CAC5C,EACE,4CAA4C,EAAM,QAAQ,qFAE3D,CACD,SAGF,GAAa,MADK,EAAI,gBAAgB,EAClB,EAAE,EAAE,IAAI,EAAyB,CACjD,EAAU,SAAW,GACvB,EACE,4CAA4C,EAAM,QAAQ,2EAE3D,MAGH,EAAY,CAAC,EAAE,CAAC,CAGlB,IAAK,IAAM,KAAU,EAAW,CAC9B,IAAM,EAAe,EAAM,QAAU,EAAY,EAAM,SAAU,EAAO,CAAG,EAAM,QAE7E,EACJ,GAAI,CACF,EAAU,MAAM,EAAe,EAAU,OAClC,EAAK,CAEZ,EAAS,gDAAgD,EAAa,IAD1D,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,GACoB,CAChF,SAGF,IAAM,EAAU,EAAmB,EAAM,CACvC,GAAI,IAAY,IAAA,GAA0B,EAAE,CAAhB,CAAE,UAAS,CACvC,GAAI,IAAe,IAAA,GAA6B,EAAE,CAAnB,CAAE,aAAY,CAC9C,CAAC,CACE,EAAO,EAAgB,EAAU,EAAQ,CAC7C,EAAO,EAAc,EAAM,EAAS,SAAS,CAE7C,IAAM,EAAU,EAAkB,EAAa,CACzC,EAAUA,EAAY,EAAQ,EAAQ,CAC5C,MAAM,EAAM,EAAQ,EAAQ,CAAE,CAAE,UAAW,GAAM,CAAC,CAClD,MAAM,EAAU,EAAS,EAAM,OAAO,CACtC,EAAO,QAAQ,KAAK,EAAQ,QAAQ,MAAO,IAAI,CAAC,EAIpD,OAAO,EAST,eAAsB,EACpB,EACA,EACA,EAC0B,CAE1B,GAAM,CAAE,gBAAiB,MAAM,OAAO,QAMhC,GACH,EAAmB,SAA4D,EAAE,EAClF,OAAQ,GAAM,GAAG,OAAS,YAAc,GAAG,OAAS,eAAe,CAC/D,EAAS,MAAM,EAAa,CAChC,KAAM,EAAmB,KACzB,WAAY,GACZ,QAAS,SACT,SAAU,SACV,OAAQ,CAAE,eAAgB,GAAM,IAAK,GAAO,CACnC,UACV,CAAC,CACF,GAAI,CAGF,OAAO,MAAM,EAAa,CAAE,qBAAoB,SAAQ,gBAFb,IACxC,MAAM,EAAO,cAAc,EAAS,CAC6B,OAAM,CAAC,QACnE,CACR,MAAM,EAAO,OAAO,EC5VxB,SAAS,EAAsB,EAAkB,EAAe,EAAmC,CAMjG,IAAM,EAJM,EACT,QAAQ,MAAO,IAAI,CACnB,QAAY,OAAO,OAAO,EAAS,GAAG,CAAE,GAAG,CAC3C,QAAQ,WAAY,GACN,CAAC,MAAM,IAAI,CAAC,OAAO,QAAQ,CAExC,EAAM,OAAS,GAAK,EAAM,EAAM,OAAS,KAAO,SAAS,EAAM,KAAK,CAExE,IAAM,EAAW,EAAM,IAAK,GAC1B,EAAE,WAAW,OAAO,EAAI,EAAE,SAAS,IAAI,CACnC,CAAE,KAAM,WAAqB,CAC7B,EAAE,WAAW,IAAI,EAAI,EAAE,SAAS,IAAI,CAClC,CAAE,KAAM,QAAkB,KAAM,EAAE,MAAM,EAAG,GAAG,CAAE,CAChD,CAAE,KAAM,SAAmB,KAAM,EAAG,CAC3C,CAUD,MAAO,CACL,QARA,EAAS,SAAW,EAChB,IACA,IACA,EACG,IAAK,GAAO,EAAE,OAAS,SAAW,EAAE,KAAO,EAAE,OAAS,QAAU,IAAI,EAAE,OAAS,IAAK,CACpF,KAAK,IAAI,CAIhB,WACA,WAAc,QAAQ,QAAQ,CAAE,QAAS,KAAM,CAAC,CACjD,CAIH,SAAS,EACP,EACA,EACA,EACgB,CAChB,IAAM,EAASC,EAAY,EAAmB,KAAM,EAAmB,MAAM,OAAO,CAC9E,EAAO,EAAmB,KAC1B,EAAW,GAAQ,KAAK,OAAS,QAIvC,MAAO,CACL,SACA,OACA,OALgC,EAAW,IAAK,GAAM,EAAsB,EAAG,EAAM,EAAS,CAKxF,CACN,OAAQ,GAAU,EAAE,CAEpB,MAAM,SAAS,EAAc,EAAgC,CAC3D,IAAM,EAAMA,EAAY,EAAQ,EAAK,CACrC,MAAM,EAAM,EAAQ,EAAI,CAAE,CAAE,UAAW,GAAM,CAAC,CAC9C,MAAMC,EAAY,EAAK,EAAS,OAAO,EAGzC,MAAM,KAAK,EAAa,EAA6B,CACnD,MAAM,EAAM,EAAQ,EAAK,CAAE,CAAE,UAAW,GAAM,CAAC,CAC/C,MAAM,EAAG,EAAK,EAAM,CAAE,UAAW,GAAM,MAAO,GAAM,CAAC,EAGvD,MAAM,UAAU,EAAsB,EAAgC,CACpE,MAAM,EAAM,EAAQ,EAAa,CAAE,CAAE,UAAW,GAAM,CAAC,CACvD,MAAMA,EAAY,EAAc,EAAS,OAAO,EAGlD,oBAAoB,EAA2C,CAC7D,IAAM,EAAa,GAAM,iBAAmB,uBAE5C,MAAO,CACL,kCACA,wCAHiB,GAAM,iBAAmB,eAGS,GACnD,uBAAuB,EAAW,GAClC,+BACA,kDACA,iCACD,CAAC,KAAK;EAAK,EAEf,CA+BH,SAAgB,EAAe,EAA+B,CAC5D,IAAM,EAAa,CACjB,SAAU,GAAQ,KAAK,OAAS,QAChC,WAAY,GAAQ,KAAK,SAAW,cACrC,CAGG,EACE,EAAK,GAAQ,eACnB,GAAI,EAAI,CAKN,GAAM,CAAE,iCAAA,EACE,+BAA+B,CACzC,EAAc,EAA8B,EAAG,MAG/C,EAAc,CAAE,KAAM,gCAAiC,CAMzD,IAAM,EAAqB,CACzB,KAAM,YACN,mBAAoB,CAGlB,MAAO,OACP,QAAQ,EAAsB,CAC5B,OAAO,EAAgB,EAAM,GAAQ,KAAK,KAAK,EAElD,CACF,CAGK,EAA4B,CAChC,KAAM,wBACN,QAAS,CACP,OAAQ,GAAQ,MAAQ,EAAE,EAE7B,CAIG,EAA4C,KAE1C,EAAwB,CAC5B,KAAM,eACN,MAAO,QACP,eAAe,EAAI,CACjB,EAAqB,GAEvB,MAAM,aAAc,CAClB,IAAM,EAAU,GAAQ,QACxB,GAAI,CAAC,GAAW,CAAC,EAAoB,OAErC,IAAM,EAAW,GAAQ,KAAK,OAAS,QACjC,CAAE,OAAQ,GAAe,EAAU,EAAmB,KAAM,EAAS,CACrE,EAAU,EAAoB,EAAoB,EAAY,EAAO,CAE3E,GAAI,CACF,MAAM,EAAQ,MAAM,EAAQ,OACrB,EAAK,CACZ,IAAM,EAAM,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAC5D,KAAK,MAAM,wBAAwB,EAAQ,KAAK,YAAY,IAAM,GAGvE,CAOG,EAA2C,KACzC,EAAoB,CACxB,KAAM,WACN,MAAO,QACP,eAAe,EAAI,CACjB,EAAoB,GAItB,MAAM,aAAc,CACd,QAAQ,SAAW,UAAY,CAAC,GACpC,GAAI,CACF,MAAM,EAAe,EAAmB,EAAS,GAAQ,KAAK,KAAK,EAAI,CAAC,OACjE,EAAK,CACZ,IAAM,EAAM,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,CAC5D,KAAK,MAAM,8CAA8C,IAAM,GAGpE,CAED,MAAO,CASL,EAAmB,CAAE,QAAS,GAAO,CAAC,CACtC,EAAsB,EAAW,CACjC,EACA,EACA,GAAK,GAAQ,SAAW,EAAE,CAC1B,EACA,EACA,EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aihu/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@aihu/arbor": "0.1.4",
|
|
32
|
-
"@aihu/router": "0.1.
|
|
33
|
-
"@aihu/runtime": "0.1.
|
|
32
|
+
"@aihu/router": "0.1.6",
|
|
33
|
+
"@aihu/runtime": "0.1.6",
|
|
34
|
+
"@aihu/server": "0.2.0",
|
|
34
35
|
"@aihu/signals": "0.1.0",
|
|
35
36
|
"vite": ">=5.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@aihu-plugin/agent-readiness": "2.0.
|
|
39
|
-
"@aihu/compiler": "0.
|
|
39
|
+
"@aihu-plugin/agent-readiness": "2.0.3",
|
|
40
|
+
"@aihu/compiler": "0.5.0"
|
|
40
41
|
},
|
|
41
42
|
"description": "Top-level app integration — wires runtime, router, and adapters into a Vite app.",
|
|
42
43
|
"repository": {
|