@aihu/compiler 0.9.6 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,7 @@ npm install @aihu/compiler
21
21
  bun add @aihu/compiler
22
22
  ```
23
23
 
24
- <sub><i>Auto-generated against `@aihu/compiler@0.9.6`.</i></sub>
24
+ <sub><i>Auto-generated against `@aihu/compiler@0.9.9`.</i></sub>
25
25
 
26
26
  <!-- END_AUTOGEN: install -->
27
27
 
@@ -32,12 +32,12 @@ bun add @aihu/compiler
32
32
 
33
33
  | | |
34
34
  |---|---|
35
- | **Version** | `0.9.6` |
35
+ | **Version** | `0.9.9` |
36
36
  | **Tier** | D — Compiler — Single-File Component (.aihu) → Web Component |
37
37
  | **Published files** | 4 entries |
38
38
  | **License** | MIT |
39
39
 
40
- <sub><i>Auto-generated against `@aihu/compiler@0.9.6`.</i></sub>
40
+ <sub><i>Auto-generated against `@aihu/compiler@0.9.9`.</i></sub>
41
41
 
42
42
  <!-- END_AUTOGEN: stats -->
43
43
 
@@ -50,7 +50,7 @@ bun add @aihu/compiler
50
50
  |---|---|---|
51
51
  | `.` | `./dist/index.js` | `—` |
52
52
 
53
- <sub><i>Auto-generated against `@aihu/compiler@0.9.6`.</i></sub>
53
+ <sub><i>Auto-generated against `@aihu/compiler@0.9.9`.</i></sub>
54
54
 
55
55
  <!-- END_AUTOGEN: exports -->
56
56
 
@@ -63,7 +63,15 @@ bun add @aihu/compiler
63
63
 
64
64
  - `vite` — `>=5.0.0`
65
65
 
66
- <sub><i>Auto-generated against `@aihu/compiler@0.9.6`.</i></sub>
66
+ **Optional dependencies (platform-specific):**
67
+
68
+ - `@aihu/compiler-darwin-arm64` — `0.1.0`
69
+ - `@aihu/compiler-darwin-x64` — `0.1.0`
70
+ - `@aihu/compiler-linux-x64-gnu` — `0.1.0`
71
+ - `@aihu/compiler-linux-arm64-gnu` — `0.1.0`
72
+ - `@aihu/compiler-win32-x64-msvc` — `0.1.0`
73
+
74
+ <sub><i>Auto-generated against `@aihu/compiler@0.9.9`.</i></sub>
67
75
 
68
76
  <!-- END_AUTOGEN: deps -->
69
77
 
@@ -77,7 +85,7 @@ bun add @aihu/compiler
77
85
  - [Macro Vocabulary spec](../../docs/superpowers/specs/2026-05-02-spec-macro-vocabulary.md)
78
86
  - [Aihu framework root](../../README.md)
79
87
 
80
- <sub><i>Auto-generated against `@aihu/compiler@0.9.6`.</i></sub>
88
+ <sub><i>Auto-generated against `@aihu/compiler@0.9.9`.</i></sub>
81
89
 
82
90
  <!-- END_AUTOGEN: see-also -->
83
91
 
@@ -88,6 +96,6 @@ bun add @aihu/compiler
88
96
 
89
97
  MIT — see [LICENSE](../../LICENSE).
90
98
 
91
- <sub><i>Auto-generated against `@aihu/compiler@0.9.6`.</i></sub>
99
+ <sub><i>Auto-generated against `@aihu/compiler@0.9.9`.</i></sub>
92
100
 
93
101
  <!-- END_AUTOGEN: license -->
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aihu-compile — ESM bin shim for @aihu/compiler.
4
+ *
5
+ * This is the COMMITTED `bin` target of @aihu/compiler. It carries NO native
6
+ * code; it resolves the platform-specific `aihu-compile` executable at runtime
7
+ * and execs it, propagating the exit code. The native binary ships separately
8
+ * via the `@aihu/compiler-<platform>` optionalDependency packages.
9
+ *
10
+ * Resolution order is identical to js/resolve-binary.ts (the shared resolver
11
+ * used by the vite plugin) and packages/css-engine/src/index.ts:
12
+ * 1. The per-platform optionalDependency package
13
+ * (`@aihu/compiler-<platform>`), resolved via createRequire().resolve of
14
+ * its package.json — the published-consumer path.
15
+ * 2. Dev fallback: the workspace-root `target/release|debug/aihu-compile`,
16
+ * only present in a from-source monorepo clone.
17
+ *
18
+ * The resolver is INLINED (not imported from ../dist/resolve-binary.js) so the
19
+ * shim works in a from-source clone BEFORE `bun run build` has emitted dist.
20
+ * Keep this in lockstep with js/resolve-binary.ts.
21
+ */
22
+ import { spawnSync } from 'node:child_process'
23
+ import { accessSync, constants, existsSync, statSync } from 'node:fs'
24
+ import { createRequire } from 'node:module'
25
+ import { dirname, join, resolve } from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url))
29
+
30
+ function detectPlatform() {
31
+ if (typeof process === 'undefined' || !process.platform || !process.arch) {
32
+ return null
33
+ }
34
+ const key = `${process.platform}-${process.arch}`
35
+ switch (key) {
36
+ case 'darwin-arm64':
37
+ return { packageName: '@aihu/compiler-darwin-arm64', binFile: 'aihu-compile' }
38
+ case 'darwin-x64':
39
+ return { packageName: '@aihu/compiler-darwin-x64', binFile: 'aihu-compile' }
40
+ case 'linux-x64':
41
+ return { packageName: '@aihu/compiler-linux-x64-gnu', binFile: 'aihu-compile' }
42
+ case 'linux-arm64':
43
+ return { packageName: '@aihu/compiler-linux-arm64-gnu', binFile: 'aihu-compile' }
44
+ case 'win32-x64':
45
+ return { packageName: '@aihu/compiler-win32-x64-msvc', binFile: 'aihu-compile.exe' }
46
+ default:
47
+ return null
48
+ }
49
+ }
50
+
51
+ function isUsableExecutable(candidate) {
52
+ try {
53
+ const st = statSync(candidate)
54
+ if (!st.isFile() || st.size === 0) return false
55
+ accessSync(candidate, constants.X_OK)
56
+ return true
57
+ } catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ function resolveCompilerBinary() {
63
+ // 0. Env override + css-engine SCRIBE_COMPILE_BIN handshake (dev override).
64
+ if (process.env.SCRIBE_COMPILE_BIN) {
65
+ return process.env.SCRIBE_COMPILE_BIN
66
+ }
67
+
68
+ const descriptor = detectPlatform()
69
+
70
+ // 1. Per-platform optionalDependency package (the published-consumer path).
71
+ if (descriptor) {
72
+ const requireFn = createRequire(import.meta.url)
73
+ try {
74
+ const pkgJson = requireFn.resolve(`${descriptor.packageName}/package.json`)
75
+ const candidate = join(dirname(pkgJson), descriptor.binFile)
76
+ if (isUsableExecutable(candidate)) return candidate
77
+ } catch {
78
+ // Not installed for this platform — fall through to dev fallback.
79
+ }
80
+ }
81
+
82
+ // 2. Dev fallback: workspace-root target/. This shim lives at
83
+ // packages/compiler/bin/, so the workspace root is three levels up
84
+ // (bin → compiler → packages → root).
85
+ const ext = process.platform === 'win32' ? '.exe' : ''
86
+ const devCandidates = [
87
+ resolve(__dirname, '../../../target/release', `aihu-compile${ext}`),
88
+ resolve(__dirname, '../../../target/debug', `aihu-compile${ext}`),
89
+ // Package-local staged binary (sibling of this shim): CI / release pipeline
90
+ // place a prebuilt binary here. In published consumers only this .mjs shim
91
+ // lives in bin/, so this candidate is absent and we fall through.
92
+ resolve(__dirname, `aihu-compile${ext}`),
93
+ ]
94
+ for (const c of devCandidates) {
95
+ if (existsSync(c)) return c
96
+ }
97
+
98
+ const platform =
99
+ typeof process !== 'undefined' ? `${process.platform}-${process.arch}` : 'unknown'
100
+ process.stderr.write(
101
+ `[@aihu/compiler] Native compiler binary not found.\n\n` +
102
+ ` Platform: ${platform}\n` +
103
+ (descriptor ? ` Expected package: ${descriptor.packageName}\n\n` : '\n') +
104
+ ` The aihu-compile binary ships via the @aihu/compiler-<platform>\n` +
105
+ ` optionalDependency packages. Your package manager may have skipped it.\n` +
106
+ ` Reinstall (npm/pnpm/bun install @aihu/compiler), or in the aihu monorepo\n` +
107
+ ` build from source: cargo build --release -p aihu-compile\n` +
108
+ ` Checked dev fallback paths: ${devCandidates.join(', ')}\n`,
109
+ )
110
+ process.exit(1)
111
+ }
112
+
113
+ const bin = resolveCompilerBinary()
114
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' })
115
+ if (result.error) {
116
+ process.stderr.write(`[@aihu/compiler] failed to exec ${bin}: ${result.error.message}\n`)
117
+ process.exit(1)
118
+ }
119
+ process.exit(result.status ?? 1)
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import{execFileSync as e}from"node:child_process";import{basename as t,dirname as n,resolve as r}from"node:path";import{fileURLToPath as i}from"node:url";const a=process.platform===`win32`?`.exe`:``;function o(){return process.env.SCRIBE_COMPILE_BIN??r(n(i(import.meta.url)),`../bin/aihu-compile${a}`)}function s(e,t){return e.replace(/(defineElement\(\s*['"][^'"]+['"]\s*,\s*defineComponent\([^]*\))\s*\)/,(e,n)=>`${n}, { shadowMode: '${t}' })`)}function c(e){return e.replace(/\(ctx\.host as ShadowRoot\)\.adoptedStyleSheets\s*=\s*\[__style__\];?/,`if (!document.adoptedStyleSheets.includes(__style__)) document.adoptedStyleSheets = [...document.adoptedStyleSheets, __style__];`)}function l(e){return/\b(?:signal|computed|effect|setSignal|onMount|onCleanup)\s*\(/.test(e)?`interactive`:`static`}function u(e){let t=/defineElement\(\s*['"]([^'"]+)['"]/m.exec(e);return t?t[1]??null:null}function d(e,t){let n=t.replace(/\\/g,`/`).replace(/^\.?\//,``).replace(/\/+$/,``);return n?e.replace(/\\/g,`/`).includes(`/${n}/`):!1}function f(e){return`aihu-layout-${e.toLowerCase()}`}function p(e){return e.replace(/const createOutletBoundary = \(\) => \{[\s\S]*?return host;\s*\n\};/,`const createOutletBoundary = () => branch('div', { 'data-aihu-outlet': '' }, []);`)}function m(e,t){let n=e.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/runtime'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`_hmrReplace`)||n.push(`_hmrReplace`),`import { ${n.join(`, `)} } from '@aihu/runtime'`}).replace(/\bdefineComponent\(/,`defineComponent(__aihu_setup__ = `),r=`
1
+ import{resolveCompilerBinary as e}from"./resolve-binary.js";import{execFileSync as t}from"node:child_process";import{basename as n}from"node:path";function r(){return process.env.SCRIBE_COMPILE_BIN??e()}function i(e,t){return e.replace(/(defineElement\(\s*['"][^'"]+['"]\s*,\s*defineComponent\([^]*\))\s*\)/,(e,n)=>`${n}, { shadowMode: '${t}' })`)}function a(e){return e.replace(/\(ctx\.host as ShadowRoot\)\.adoptedStyleSheets\s*=\s*\[__style__\];?/,`if (!document.adoptedStyleSheets.includes(__style__)) document.adoptedStyleSheets = [...document.adoptedStyleSheets, __style__];`)}function o(e){return/\b(?:signal|computed|effect|setSignal|onMount|onCleanup)\s*\(/.test(e)?`interactive`:`static`}function s(e){let t=/defineElement\(\s*['"]([^'"]+)['"]/m.exec(e);return t?t[1]??null:null}function c(e,t){let n=t.replace(/\\/g,`/`).replace(/^\.?\//,``).replace(/\/+$/,``);return n?e.replace(/\\/g,`/`).includes(`/${n}/`):!1}function l(e){return`aihu-layout-${e.toLowerCase()}`}function u(e){return e.replace(/const createOutletBoundary = \(\) => \{[\s\S]*?return host;\s*\n\};/,`const createOutletBoundary = () => branch('div', { 'data-aihu-outlet': '' }, []);`)}function d(e,t){let n=e.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/runtime'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`_hmrReplace`)||n.push(`_hmrReplace`),`import { ${n.join(`, `)} } from '@aihu/runtime'`}).replace(/\bdefineComponent\(/,`defineComponent(__aihu_setup__ = `),r=`
2
2
  export { __aihu_setup__ as default }
3
3
 
4
4
  if (typeof __DEV__ !== 'undefined' && __DEV__ && import.meta.hot) {
@@ -13,7 +13,7 @@ if (typeof __DEV__ !== 'undefined' && __DEV__ && import.meta.hot) {
13
13
  })
14
14
  }
15
15
  `;return`let __aihu_setup__: ((ctx: any) => any) | undefined
16
- `+n+r}function h(e,t){let n=e.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/runtime'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`_hydrateOnVisible`)||n.push(`_hydrateOnVisible`),`import { ${n.join(`, `)} } from '@aihu/runtime'`}),r=n.replace(/defineElement\(\s*('[^']+'|"[^"]+")\s*,\s*defineComponent\(/,(e,t)=>`defineElement(${t}, __aihu_wrap_defer__(defineComponent(`);if(r===n)return e;let i=r.replace(/\)\s*\)\s*\nexport\s/,`)))
16
+ `+n+r}function f(e,t){let n=e.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/runtime'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`_hydrateOnVisible`)||n.push(`_hydrateOnVisible`),`import { ${n.join(`, `)} } from '@aihu/runtime'`}),r=n.replace(/defineElement\(\s*('[^']+'|"[^"]+")\s*,\s*defineComponent\(/,(e,t)=>`defineElement(${t}, __aihu_wrap_defer__(defineComponent(`);if(r===n)return e;let i=r.replace(/\)\s*\)\s*\nexport\s/,`)))
17
17
  export `);return i===r&&(i=r.replace(/\)\s*\)\s*$/,`)))
18
18
  `)),i===r?e:`
19
19
  // Plan 3.3 (Islands) — defer attribute support. Wraps the constructor
@@ -32,14 +32,14 @@ function __aihu_wrap_defer__<T extends typeof HTMLElement>(Ctor: T): T {
32
32
  }
33
33
  return Ctor
34
34
  }
35
- `+i}function g(e,t){if(!/defineElement\(\s*['"][^'"]+['"]\s*,\s*defineComponent\(/.test(e))return e;let n=e.replace(/^\s*import\s*\{[^}]*\}\s*from\s*'@aihu\/runtime'\s*;?\s*$/m,``).replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/arbor'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`mount`)||n.push(`mount`),`import { ${n.join(`, `)} } from '@aihu/arbor'`}),r=JSON.stringify(t);return`// SCRIBE_STATIC_ISLAND — zero @aihu/runtime references\n${n.replace(/defineElement\(\s*['"][^'"]+['"]\s*,\s*defineComponent\(/,`customElements.define(${r}, class extends HTMLElement {\n connectedCallback() {\n const root = this.attachShadow({ mode: 'open' })\n const __aihu_setup__ = (`).replace(/\)\s*\)\s*$/,`)
35
+ `+i}function p(e,t){if(!/defineElement\(\s*['"][^'"]+['"]\s*,\s*defineComponent\(/.test(e))return e;let n=e.replace(/^\s*import\s*\{[^}]*\}\s*from\s*'@aihu\/runtime'\s*;?\s*$/m,``).replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/arbor'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`mount`)||n.push(`mount`),`import { ${n.join(`, `)} } from '@aihu/arbor'`}),r=JSON.stringify(t);return`// SCRIBE_STATIC_ISLAND — zero @aihu/runtime references\n${n.replace(/defineElement\(\s*['"][^'"]+['"]\s*,\s*defineComponent\(/,`customElements.define(${r}, class extends HTMLElement {\n connectedCallback() {\n const root = this.attachShadow({ mode: 'open' })\n const __aihu_setup__ = (`).replace(/\)\s*\)\s*$/,`)
36
36
  mount(__aihu_setup__({ host: root, element: this }), root)
37
37
  }
38
38
  })
39
- `)}`}function _(n,r,i){let a=t(r,`.aihu`),s=[`--stdin`,`--tag`,i?.tag??a,`--path`,r];return i?.sidecarOut&&s.push(`--sidecar-out`,i.sidecarOut),i?.target&&s.push(`--target`,i.target),{code:e(o(),s,{input:n,encoding:`utf8`}),map:null}}function v(e){return e.replace(/\\/g,`\\\\`).replace(/`/g,"\\`").replace(/\$\{/g,"\\${")}function y(e,t){if(!t.trim())return e;let n=v(t),r=/(__style__\.replaceSync\(`)[^]*?(`\);)/;if(r.test(e))return e.replace(r,(e,t,r)=>`${t}${n}${r}`);if(/defineComponent\(\s*\((_ctx|ctx)\)\s*=>\s*\{/.exec(e)==null)return e;let i=e.split(`
39
+ `)}`}function m(e,i,a){let o=n(i,`.aihu`),s=[`--stdin`,`--tag`,a?.tag??o,`--path`,i];return a?.sidecarOut&&s.push(`--sidecar-out`,a.sidecarOut),a?.target&&s.push(`--target`,a.target),{code:t(r(),s,{input:e,encoding:`utf8`}),map:null}}function h(e){return e.replace(/\\/g,`\\\\`).replace(/`/g,"\\`").replace(/\$\{/g,"\\${")}function g(e,t){if(!t.trim())return e;let n=h(t),r=/(__style__\.replaceSync\(`)[^]*?(`\);)/;if(r.test(e))return e.replace(r,(e,t,r)=>`${t}${n}${r}`);if(/defineComponent\(\s*\((_ctx|ctx)\)\s*=>\s*\{/.exec(e)==null)return e;let i=e.split(`
40
40
  `),a=-1;for(let e=i.length-1;e>=0;e--){let t=(i[e]??``).trim();if(t.startsWith(`import `)||t.startsWith(`import{`)){a=e;break}}let o=`const __style__ = new CSSStyleSheet();\n__style__.replaceSync(\`${n}\`);`;a===-1?i.unshift(o):i.splice(a+1,0,o);let s=i.join(`
41
41
  `);return s=s.replace(/defineComponent\(\s*\((?:_ctx|ctx)\)\s*=>\s*\{/,`defineComponent((ctx) => {
42
- (ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];`),s}const b=`\0virtual:aihu-utility/`;function x(e){let t=5381;for(let n=0;n<e.length;n++)t=(t*33^e.charCodeAt(n))>>>0;return t.toString(36)}function S(e,t,n){if(!t.trim())return null;let r=`${b}${x(n)}.css`;return{code:`import ${JSON.stringify(r)};\n`+e,virtualId:r}}function C(n,r){let i=[`--stdin`,`--tag`,r?t(r,`.aihu`):`Component`,`--ast-json`];r&&i.push(`--path`,r);let a=e(o(),i,{input:n,encoding:`utf8`});return JSON.parse(a)}function w(n,r){let i=[`--stdin`,`--tag`,r?t(r,`.aihu`):`Component`,`--route-json`];r&&i.push(`--path`,r);let a=e(o(),i,{input:n,encoding:`utf8`}).trim();return a===``||a===`null`?null:JSON.parse(a)}function T(e){let t;t=e.includes(`from '@aihu/arbor'`)?e.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/arbor'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`mount`)||n.push(`mount`),`import { ${n.join(`, `)} } from '@aihu/arbor'`}):`import { mount } from '@aihu/arbor'\n${e}`,/import\s+\{[^}]*\}\s+from\s+'@aihu\/signals'/.test(t)?t=t.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/signals'/,(e,t)=>{if(e.startsWith(`import type`))return e;let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`signal`)||n.push(`signal`),`import { ${n.join(`, `)} } from '@aihu/signals'`}):/import.*from\s*'@aihu\/signals'/.test(t)?/import\s+type\s+\{[^}]*\}\s+from\s+'@aihu\/signals'/.test(t)&&!t.match(/import\s+\{[^}]*\}\s+from\s+'@aihu\/signals'/)&&(t=t.replace(/(import\s+type\s+\{[^}]*\}\s+from\s+'@aihu\/signals')/,(e,t)=>`${t}\nimport { signal } from '@aihu/signals'`)):t=t.replace(/import\s*\{[^}]*\}\s*from\s*'@aihu\/arbor'/,e=>`${e}\nimport { signal } from '@aihu/signals'`),t=t.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/runtime'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`_setMount`)||n.push(`_setMount`),n.includes(`_setSignal`)||n.push(`_setSignal`),`import { ${n.join(`, `)} } from '@aihu/runtime'`});let n=t.split(`
42
+ (ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];`),s}const _=`\0virtual:aihu-utility/`;function v(e){let t=5381;for(let n=0;n<e.length;n++)t=(t*33^e.charCodeAt(n))>>>0;return t.toString(36)}function y(e,t,n){if(!t.trim())return null;let r=`${_}${v(n)}.css`;return{code:`import ${JSON.stringify(r)};\n`+e,virtualId:r}}function b(e,i){let a=[`--stdin`,`--tag`,i?n(i,`.aihu`):`Component`,`--ast-json`];i&&a.push(`--path`,i);let o=t(r(),a,{input:e,encoding:`utf8`});return JSON.parse(o)}function x(e,i){let a=[`--stdin`,`--tag`,i?n(i,`.aihu`):`Component`,`--route-json`];i&&a.push(`--path`,i);let o=t(r(),a,{input:e,encoding:`utf8`}).trim();return o===``||o===`null`?null:JSON.parse(o)}function S(e){let t;t=e.includes(`from '@aihu/arbor'`)?e.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/arbor'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`mount`)||n.push(`mount`),`import { ${n.join(`, `)} } from '@aihu/arbor'`}):`import { mount } from '@aihu/arbor'\n${e}`,/import\s+\{[^}]*\}\s+from\s+'@aihu\/signals'/.test(t)?t=t.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/signals'/,(e,t)=>{if(e.startsWith(`import type`))return e;let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`signal`)||n.push(`signal`),`import { ${n.join(`, `)} } from '@aihu/signals'`}):/import.*from\s*'@aihu\/signals'/.test(t)?/import\s+type\s+\{[^}]*\}\s+from\s+'@aihu\/signals'/.test(t)&&!t.match(/import\s+\{[^}]*\}\s+from\s+'@aihu\/signals'/)&&(t=t.replace(/(import\s+type\s+\{[^}]*\}\s+from\s+'@aihu\/signals')/,(e,t)=>`${t}\nimport { signal } from '@aihu/signals'`)):t=t.replace(/import\s*\{[^}]*\}\s*from\s*'@aihu\/arbor'/,e=>`${e}\nimport { signal } from '@aihu/signals'`),t=t.replace(/import\s*\{([^}]*)\}\s*from\s*'@aihu\/runtime'/,(e,t)=>{let n=t.split(`,`).map(e=>e.trim()).filter(Boolean);return n.includes(`_setMount`)||n.push(`_setMount`),n.includes(`_setSignal`)||n.push(`_setSignal`),`import { ${n.join(`, `)} } from '@aihu/runtime'`});let n=t.split(`
43
43
  `),r=-1;for(let e=n.length-1;e>=0;e--){let t=(n[e]??``).trim();if(t.startsWith(`import `)||t.startsWith(`import{`)){r=e;break}}return r!==-1&&(n.splice(r+1,0,`_setMount(mount)`,`_setSignal(signal)`,``),t=n.join(`
44
- `)),t}let E,D=!1;async function O(e,t){if(E===null)return``;if(process.env.SCRIBE_COMPILE_BIN??(process.env.SCRIBE_COMPILE_BIN=o()),E===void 0)try{E=await import(`@aihu/css-engine`)}catch{return E=null,``}try{return E.compileSfc(e,t)}catch(e){if(!D){D=!0;let t=e instanceof Error?e.message:String(e);console.warn(`[@aihu/compiler] @aihu/css-engine is installed but compileSfc() failed; utility classes will not emit. Original error: ${t}\nHint: ensure the native css-core binary is installed (install/upgrade @aihu/css-engine + its per-platform optional dep, or run \`cargo build --release -p aihu-css-core\` in a dev clone).`)}return``}}function k(e){let n=e?.islands!==!1,r=e?.shadowMode,i=e?.target,a=e?.layoutsDir??`src/layouts`,o=new Map;return{name:`aihu-compiler`,enforce:`pre`,resolveId(e){return e.startsWith(`\0virtual:aihu-utility/`)?e:null},load(e){return e.startsWith(`\0virtual:aihu-utility/`)?o.get(e)??null:null},transform(e,v){let b=v.split(`?`)[0];if(b.endsWith(`.aihu`))return(async()=>{let v=`${b}.ts`,x=d(b,a),C=x?f(t(b,`.aihu`)):void 0,w=_(e,b,{sidecarOut:v,...i?{target:i}:{},...C?{tag:C}:{}}),E=/^\/\/ @aihu:shadow (open|closed|none)\b/m.exec(w.code)?.[1]??r,D=E==null?w.code:s(w.code,E);E===`none`&&(D=c(D)),x&&(D=p(D));let k=await O(e,b);if(k)if(E===`none`){let e=S(D,k,b);e&&(o.set(e.virtualId,k),D=e.code)}else D=y(D,k);let A=u(D),j,M=/defineComponent\(\s*\{[^]*?\bbase\s*:/.test(D);n&&A!==null&&!M&&l(D)===`static`?j=g(D,A):A===null?(j=D,j=T(j)):(j=m(D,A),j=h(j,A),j=T(j));try{let e=await import(`vite`);return`transformWithEsbuild`in e&&typeof e.transformWithEsbuild==`function`?{code:(await e.transformWithEsbuild(j,`component.ts`,{target:`esnext`,sourcemap:!1})).code,map:null}:{code:j,moduleType:`ts`,map:null}}catch{return{code:j,map:null}}})()}}}export{b as VIRTUAL_UTILITY_PREFIX,h as _buildDeferredHydration,g as _buildStaticIsland,l as _classifyIsland,y as _foldCssEngineStyles,S as _foldCssEngineStylesGlobal,c as _globalizeAuthoredStyle,x as _hashIdForUtilityCss,T as _injectAutoWiring,s as _injectShadowMode,d as _isLayoutFile,f as _layoutTag,p as _passivizeOutlet,k as aihuCompilerPlugin,w as compileRouteMeta,C as compileToAst,_ as transform};
44
+ `)),t}let C,w=!1;async function T(e,t){if(C===null)return``;if(process.env.SCRIBE_COMPILE_BIN??(process.env.SCRIBE_COMPILE_BIN=r()),C===void 0)try{C=await import(`@aihu/css-engine`)}catch{return C=null,``}try{return C.compileSfc(e,t)}catch(e){if(!w){w=!0;let t=e instanceof Error?e.message:String(e);console.warn(`[@aihu/compiler] @aihu/css-engine is installed but compileSfc() failed; utility classes will not emit. Original error: ${t}\nHint: ensure the native css-core binary is installed (install/upgrade @aihu/css-engine + its per-platform optional dep, or run \`cargo build --release -p aihu-css-core\` in a dev clone).`)}return``}}function E(e){let t=e?.islands!==!1,r=e?.shadowMode,h=e?.target,_=e?.layoutsDir??`src/layouts`,v=new Map;return{name:`aihu-compiler`,enforce:`pre`,resolveId(e){return e.startsWith(`\0virtual:aihu-utility/`)?e:null},load(e){return e.startsWith(`\0virtual:aihu-utility/`)?v.get(e)??null:null},transform(e,b){let x=b.split(`?`)[0];if(x.endsWith(`.aihu`))return(async()=>{let b=`${x}.ts`,C=c(x,_),w=C?l(n(x,`.aihu`)):void 0,E=m(e,x,{sidecarOut:b,...h?{target:h}:{},...w?{tag:w}:{}}),D=/^\/\/ @aihu:shadow (open|closed|none)\b/m.exec(E.code)?.[1]??r,O=D==null?E.code:i(E.code,D);D===`none`&&(O=a(O)),C&&(O=u(O));let k=await T(e,x);if(k)if(D===`none`){let e=y(O,k,x);e&&(v.set(e.virtualId,k),O=e.code)}else O=g(O,k);let A=s(O),j,M=/defineComponent\(\s*\{[^]*?\bbase\s*:/.test(O);t&&A!==null&&!M&&o(O)===`static`?j=p(O,A):A===null?(j=O,j=S(j)):(j=d(O,A),j=f(j,A),j=S(j));try{let e=await import(`vite`);return`transformWithEsbuild`in e&&typeof e.transformWithEsbuild==`function`?{code:(await e.transformWithEsbuild(j,`component.ts`,{target:`esnext`,sourcemap:!1})).code,map:null}:{code:j,moduleType:`ts`,map:null}}catch{return{code:j,map:null}}})()}}}export{_ as VIRTUAL_UTILITY_PREFIX,f as _buildDeferredHydration,p as _buildStaticIsland,o as _classifyIsland,g as _foldCssEngineStyles,y as _foldCssEngineStylesGlobal,a as _globalizeAuthoredStyle,v as _hashIdForUtilityCss,S as _injectAutoWiring,i as _injectShadowMode,c as _isLayoutFile,l as _layoutTag,u as _passivizeOutlet,E as aihuCompilerPlugin,x as compileRouteMeta,b as compileToAst,m as transform};
45
45
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../js/index.ts"],"sourcesContent":["/**\n * @aihu/compiler — TypeScript wrapper around the aihu-compile Rust binary.\n *\n * Exports:\n * transform(source, id) — compile a single .aihu file to TypeScript\n * aihuCompilerPlugin() — Vite plugin that wires transform() into the build\n */\nimport { execFileSync } from 'node:child_process'\nimport { basename, dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n// Binary resolution: env var override, fallback to the bin/ directory written\n// by the postinstall hook (packages/compiler/bin/aihu-compile[.exe]).\n//\n// Bug 6 fix — resolveBinPath() is CALL-TIME, not module-load-time. The Vite\n// plugin's `_maybeCompileUtilityCss` sets `process.env.SCRIBE_COMPILE_BIN` so\n// that css-engine's bundled copy of `compileToAst` spawns THIS compiler's\n// binary. Prior to this fix `binPath` was a module-scope const captured at\n// import time, so the env-var assignment was always too late and `compileSfc`\n// failed with ENOENT on the (non-existent) `packages/css-engine/bin/aihu-compile`\n// path. Re-reading on every call is essentially free (string concatenation +\n// a single env lookup) and makes the SCRIBE_COMPILE_BIN handshake actually work.\nconst ext = process.platform === 'win32' ? '.exe' : ''\nfunction resolveBinPath(): string {\n return (\n process.env.SCRIBE_COMPILE_BIN ??\n resolve(dirname(fileURLToPath(import.meta.url)), `../bin/aihu-compile${ext}`)\n )\n}\n\n// Minimal VitePlugin interface — avoids importing from 'vite' at compile time.\n// Structurally compatible with Vite's Plugin type.\ninterface VitePlugin {\n readonly name: string\n enforce?: 'pre' | 'post'\n resolveId?: (\n source: string,\n importer?: string,\n ) => string | null | undefined | Promise<string | null | undefined>\n load?: (id: string) => string | null | undefined | Promise<string | null | undefined>\n transform?: (\n code: string,\n id: string,\n ) => Promise<{ code: string; map: null }> | { code: string; map: null } | null | undefined\n}\n\n/**\n * Options for `aihuCompilerPlugin()` (Plan 3.3 — Islands).\n */\nexport interface AihuCompilerPluginOptions {\n /**\n * When `true` (default), components classified as `'static'` by\n * `_classifyIsland()` are emitted with a minimal HTML-only registration\n * shim that ships **zero** `@aihu/runtime` and `@aihu/signals` JS to\n * the browser. Components classified as `'interactive'` retain the\n * full runtime path.\n *\n * Setting `islands: false` opts every component back into the unified\n * runtime path (Plan 3.2 baseline behaviour).\n */\n islands?: boolean\n\n /**\n * Project-wide shadow-DOM mode applied to every `.aihu` SFC compiled\n * by this plugin instance. When set, the plugin post-processes the\n * compiled JS to inject `, { shadowMode: '<mode>' }` as the third arg\n * to the emitted `defineElement(tag, defineComponent(...))` call.\n *\n * - `'open'` — default browser behaviour (shadow root, externally readable).\n * - `'closed'` — shadow root, externally hidden.\n * - `'none'` — **no shadow root.** The component mounts into its own\n * element. Required for global utility-class CSS frameworks\n * like Tailwind, UnoCSS, Pico that rely on the cascade.\n *\n * Per-component override is not yet supported via SFC syntax (post-v1).\n * For per-component control today, hand-author the component with\n * `defineElement(tag, Ctor, { shadowMode: '...' })`.\n */\n shadowMode?: 'open' | 'closed' | 'none'\n\n /**\n * Build target threaded to the compiler binary (`--target`). Defaults to the\n * compiler's `universal` target (current behaviour). Set to `'client'` for a\n * browser bundle that must NOT ship the server `__agentBinding` (policy) and\n * instead gets the policy-free `@agent` opaque-ID dispatcher + the per-instance\n * `_registerAgentDispatcher` wiring the capability bridge reads after mount.\n * See `examples/agent-driven-demo`.\n */\n target?: 'client' | 'server' | 'universal'\n\n /**\n * Directory (relative to the project root) holding layout SFCs. Default:\n * `'src/layouts'`. Files under this directory are compiled in **layout mode**:\n * their custom element is registered under the namespaced tag\n * `aihu-layout-<stem>` (a layout stem like `app` is not a valid custom-element\n * name on its own), and their `<$outlet>` lowers to a **passive**\n * `data-aihu-outlet` marker rather than the reactive route-driven boundary —\n * because `@aihu/app`'s client renderer fills the marker imperatively and the\n * reactive boundary would otherwise clear it on mount.\n *\n * Kept in sync with `@aihu/router`'s `layoutTagFor()` (`virtual:aihu-layouts`).\n */\n layoutsDir?: string\n}\n\n/**\n * Inject `{ shadowMode: '...' }` as the third argument to the emitted\n * `defineElement('tag', defineComponent(...))` call. The compiler emits\n * exactly two arguments today; this rewrites the closing of the\n * defineElement call to include the options object. Idempotent — leaves\n * code untouched when the closer is not in the expected shape.\n *\n * @internal\n */\nexport function _injectShadowMode(code: string, mode: 'open' | 'closed' | 'none'): string {\n // Match the trailing `))` that closes `defineElement(tag, defineComponent(setup))`.\n // The compiler always emits this exact two-paren close as the final tokens of\n // the defineElement call — we anchor on it and append the options object.\n // biome-ignore lint/correctness/noEmptyCharacterClassInRegex: [^] is valid JS — matches any char including newlines\n const re = /(defineElement\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*defineComponent\\([^]*\\))\\s*\\)/\n const replaced = code.replace(re, (_m, inner: string) => `${inner}, { shadowMode: '${mode}' })`)\n return replaced\n}\n\n/**\n * Light-DOM (`shadowMode:'none'`) recipes: redirect the authored `@style`\n * block's per-instance `host.adoptedStyleSheets = [__style__]` assignment to\n * `document.adoptedStyleSheets` so the recipe's class-scoped CSS reaches the\n * global cascade (a light-DOM host has no shadow root, making the original\n * setter a silent no-op). The module-level `__style__` is shared across\n * instances; the `includes` guard keeps the global adoption idempotent.\n *\n * @internal\n */\nexport function _globalizeAuthoredStyle(code: string): string {\n // The Rust codegen emits exactly: `(ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];`\n const re = /\\(ctx\\.host as ShadowRoot\\)\\.adoptedStyleSheets\\s*=\\s*\\[__style__\\];?/\n return code.replace(\n re,\n 'if (!document.adoptedStyleSheets.includes(__style__)) document.adoptedStyleSheets = [...document.adoptedStyleSheets, __style__];',\n )\n}\n\n/**\n * Classify the compiled output of a single `.aihu` module as either a\n * **static** island (no reactive state — purely declarative DOM) or an\n * **interactive** island (uses the signals reactivity system).\n *\n * The heuristic is intentionally conservative: any source-level reference\n * to a primitive that requires the `defineComponent` owner context flips\n * the file to `'interactive'`. False positives (e.g. a string literal\n * containing `signal(`) are tolerable — they only forfeit the static-island\n * optimisation. False negatives are forbidden: a static-classified file\n * MUST NOT depend on the signals runtime at execution time.\n *\n * Owner-requiring primitives covered:\n * - `signal(`, `computed(`, `effect(`, `setSignal(` (signals runtime)\n * - `onMount(`, `onCleanup(` (lifecycle hooks — throw `no owner` outside\n * `defineComponent` because they push into the active owner's mount/\n * cleanup queues)\n *\n * Plan 3.3 / acceptance criterion 1.\n *\n * @internal\n */\nexport function _classifyIsland(compiledCode: string): 'static' | 'interactive' {\n // Match call sites of the reactive + lifecycle primitives. Use word-boundary\n // anchors so identifiers like `mySignal(` or `__effect(` do not trip the\n // heuristic. The `(` is required so that bare imports of the names in an\n // unused `import { signal }` line do not flip an otherwise-static module.\n return /\\b(?:signal|computed|effect|setSignal|onMount|onCleanup)\\s*\\(/.test(compiledCode)\n ? 'interactive'\n : 'static'\n}\n\n/**\n * Extract the custom element tag name from compiler-emitted code.\n * The compiler always emits `defineElement('tag-name', ...)` as the\n * first call — pull the first string literal argument.\n * Returns `null` if no `defineElement` call is found.\n * @internal\n */\nfunction _extractElementTag(code: string): string | null {\n const m = /defineElement\\(\\s*['\"]([^'\"]+)['\"]/m.exec(code)\n return m ? (m[1] ?? null) : null\n}\n\n/**\n * Is `rawId` a layout SFC (a `.aihu` file under the configured layouts dir)?\n * Root-independent: matches the `<layoutsDir>/` segment anywhere in the path,\n * which is sufficient because the layouts dir is a project-relative convention.\n * @internal\n */\nexport function _isLayoutFile(rawId: string, layoutsDir: string): boolean {\n const ld = layoutsDir\n .replace(/\\\\/g, '/')\n .replace(/^\\.?\\//, '')\n .replace(/\\/+$/, '')\n if (!ld) return false\n return rawId.replace(/\\\\/g, '/').includes(`/${ld}/`)\n}\n\n/**\n * Layout custom-element tag for a filename stem. MUST match\n * `@aihu/router`'s `layoutTagFor()` so the generated `virtual:aihu-layouts`\n * map and the registered element agree on the tag.\n * @internal\n */\nexport function _layoutTag(stem: string): string {\n return `aihu-layout-${stem.toLowerCase()}`\n}\n\n/**\n * Collapse the reactive `<$outlet>` boundary the Rust codegen emits into a\n * passive `data-aihu-outlet` marker. Layout SFCs are rendered by `@aihu/app`'s\n * imperative client renderer, which fills the marker itself; the default\n * boundary's mount-time `effect()` reads `useRoute()` (null under the imperative\n * path) and clears the marker, which would wipe the page the renderer inserts.\n *\n * Anchors on the exact `const createOutletBoundary = () => { … return host; };`\n * block the codegen emits (`packages/compiler/src/codegen/emit.rs`). No-op when\n * the layout declares no `<$outlet>`.\n * @internal\n */\nexport function _passivizeOutlet(code: string): string {\n return code.replace(\n /const createOutletBoundary = \\(\\) => \\{[\\s\\S]*?return host;\\s*\\n\\};/,\n `const createOutletBoundary = () => branch('div', { 'data-aihu-outlet': '' }, []);`,\n )\n}\n\n/**\n * Instrument a compiled `.aihu` module with HMR support.\n *\n * The compiler always emits:\n *\n * import { defineComponent, defineElement } from '@aihu/runtime'\n * defineElement('tag', defineComponent((_ctx) => { ... }))\n *\n * This function:\n *\n * 1. Adds `_hmrReplace` to the `@aihu/runtime` import.\n * 2. Prepends a module-level slot variable `__aihu_setup__`.\n * 3. Rewrites the single `defineComponent(` call so the setup function\n * is captured via an assignment expression:\n * `defineComponent(__aihu_setup__ = ` (valid JS; assignment has\n * lower precedence than arrow fn, so `defineComponent` still\n * receives the function as its argument).\n * 4. Appends `export { __aihu_setup__ as default }` so that Vite's\n * `import.meta.hot.accept` callback receives the new setup via\n * `newModule.default` on hot reload.\n * 5. Appends the `import.meta.hot.accept` block, gated on `__DEV__`.\n *\n * The `__DEV__` guard ensures production bundlers (where they replace\n * `__DEV__` with `false`) dead-code-eliminate the entire HMR block.\n *\n * @internal\n */\nfunction _buildHmrCode(compiledCode: string, elementTag: string): string {\n // Step 1 — add _hmrReplace to the @aihu/runtime import.\n const withImport = compiledCode.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/runtime'/,\n (_m, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('_hmrReplace')) parts.push('_hmrReplace')\n return `import { ${parts.join(', ')} } from '@aihu/runtime'`\n },\n )\n\n // Step 2+3 — prepend slot variable and rewrite the defineComponent call.\n // Compiler emits exactly one `defineComponent(` followed by a function expr.\n // Rewrite: defineComponent(fn) → defineComponent(__aihu_setup__ = fn)\n // Assignment expression evaluates to `fn`, so defineComponent still\n // receives the setup function as its first argument unchanged.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const preamble = `let __aihu_setup__: ((ctx: any) => any) | undefined\\n`\n\n const patchedBody = withImport.replace(/\\bdefineComponent\\(/, 'defineComponent(__aihu_setup__ = ')\n\n const tag = JSON.stringify(elementTag)\n // Step 4+5 — postamble with default export and HMR acceptance.\n const postamble = `\nexport { __aihu_setup__ as default }\n\nif (typeof __DEV__ !== 'undefined' && __DEV__ && import.meta.hot) {\n import.meta.hot.accept((newModule) => {\n if (!newModule) return\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const newSetup = (newModule as any)['default']\n if (typeof newSetup !== 'function') return\n document.querySelectorAll(${tag}).forEach((el) => {\n _hmrReplace(el as HTMLElement, newSetup)\n })\n })\n}\n`\n\n return preamble + patchedBody + postamble\n}\n\n/**\n * Rewrite an interactive-island module so its `connectedCallback` waits\n * for the element to scroll into view before mounting. Plan 3.3 — applied\n * only when the consumer adds `defer` to the custom element tag (e.g.\n * `<my-counter defer>`); the runtime helper checks the attribute and\n * either mounts immediately or registers an `IntersectionObserver`.\n *\n * Implementation: the helper is added as a `_hydrateOnVisible` import\n * from `@aihu/runtime`, and the compiler-emitted `defineElement(...)`\n * call is wrapped in a `defineElement` that intercepts `connectedCallback`\n * to honour the `defer` attribute.\n *\n * The whole indirection is tree-shaken when no `.aihu` module reaches\n * this branch, because `_hydrateOnVisible` is exported from its own\n * sibling module inside `@aihu/runtime`.\n *\n * @internal\n */\nexport function _buildDeferredHydration(compiledCode: string, elementTag: string): string {\n // Add _hydrateOnVisible to the @aihu/runtime import.\n const withImport = compiledCode.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/runtime'/,\n (_m, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('_hydrateOnVisible')) parts.push('_hydrateOnVisible')\n return `import { ${parts.join(', ')} } from '@aihu/runtime'`\n },\n )\n\n // Wrap the class returned by defineComponent BEFORE defineElement\n // consumes it. The HTML spec caches lifecycle callbacks at\n // customElements.define() time, so we MUST mutate the prototype\n // before that call — not after. We accomplish this with a synchronous\n // helper invoked between defineComponent and defineElement.\n //\n // Source pattern (compiler-emitted):\n // defineElement('tag', defineComponent((_ctx) => { ... }))\n //\n // After this rewrite:\n // defineElement('tag', __aihu_wrap_defer__(defineComponent((_ctx) => { ... })))\n //\n // …with __aihu_wrap_defer__ defined in the appended preamble.\n const patched = withImport.replace(\n /defineElement\\(\\s*('[^']+'|\"[^\"]+\")\\s*,\\s*defineComponent\\(/,\n (_m, tagLit: string) => `defineElement(${tagLit}, __aihu_wrap_defer__(defineComponent(`,\n )\n // Match the closing `))` of the defineElement call. The HMR pass may\n // have inserted `__aihu_setup__ = ` before the inner function, but\n // the trailing `))` shape is unchanged. Replace exactly one occurrence\n // by anchoring on end-of-string trim; bail if the shape does not match.\n if (patched === withImport) {\n // The expected `defineElement(<tag>, defineComponent(` shape was not\n // present (e.g. compiler output changed). Skip defer wrapping rather\n // than emit broken code.\n return compiledCode\n }\n // Add a trailing `)` to balance the extra `(` from __aihu_wrap_defer__.\n // Source shape after _buildHmrCode is:\n // defineElement('tag', defineComponent(__aihu_setup__ = (_ctx) => {...}))\n // export { __aihu_setup__ as default }\n // if (typeof __DEV__ !== ...) { ... }\n // We must close BEFORE the export line. Match the first `))` followed\n // by a newline and `export` (or end-of-string for the unwrapped case).\n let balanced = patched.replace(/\\)\\s*\\)\\s*\\nexport\\s/, ')))\\nexport ')\n if (balanced === patched) {\n // No HMR postamble — the `))` is at end-of-string.\n balanced = patched.replace(/\\)\\s*\\)\\s*$/, ')))\\n')\n }\n if (balanced === patched) {\n // Could not find the matching `))` — bail out.\n return compiledCode\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const helper = `\n// Plan 3.3 (Islands) — defer attribute support. Wraps the constructor\n// returned by defineComponent so instances bearing the \\`defer\\` attribute\n// hydrate lazily via IntersectionObserver. Bare instances retain the\n// eager Plan 3.2 hydration path.\nfunction __aihu_wrap_defer__<T extends typeof HTMLElement>(Ctor: T): T {\n const orig = (Ctor.prototype as unknown as { connectedCallback?: () => void }).connectedCallback\n if (typeof orig !== 'function') return Ctor\n ;(Ctor.prototype as unknown as { connectedCallback: () => void }).connectedCallback = function (this: HTMLElement) {\n if (this.hasAttribute('defer')) {\n _hydrateOnVisible(this, () => orig.call(this))\n } else {\n orig.call(this)\n }\n }\n return Ctor\n}\n`\n void elementTag\n return helper + balanced\n}\n\n/**\n * Build a static-island shim for a compiled module.\n *\n * The compiled module emitted by the Rust codegen has the shape:\n *\n * import { branch, leaf, slot } from '@aihu/arbor'\n * import { defineComponent, defineElement } from '@aihu/runtime'\n * defineElement('tag', defineComponent((_ctx) => { return <tree> }))\n *\n * For a static island we know `<tree>` contains no `signal(`/`computed(`\n * calls. We can therefore:\n *\n * 1. Drop the `@aihu/runtime` import (saves ~600 B gz of defineComponent\n * + defineElement + bootstrap glue).\n * 2. Replace `defineElement(tag, defineComponent(setup))` with a tiny\n * inline class that mounts the tree directly via `mount()` (which the\n * arbor barrel already exports).\n * 3. Tag the file with a `// SCRIBE_STATIC_ISLAND` comment so consumers\n * can audit which routes shipped zero-JS-runtime.\n *\n * Falls back to the original code if the regex shape does not match\n * (defensive: a future compiler change must opt back into static-island\n * emission explicitly rather than silently break).\n *\n * @internal\n */\nexport function _buildStaticIsland(compiledCode: string, elementTag: string): string {\n // Confirm the shape we expect: a single defineElement(...) call wrapping\n // a single defineComponent(...) call. Bail out otherwise.\n const callRe = /defineElement\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*defineComponent\\(/\n if (!callRe.test(compiledCode)) return compiledCode\n\n // Strip the `@aihu/runtime` import line entirely — static islands\n // don't reference defineComponent/defineElement after the rewrite.\n const withoutRuntimeImport = compiledCode.replace(\n /^\\s*import\\s*\\{[^}]*\\}\\s*from\\s*'@aihu\\/runtime'\\s*;?\\s*$/m,\n '',\n )\n\n // Ensure `mount` is imported from @aihu/arbor (it already exposes\n // branch/leaf/slot, so we just append `mount` to the existing list).\n const withArborMount = withoutRuntimeImport.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/arbor'/,\n (_m, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('mount')) parts.push('mount')\n return `import { ${parts.join(', ')} } from '@aihu/arbor'`\n },\n )\n\n // Replace `defineElement('tag', defineComponent((_ctx) => { ... }))`\n // with an inline `customElements.define` whose connectedCallback mounts\n // the static tree. The setup function is captured verbatim by replacing\n // the wrapping calls with anonymous-IIFE bookends.\n const tagJson = JSON.stringify(elementTag)\n const rewritten = withArborMount\n .replace(\n /defineElement\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*defineComponent\\(/,\n `customElements.define(${tagJson}, class extends HTMLElement {\\n connectedCallback() {\\n const root = this.attachShadow({ mode: 'open' })\\n const __aihu_setup__ = (`,\n )\n .replace(\n /\\)\\s*\\)\\s*$/,\n `)\\n mount(__aihu_setup__({ host: root, element: this }), root)\\n }\\n})\\n`,\n )\n\n return `// SCRIBE_STATIC_ISLAND — zero @aihu/runtime references\\n${rewritten}`\n}\n\n/**\n * Compile a .aihu source string to TypeScript.\n * map is null — source maps are deferred to v1 (OQ-C8)\n *\n * B3b — when `sidecarOut` is provided, also writes the per-SFC `.aihu.ts`\n * sidecar at that path. Callers (e.g. the Vite plugin) typically pass\n * `<source-id>.ts` so `tsc --noEmit` discovers per-SFC template expressions.\n */\nexport function transform(\n source: string,\n id: string,\n options?: {\n sidecarOut?: string\n target?: 'client' | 'server' | 'universal'\n /** Override the registered custom-element tag (default: file stem). Used for layouts. */\n tag?: string\n },\n): { code: string; map: null } {\n const stem = basename(id, '.aihu')\n const args = ['--stdin', '--tag', options?.tag ?? stem, '--path', id]\n if (options?.sidecarOut) {\n args.push('--sidecar-out', options.sidecarOut)\n }\n // T6 (go-public demo) — thread the build target so a client bundle gets the\n // policy-free `@agent` dispatcher (and the per-instance registration the\n // capability bridge needs) instead of the server `__agentBinding`. Defaults to\n // the compiler's `universal` target when omitted (existing behaviour).\n if (options?.target) {\n args.push('--target', options.target)\n }\n const code = execFileSync(resolveBinPath(), args, {\n input: source,\n encoding: 'utf8',\n })\n return {\n code,\n map: null, // source maps deferred to v1 (OQ-C8)\n }\n}\n\n/**\n * Escape a CSS string for safe interpolation inside a JS template literal.\n * The Rust codegen places the authored `@style` body raw inside a backtick\n * literal, so it already assumes no backticks in `@style`. css-engine output\n * (theme tokens + utility rules) likewise never contains backticks, but we\n * escape `\\`, `` ` `` and `${` defensively so a future token value can't\n * break out of the literal.\n *\n * @internal\n */\nfunction _escapeForTemplateLiteral(css: string): string {\n return css.replace(/\\\\/g, '\\\\\\\\').replace(/`/g, '\\\\`').replace(/\\$\\{/g, '\\\\${')\n}\n\n/**\n * Fold css-engine-produced scoped CSS into a compiled `.aihu` module.\n *\n * The Rust codegen emits the authored `@style` block (when present) as:\n *\n * const __style__ = new CSSStyleSheet();\n * __style__.replaceSync(`<authored css>`);\n * defineElement('tag', defineComponent((ctx) => {\n * (ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];\n * return ...\n * }))\n *\n * css-engine's `compileSfc` output is the COMPLETE per-SFC stylesheet:\n * `:host` theme tokens, the variant-resolved utility-class rules, AND the\n * folded authored `@style` block (under an `authored @style` CSS comment).\n * So it is authoritative — we adopt it as the single shadow `<style>` and\n * the authored `@style` keeps emitting through it (acceptance: \"@style still\n * emits correctly alongside\").\n *\n * Two shapes are handled:\n *\n * 1. **SFC has an `@style` block** — the Rust codegen already declared\n * `__style__` with the raw `@style` body. We REPLACE that body with the\n * css-engine output (which already CONTAINS the `@style` block) so the\n * `@style` rules are not duplicated. The existing `adoptedStyleSheets`\n * assignment is reused unchanged.\n *\n * 2. **SFC has NO `@style` block** — there is no `__style__`. We inject a\n * fresh `__style__` declaration after the last import and an\n * `adoptedStyleSheets` assignment as the first statement of the setup\n * function. The compiler emits the setup param as `_ctx` in this case;\n * we rename it to `ctx` so the injected `ctx.host` reference resolves.\n *\n * Runs on the RAW compiled output BEFORE the island / HMR / auto-wiring\n * transforms so those passes operate on the folded module uniformly:\n * - The static-island shim calls `__aihu_setup__({ host: root, ... })`\n * where `root` is the shadow root, so `ctx.host` is valid there too.\n * - The HMR / defer passes only touch the `defineElement(...)` wrapper and\n * the runtime import; they do not disturb `__style__` or the setup body.\n *\n * No-ops (returns input unchanged) when `css` is empty/whitespace.\n *\n * @internal\n */\nexport function _foldCssEngineStyles(compiledCode: string, css: string): string {\n if (!css.trim()) return compiledCode\n const escaped = _escapeForTemplateLiteral(css)\n\n // Shape 1 — an authored @style block already declared __style__. css-engine\n // output already includes that @style block, so REPLACE the replaceSync body\n // (between the backticks) wholesale to avoid duplicating the @style rules.\n // The codegen emits `__style__.replaceSync(`<body>`);` as a single statement;\n // match the body non-greedily up to the closing backtick + paren.\n // biome-ignore lint/correctness/noEmptyCharacterClassInRegex: [^] matches any char incl. newlines\n const styleBodyRe = /(__style__\\.replaceSync\\(`)[^]*?(`\\);)/\n if (styleBodyRe.test(compiledCode)) {\n // Use a function replacer so any `$` in the CSS isn't read as a\n // replacement-pattern backreference.\n return compiledCode.replace(styleBodyRe, (_m, open: string, close: string) => {\n return `${open}${escaped}${close}`\n })\n }\n\n // Shape 2 — no @style block. Inject a fresh stylesheet + adoption.\n // Bail (no-op) if the expected defineComponent setup shape is absent.\n const setupRe = /defineComponent\\(\\s*\\((_ctx|ctx)\\)\\s*=>\\s*\\{/\n const m = setupRe.exec(compiledCode)\n if (m == null) return compiledCode\n\n // Inject the module-level stylesheet declaration after the last import line.\n const lines = compiledCode.split('\\n')\n let lastImportIdx = -1\n for (let i = lines.length - 1; i >= 0; i--) {\n const t = (lines[i] ?? '').trim()\n if (t.startsWith('import ') || t.startsWith('import{')) {\n lastImportIdx = i\n break\n }\n }\n const decl = `const __style__ = new CSSStyleSheet();\\n__style__.replaceSync(\\`${escaped}\\`);`\n if (lastImportIdx !== -1) {\n lines.splice(lastImportIdx + 1, 0, decl)\n } else {\n lines.unshift(decl)\n }\n let withDecl = lines.join('\\n')\n\n // Rename the setup param to `ctx` (codegen emits `_ctx` when no @style/ctx\n // usage) and inject the adoption as the first statement of the setup body.\n withDecl = withDecl.replace(\n /defineComponent\\(\\s*\\((?:_ctx|ctx)\\)\\s*=>\\s*\\{/,\n 'defineComponent((ctx) => {\\n (ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];',\n )\n return withDecl\n}\n\n/**\n * Virtual-module prefix used by the `shadowMode === 'none'` branch to route\n * per-SFC utility CSS through Vite's built-in CSS pipeline. The plugin\n * (`aihuCompilerPlugin`) implements `resolveId` + `load` for ids matching\n * `VIRTUAL_UTILITY_PREFIX + '<hash>.css'`, returning the stored CSS body so\n * Vite hoists it into the bundle CSS asset (`dist/assets/*.css`) — NOT into\n * `host.adoptedStyleSheets`, which is a no-op when there is no shadow root.\n *\n * The trailing `.css` extension is mandatory: Vite's built-in CSS plugin keys\n * off the extension to know it should run the CSS pipeline on the module.\n *\n * @internal\n */\nexport const VIRTUAL_UTILITY_PREFIX = '\\0virtual:aihu-utility/'\n\n/**\n * Stable short hash for keying the virtual-CSS module per source-SFC id.\n *\n * djb2-style; collisions are tolerable here because (a) each entry stores its\n * own CSS body, so a hash collision would only matter if two distinct SFCs\n * hashed to the same key AND were processed concurrently; (b) collisions are\n * recoverable — Vite would simply load the wrong CSS for one SFC; we still\n * keyed on the unhashed id internally to avoid that. The hash only appears in\n * the bundled asset URL.\n *\n * @internal\n */\nexport function _hashIdForUtilityCss(id: string): string {\n let h = 5381\n for (let i = 0; i < id.length; i++) {\n h = ((h * 33) ^ id.charCodeAt(i)) >>> 0\n }\n return h.toString(36)\n}\n\n/**\n * Bug 6 — `shadowMode === 'none'` branch.\n *\n * Routes utility CSS to Vite's CSS pipeline (which folds CSS imports into the\n * bundled `dist/assets/*.css` asset) instead of to `host.adoptedStyleSheets`\n * (a no-op on an element with no shadow root). Returns a prelude `import` that\n * the plugin's `resolveId` + `load` hooks resolve to the stored CSS body.\n *\n * The `__style__` shadow path is NOT invoked here — utility CSS for a\n * cascade-mode component MUST hit the global stylesheet, not a per-element\n * stylesheet that would be silently dropped by `HTMLElement`'s setter.\n *\n * Authored `@style` blocks still emit through the Rust codegen's `<style>`\n * node and are unaffected. (If a component opts into `shadowMode: 'none'` and\n * authors an `@style` block, the codegen still wires it through the\n * non-shadow path — that is the runtime's contract, not this hook's.)\n *\n * @internal\n */\nexport function _foldCssEngineStylesGlobal(\n compiledCode: string,\n css: string,\n id: string,\n): { code: string; virtualId: string } | null {\n if (!css.trim()) return null\n const hash = _hashIdForUtilityCss(id)\n const virtualId = `${VIRTUAL_UTILITY_PREFIX}${hash}.css`\n // Prepend the CSS import as a side-effect-only import so Vite's CSS plugin\n // hoists it into the bundle. We use the NULL-byte virtual id form\n // (Rollup/Vite convention for \"owned by this plugin\"); other plugins will\n // skip it. The compiler's transform returns this prepended code, which the\n // downstream esbuild/oxc strip leaves untouched (it's just an import).\n const prelude = `import ${JSON.stringify(virtualId)};\\n`\n return { code: prelude + compiledCode, virtualId }\n}\n\n// ─── v1.0.10a — compiler AST-export hook ─────────────────────────────────────\n//\n// Thin TS wrapper over the `aihu-compile --ast-json` flag. Returns the parsed\n// `.aihu` SFC AST in a stable, serializable shape consumed by the CSS engine's\n// AST scanner (`css-2-ast-scanner`). Mirrors the typed contract in\n// `docs/superpowers/specs/compiler-ast-export-hook.md` §4.\n\n/** Top-level AST export — one per .aihu SFC. */\nexport interface SfcAst {\n /** Resolved custom-element tag name (meta.name → route.name → file stem). */\n tag: string\n /** AST schema version — bumped on any breaking shape change (semver-tied). */\n astVersion: 1\n /** The @style block, if the SFC declared one. */\n style: SfcStyleBlock | null\n /** Parsed template tree. null when the SFC has no @template block. */\n template: SfcNode[] | null\n /** SFC-level metadata. */\n meta: SfcMeta\n}\n\nexport interface SfcStyleBlock {\n /** Verbatim CSS body of the @style block (braces stripped, $global token removed). */\n content: string\n /** 'scoped' (default) or 'global' (@style { $global ... }). */\n scope: 'scoped' | 'global'\n}\n\nexport interface SfcMeta {\n /** From @meta { name } / @route { name } / file stem — never null after resolution. */\n name: string\n}\n\n/** Discriminated union mirroring Rust `TemplateNode`. */\nexport type SfcNode =\n | { kind: 'element'; tag: string; attrs: SfcAttr[]; children: SfcNode[] }\n | { kind: 'macroElement'; name: string; attrs: SfcAttr[]; children: SfcNode[] }\n | { kind: 'text'; value: string }\n | { kind: 'interpolation'; expr: string }\n | { kind: 'ifBlock'; branches: Array<{ cond: string; body: SfcNode[] }> }\n | {\n kind: 'eachBlock'\n list: string\n item: string\n idx: string | null\n key: string | null\n body: SfcNode[]\n emptyBody: SfcNode[] | null\n }\n | { kind: 'htmlBlock'; expr: string }\n\n/** Discriminated union mirroring Rust `Attr` — the three class-forms key on `kind`. */\nexport type SfcAttr =\n | { kind: 'static'; name: string; value: string } // Form A\n | { kind: 'binding'; name: string; expr: string } // Form B\n | { kind: 'macro'; name: string; value: SfcMacroValue } // Form C (and on:/bind:/emit:/if/each/…)\n\nexport type SfcMacroValue =\n | { form: 'quoted'; value: string }\n | { form: 'curly'; expr: string }\n | { form: 'boolean' }\n\n/**\n * Parse a .aihu source string to its structured AST.\n *\n * Thin wrapper over the Rust binary (mirrors `transform()`): spawns\n * `aihu-compile --stdin --tag <stem> --ast-json`, feeds `source` on stdin, and\n * `JSON.parse`s stdout. `id` is optional and only used to derive the tag stem\n * and the `--path` arg (for `@route` C500 checks), identical to `transform()`.\n *\n * Throws on parse failure — the Rust binary exits non-zero and `execFileSync`\n * surfaces the diagnostic (same error path as `transform()`).\n */\nexport function compileToAst(source: string, id?: string): SfcAst {\n const stem = id ? basename(id, '.aihu') : 'Component'\n const args = ['--stdin', '--tag', stem, '--ast-json']\n if (id) {\n args.push('--path', id)\n }\n const json = execFileSync(resolveBinPath(), args, {\n input: source,\n encoding: 'utf8',\n })\n return JSON.parse(json) as SfcAst\n}\n\n/**\n * Structured `@route` metadata (the `.route.json` sidecar shape). All fields\n * optional — only what the SFC's `@route` block declares is present. `head` is\n * left opaque here (the router owns its shape).\n */\nexport interface RouteMeta {\n pattern?: string\n name?: string\n layout?: string\n middleware?: string[]\n ssr?: boolean\n params?: string[]\n head?: unknown\n}\n\n/**\n * Parse a `.aihu` source string and return its `@route` metadata, or `null`\n * when the SFC declares no `@route` block.\n *\n * Thin wrapper over the Rust binary (mirrors {@link compileToAst}): spawns\n * `aihu-compile --stdin --tag <stem> --route-json`, feeds `source` on stdin,\n * and `JSON.parse`s stdout. This is how build tools recover full route\n * metadata (`head`/`middleware`/`params`/`ssr`/`layout`) for the SPA build\n * path, where no `.route.json` sidecar is written to disk.\n *\n * Throws on parse failure (same error path as `transform()`/`compileToAst()`).\n */\nexport function compileRouteMeta(source: string, id?: string): RouteMeta | null {\n const stem = id ? basename(id, '.aihu') : 'Component'\n const args = ['--stdin', '--tag', stem, '--route-json']\n if (id) {\n args.push('--path', id)\n }\n const out = execFileSync(resolveBinPath(), args, {\n input: source,\n encoding: 'utf8',\n }).trim()\n if (out === '' || out === 'null') return null\n return JSON.parse(out) as RouteMeta\n}\n\n/**\n * Inject `_setMount(mount)` + `_setSignal(signal)` auto-wiring into a compiled\n * `.aihu` module. Adds the necessary symbols to existing imports and inserts\n * the boot calls right after the last `import` statement.\n *\n * @internal\n */\nexport function _injectAutoWiring(code: string): string {\n // 1. Add `mount` to the @aihu/arbor import (or create it).\n let result: string\n if (code.includes(\"from '@aihu/arbor'\")) {\n result = code.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/arbor'/,\n (_m: string, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('mount')) parts.push('mount')\n return `import { ${parts.join(', ')} } from '@aihu/arbor'`\n },\n )\n } else {\n result = `import { mount } from '@aihu/arbor'\\n${code}`\n }\n\n // 2. Add `signal` to the non-type @aihu/signals import (or create it).\n // Note: `import\\s+\\{` does NOT match `import type {` (the regex needs `{` immediately\n // after whitespace, whereas `import type {` has `type` in between). No negation guard\n // is needed — the replace callback below already skips `import type` lines.\n if (/import\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals'/.test(result)) {\n // There IS a value import from signals — add `signal` if missing.\n result = result.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/signals'/,\n (_m: string, imports: string) => {\n // Skip type-only imports\n if (_m.startsWith('import type')) return _m\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('signal')) parts.push('signal')\n return `import { ${parts.join(', ')} } from '@aihu/signals'`\n },\n )\n } else if (!/import.*from\\s*'@aihu\\/signals'/.test(result)) {\n // No signals import at all — insert after arbor import\n result = result.replace(\n /import\\s*\\{[^}]*\\}\\s*from\\s*'@aihu\\/arbor'/,\n (m: string) => `${m}\\nimport { signal } from '@aihu/signals'`,\n )\n }\n // If only `import type { Signal }` exists, insert value import after it\n else if (\n /import\\s+type\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals'/.test(result) &&\n !result.match(/import\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals'/)\n ) {\n result = result.replace(\n /(import\\s+type\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals')/,\n (_m: string, typeImport: string) => `${typeImport}\\nimport { signal } from '@aihu/signals'`,\n )\n }\n\n // 3. Add `_setMount`, `_setSignal` to the @aihu/runtime import.\n result = result.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/runtime'/,\n (_m: string, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('_setMount')) parts.push('_setMount')\n if (!parts.includes('_setSignal')) parts.push('_setSignal')\n return `import { ${parts.join(', ')} } from '@aihu/runtime'`\n },\n )\n\n // 4. Insert boot calls after the last `import` statement.\n const lines = result.split('\\n')\n let lastImportIdx = -1\n for (let i = lines.length - 1; i >= 0; i--) {\n const t = (lines[i] ?? '').trim()\n if (t.startsWith('import ') || t.startsWith('import{')) {\n lastImportIdx = i\n break\n }\n }\n if (lastImportIdx !== -1) {\n lines.splice(lastImportIdx + 1, 0, '_setMount(mount)', '_setSignal(signal)', '')\n result = lines.join('\\n')\n }\n\n return result\n}\n\n/**\n * Vite plugin that compiles .aihu files to TypeScript during build and dev.\n *\n * Use `enforce: 'pre'` so the hook fires before Vite/Rollup's built-in\n * parsers attempt to process the raw .aihu content as JavaScript.\n *\n * @example\n * // vite.config.ts\n * import { aihuCompilerPlugin } from '@aihu/compiler'\n * export default { plugins: [aihuCompilerPlugin()] }\n *\n * **Known Limitation — Bun + Rollup4 ESM incompatibility (v0):**\n *\n * `bun vite build` fails in the `fixtures/vite-counter` fixture with two\n * cascading errors:\n *\n * 1. **Missing devDependency:** `vite` is declared only as an optional\n * `peerDependency` in `packages/compiler/package.json`. Bun does not\n * install optional peers automatically, so `bun vite build` exits\n * immediately with `Cannot find package 'vite'`.\n *\n * 2. **Bun + Rollup4 bridge:** Even with Vite installed, Bun processes\n * `vite.config.ts` through its own internal bundler before handing off\n * to Rollup4. When `@aihu/compiler` is resolved from the workspace\n * symlink (`dist/index.js`), Bun's ESM loader evaluates the module at\n * config-load time. The subprocess call inside `transform()` depends on\n * the Rust binary being at `../bin/aihu-compile` relative to `dist/`\n * (written by the postinstall hook). In a dev workspace where postinstall\n * has not run, this path does not exist and `execFileSync` throws. Bun surfaces\n * the error as a config-load failure, not a per-file transform error,\n * causing the entire build to abort before any `.aihu` file is\n * processed.\n *\n * **Workaround (v0):** Use `bun run integrate.ts` directly from\n * `packages/compiler/fixtures/vite-counter/`. This script calls\n * `transform()` from `@aihu/compiler` without involving Vite or Rollup.\n * Preconditions: (1) `cargo build --release` in `packages/compiler/`,\n * (2) `bun install` at the repo root.\n *\n * **v1 resolution:** Add `vite` as a `devDependency` in\n * `packages/compiler/package.json`; add a WASM or pre-built binary\n * strategy so the Rust binary is bundled with the npm package and does not\n * require a separate `cargo build --release` step.\n */\n/**\n * Minimal structural type for the `@aihu/css-engine` module surface this\n * plugin uses. Declared locally so the compiler never type-imports the\n * css-engine package (which would create a compile-time edge against an\n * optional peer that may be absent).\n *\n * @internal\n */\ninterface CssEngineModule {\n compileSfc(source: string, id?: string): string\n}\n\n// Memoised resolution of the optional `@aihu/css-engine` peer. `undefined`\n// = not yet attempted; `null` = attempted and unavailable (no-op path);\n// a module object = available. The dynamic import is attempted once per\n// process — repeated absence does not re-pay the resolution cost.\nlet _cssEngine: CssEngineModule | null | undefined\n\n// Whether we've already surfaced a one-shot warning that css-engine resolved\n// but `compileSfc` threw (typically: native css-core binary unresolvable in\n// the consumer's install — e.g. lockfile pins the per-platform placeholder\n// version). The transform stays non-fatal, but going fully silent leaves users\n// chasing \"why did my utility classes never emit?\". One warn per process.\nlet _cssEngineWarned = false\n\n// The optional-peer module specifier, held in a VARIABLE so TypeScript never\n// statically resolves `@aihu/css-engine`'s declarations at typecheck time.\n// css-engine depends on @aihu/compiler for its AST, so the two form a\n// circular package relationship; under CI's frozen install + moon build\n// ordering, css-engine's `dist`/`.d.ts` are not guaranteed to exist when\n// `compiler:typecheck` runs. A literal `import('@aihu/css-engine')` makes the\n// compiler emit TS2307 in that window (the `as` cast affects the RESULT type\n// only, not whether TS attempts module resolution). Resolving through this\n// variable keeps the import fully dynamic — no compile-time edge on the peer.\nconst _CSS_ENGINE_SPECIFIER = '@aihu/css-engine'\n\n/**\n * Lazily resolve `@aihu/css-engine` and compile a `.aihu` source's utility\n * classes to scoped CSS. Returns `''` when css-engine is not installed\n * (the optional-peer no-op path) or when compilation fails for any reason —\n * a CSS-engine failure MUST NOT break an otherwise-valid `.aihu` build.\n *\n * Sets `process.env.SCRIBE_COMPILE_BIN` to this plugin's resolved compiler\n * binary before calling `compileSfc`: css-engine re-derives the SFC AST via\n * its own bundled copy of `compileToAst`, whose binary path is resolved\n * relative to the css-engine package — which does NOT ship the compiler\n * binary. Pointing it at our `binPath` guarantees the AST css-engine parses\n * is produced by the exact same compiler this build uses.\n *\n * @internal\n */\nasync function _maybeCompileUtilityCss(source: string, id: string): Promise<string> {\n if (_cssEngine === null) return ''\n // Ensure css-engine's bundled `compileToAst` spawns the SAME compiler\n // binary this plugin uses (it has no compiler binary of its own). Set\n // this BEFORE the dynamic import so that any module-load-time evaluation\n // of `process.env.SCRIBE_COMPILE_BIN` in css-engine's bundled dist (older\n // bundles capture this into a module-scope const at line 8 of\n // `packages/css-engine/dist/index.js`) sees the correct value. After Bug 6,\n // the source `compileToAst` resolves the bin lazily on each call, so once\n // css-engine is rebuilt this set-before-import is belt-and-braces.\n if (process.env.SCRIBE_COMPILE_BIN == null) {\n process.env.SCRIBE_COMPILE_BIN = resolveBinPath()\n }\n if (_cssEngine === undefined) {\n try {\n // Guarded, lazy, OPTIONAL — see the plugin transform for the rationale.\n // Importing via the `_CSS_ENGINE_SPECIFIER` variable (not a string\n // literal) keeps this fully dynamic: TS does NOT resolve the peer's\n // `.d.ts` at typecheck time, so `compiler:typecheck` passes even when\n // css-engine's `dist` has not been built (the CI build-order window).\n _cssEngine = (await import(_CSS_ENGINE_SPECIFIER)) as unknown as CssEngineModule\n } catch {\n _cssEngine = null\n return ''\n }\n }\n try {\n return _cssEngine.compileSfc(source, id)\n } catch (err) {\n // A css-engine compile failure is non-fatal: fall back to the no-op\n // path (utility classes don't emit) rather than aborting the build.\n // BUT — silently swallowing this means a user who clearly intends\n // css-engine to be active (the peer resolved) will never know their\n // utility classes are inert. Surface a one-shot warning with the\n // underlying error + an install/upgrade hint. Idempotent per process.\n if (!_cssEngineWarned) {\n _cssEngineWarned = true\n const msg = err instanceof Error ? err.message : String(err)\n console.warn(\n `[@aihu/compiler] @aihu/css-engine is installed but compileSfc() failed; ` +\n `utility classes will not emit. Original error: ${msg}\\n` +\n `Hint: ensure the native css-core binary is installed ` +\n `(install/upgrade @aihu/css-engine + its per-platform optional dep, ` +\n `or run \\`cargo build --release -p aihu-css-core\\` in a dev clone).`,\n )\n }\n return ''\n }\n}\n\nexport function aihuCompilerPlugin(options?: AihuCompilerPluginOptions): VitePlugin {\n const islandsEnabled = options?.islands !== false\n const shadowMode = options?.shadowMode\n const target = options?.target\n const layoutsDir = options?.layoutsDir ?? 'src/layouts'\n\n // Bug 6 — per-instance store of virtual utility-CSS modules. Keyed by the\n // full virtual id (NUL-prefixed). Populated by the transform hook when\n // `shadowMode === 'none'` produces utility CSS; drained by the `load` hook\n // when Vite's CSS pipeline asks for the module body. Lives on the plugin\n // instance so multiple `aihuCompilerPlugin()` calls in the same build don't\n // alias each other's css.\n const utilityCssStore = new Map<string, string>()\n\n return {\n name: 'aihu-compiler',\n enforce: 'pre',\n resolveId(source) {\n // Own all `\\0virtual:aihu-utility/<hash>.css` ids so Vite's resolver\n // doesn't try to find them on disk. Returning the id verbatim is the\n // Rollup convention for \"I'll handle the load.\"\n if (source.startsWith(VIRTUAL_UTILITY_PREFIX)) return source\n return null\n },\n load(id) {\n if (!id.startsWith(VIRTUAL_UTILITY_PREFIX)) return null\n // Vite's CSS pipeline runs on the returned source because the id ends\n // in `.css` — it parses, minifies (in build), and hoists into a CSS\n // asset chunk that lands in `dist/assets/<name>-<hash>.css`.\n return utilityCssStore.get(id) ?? null\n },\n transform(code, id) {\n // Strip Vite query strings (e.g. `?import`, `?t=...`) before checking the extension.\n const rawId = id.split('?')[0]!\n if (!rawId.endsWith('.aihu')) return\n return (async () => {\n // B3b — write per-SFC `.aihu.ts` sidecar adjacent to source so\n // `tsc --noEmit` over `**/*.aihu.ts` type-checks template\n // expressions end-to-end (Architect spec §7 path (i)).\n const sidecarOut = `${rawId}.ts`\n // Layout SFCs (under the layouts dir) compile in layout mode: a\n // namespaced `aihu-layout-<stem>` tag + a passive <$outlet> marker.\n const isLayout = _isLayoutFile(rawId, layoutsDir)\n const layoutTag = isLayout ? _layoutTag(basename(rawId, '.aihu')) : undefined\n const tOpts = {\n sidecarOut,\n ...(target ? { target } : {}),\n ...(layoutTag ? { tag: layoutTag } : {}),\n }\n const result = transform(code, rawId, tOpts)\n // §9.4 per-file shadow override: the Rust `$shadow` macro emits a leading\n // `// @aihu:shadow <mode>` marker; it wins over the plugin's global\n // shadowMode and drives BOTH _injectShadowMode and the css fold branch.\n const perFileShadow = /^\\/\\/ @aihu:shadow (open|closed|none)\\b/m.exec(result.code)?.[1] as\n | 'open'\n | 'closed'\n | 'none'\n | undefined\n const effectiveShadow = perFileShadow ?? shadowMode\n let compiled =\n effectiveShadow != null ? _injectShadowMode(result.code, effectiveShadow) : result.code\n // Light-DOM: the authored `@style` block compiled to a per-instance\n // `host.adoptedStyleSheets` assignment, but a light-DOM host has no\n // shadow root so that setter is a no-op. Redirect the module-level\n // sheet to `document.adoptedStyleSheets` (idempotent) so authored recipe\n // CSS reaches the global cascade alongside the css-engine utility CSS.\n if (effectiveShadow === 'none') compiled = _globalizeAuthoredStyle(compiled)\n if (isLayout) compiled = _passivizeOutlet(compiled)\n\n // ── css-engine hook (optional, lazy, no circular dep) ──────────────\n // @aihu/css-engine depends on @aihu/compiler (for its AST), so the\n // compiler MUST NOT hard-depend on it. It is declared an OPTIONAL\n // peerDependency and pulled in ONLY via this guarded dynamic import:\n // when present, we compile the SFC's utility classes to scoped CSS\n // and fold it into the component's shadow `<style>`; when absent the\n // import throws and we no-op (utility classes simply don't emit —\n // the pre-hook behaviour). This keeps css-engine an opt-in enhancement\n // with zero dependency cycle.\n const utilityCss = await _maybeCompileUtilityCss(code, rawId)\n if (utilityCss) {\n if (effectiveShadow === 'none') {\n // Bug 6 — no shadow root → `host.adoptedStyleSheets` is a no-op.\n // Route utility CSS through Vite's CSS pipeline via a virtual\n // `.css` import so it lands in `dist/assets/*.css` and reaches the\n // global cascade. The authored `@style` block (if any) still\n // emits via the Rust codegen's normal path and is unaffected.\n const folded = _foldCssEngineStylesGlobal(compiled, utilityCss, rawId)\n if (folded) {\n utilityCssStore.set(folded.virtualId, utilityCss)\n compiled = folded.code\n }\n } else {\n // `shadowMode: 'open' | 'closed'` (default): fold into the\n // per-component `CSSStyleSheet` adopted by the shadow root.\n compiled = _foldCssEngineStyles(compiled, utilityCss)\n }\n }\n\n const elementTag = _extractElementTag(compiled)\n\n let out: string\n\n // §9.4 — a base-extending recipe (`defineComponent({ base: X, ... })`)\n // MUST take the full defineComponent/defineElement path: the static\n // island shim inlines `class extends HTMLElement` and cannot honor a\n // base class. Force-classify it interactive regardless of signal usage.\n // biome-ignore lint/correctness/noEmptyCharacterClassInRegex: [^] is valid JS — matches any char including newlines\n const hasBase = /defineComponent\\(\\s*\\{[^]*?\\bbase\\s*:/.test(compiled)\n\n // Plan 3.3 — static-island fast path. Bypasses HMR injection because\n // a component with no signals has no setup state to hot-replace.\n // Static islands strip @aihu/runtime entirely — do NOT inject auto-wiring\n // (it would reference _setMount/_setSignal as undefined identifiers).\n if (\n islandsEnabled &&\n elementTag !== null &&\n !hasBase &&\n _classifyIsland(compiled) === 'static'\n ) {\n out = _buildStaticIsland(compiled, elementTag)\n } else if (elementTag !== null) {\n // Inject HMR instrumentation. The injected block is gated on\n // `typeof __DEV__ !== 'undefined' && __DEV__` so production\n // bundlers dead-code-eliminate it when they set __DEV__ = false.\n out = _buildHmrCode(compiled, elementTag)\n // Plan 3.3 — interactive islands also gain `defer` attribute\n // support so individual instances can opt into lazy hydration.\n out = _buildDeferredHydration(out, elementTag)\n // Inject auto-wiring so consumers don't need a manual main.ts bootstrap.\n out = _injectAutoWiring(out)\n } else {\n out = compiled\n // Inject auto-wiring so consumers don't need a manual main.ts bootstrap.\n out = _injectAutoWiring(out)\n }\n\n // The Rust compiler emits TypeScript (type casts, import type, etc.) and\n // the injected HMR / defer helpers also contain TS generics and casts.\n // Vite does NOT re-run its TS-strip step when a plugin returns code for a\n // non-.ts ID, so we must strip types ourselves before returning.\n //\n // Priority: always try transformWithEsbuild first — it strips types to\n // plain JS in both Vite 5 (via esbuild) and Vite 8 (deprecated wrapper).\n // Using moduleType:'ts' only as a last resort because `import('vite')`\n // resolves to the root node_modules vite (which may be v8 even when a\n // consumer project runs v5), causing v5's Rollup to receive raw TypeScript\n // and fail on import-type / as-casts.\n try {\n const vite = await import('vite')\n if ('transformWithEsbuild' in vite && typeof vite.transformWithEsbuild === 'function') {\n const stripped = await vite.transformWithEsbuild(out, 'component.ts', {\n target: 'esnext',\n sourcemap: false,\n })\n return { code: stripped.code, map: null }\n }\n // Fallback for future Vite versions where esbuild is fully removed:\n // return TS and let Rolldown strip types natively.\n // biome-ignore lint/suspicious/noExplicitAny: moduleType is rolldown API\n return { code: out, moduleType: 'ts', map: null } as any\n } catch {\n // If running outside Vite (e.g. tests, standalone transform), return as-is.\n return { code: out, map: null }\n }\n })()\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../js/index.ts"],"sourcesContent":["/**\n * @aihu/compiler — TypeScript wrapper around the aihu-compile Rust binary.\n *\n * Exports:\n * transform(source, id) — compile a single .aihu file to TypeScript\n * aihuCompilerPlugin() — Vite plugin that wires transform() into the build\n */\nimport { execFileSync } from 'node:child_process'\nimport { basename } from 'node:path'\nimport { resolveCompilerBinary } from './resolve-binary.ts'\n\n// Binary resolution: env var override, then the per-platform optionalDependency\n// package (`@aihu/compiler-<platform>`) with a workspace `target/` dev fallback —\n// see js/resolve-binary.ts (a clone of css-engine's resolver). The published\n// @aihu/compiler tarball ships only the JS shim (bin/aihu-compile.mjs); the\n// native binary arrives via the optionalDependency packages, so there is no\n// `../bin/aihu-compile` relative path anymore.\n//\n// Bug 6 fix — resolveBinPath() is CALL-TIME, not module-load-time. The Vite\n// plugin's `_maybeCompileUtilityCss` sets `process.env.SCRIBE_COMPILE_BIN` so\n// that css-engine's bundled copy of `compileToAst` spawns THIS compiler's\n// binary. Prior to this fix `binPath` was a module-scope const captured at\n// import time, so the env-var assignment was always too late and `compileSfc`\n// failed with ENOENT. Re-reading on every call is essentially free (an env\n// lookup, then a memoized resolve) and makes the SCRIBE_COMPILE_BIN handshake\n// actually work.\nfunction resolveBinPath(): string {\n return process.env.SCRIBE_COMPILE_BIN ?? resolveCompilerBinary()\n}\n\n// Minimal VitePlugin interface — avoids importing from 'vite' at compile time.\n// Structurally compatible with Vite's Plugin type.\ninterface VitePlugin {\n readonly name: string\n enforce?: 'pre' | 'post'\n resolveId?: (\n source: string,\n importer?: string,\n ) => string | null | undefined | Promise<string | null | undefined>\n load?: (id: string) => string | null | undefined | Promise<string | null | undefined>\n transform?: (\n code: string,\n id: string,\n ) => Promise<{ code: string; map: null }> | { code: string; map: null } | null | undefined\n}\n\n/**\n * Options for `aihuCompilerPlugin()` (Plan 3.3 — Islands).\n */\nexport interface AihuCompilerPluginOptions {\n /**\n * When `true` (default), components classified as `'static'` by\n * `_classifyIsland()` are emitted with a minimal HTML-only registration\n * shim that ships **zero** `@aihu/runtime` and `@aihu/signals` JS to\n * the browser. Components classified as `'interactive'` retain the\n * full runtime path.\n *\n * Setting `islands: false` opts every component back into the unified\n * runtime path (Plan 3.2 baseline behaviour).\n */\n islands?: boolean\n\n /**\n * Project-wide shadow-DOM mode applied to every `.aihu` SFC compiled\n * by this plugin instance. When set, the plugin post-processes the\n * compiled JS to inject `, { shadowMode: '<mode>' }` as the third arg\n * to the emitted `defineElement(tag, defineComponent(...))` call.\n *\n * - `'open'` — default browser behaviour (shadow root, externally readable).\n * - `'closed'` — shadow root, externally hidden.\n * - `'none'` — **no shadow root.** The component mounts into its own\n * element. Required for global utility-class CSS frameworks\n * like Tailwind, UnoCSS, Pico that rely on the cascade.\n *\n * Per-component override is not yet supported via SFC syntax (post-v1).\n * For per-component control today, hand-author the component with\n * `defineElement(tag, Ctor, { shadowMode: '...' })`.\n */\n shadowMode?: 'open' | 'closed' | 'none'\n\n /**\n * Build target threaded to the compiler binary (`--target`). Defaults to the\n * compiler's `universal` target (current behaviour). Set to `'client'` for a\n * browser bundle that must NOT ship the server `__agentBinding` (policy) and\n * instead gets the policy-free `@agent` opaque-ID dispatcher + the per-instance\n * `_registerAgentDispatcher` wiring the capability bridge reads after mount.\n * See `examples/agent-driven-demo`.\n */\n target?: 'client' | 'server' | 'universal'\n\n /**\n * Directory (relative to the project root) holding layout SFCs. Default:\n * `'src/layouts'`. Files under this directory are compiled in **layout mode**:\n * their custom element is registered under the namespaced tag\n * `aihu-layout-<stem>` (a layout stem like `app` is not a valid custom-element\n * name on its own), and their `<$outlet>` lowers to a **passive**\n * `data-aihu-outlet` marker rather than the reactive route-driven boundary —\n * because `@aihu/app`'s client renderer fills the marker imperatively and the\n * reactive boundary would otherwise clear it on mount.\n *\n * Kept in sync with `@aihu/router`'s `layoutTagFor()` (`virtual:aihu-layouts`).\n */\n layoutsDir?: string\n}\n\n/**\n * Inject `{ shadowMode: '...' }` as the third argument to the emitted\n * `defineElement('tag', defineComponent(...))` call. The compiler emits\n * exactly two arguments today; this rewrites the closing of the\n * defineElement call to include the options object. Idempotent — leaves\n * code untouched when the closer is not in the expected shape.\n *\n * @internal\n */\nexport function _injectShadowMode(code: string, mode: 'open' | 'closed' | 'none'): string {\n // Match the trailing `))` that closes `defineElement(tag, defineComponent(setup))`.\n // The compiler always emits this exact two-paren close as the final tokens of\n // the defineElement call — we anchor on it and append the options object.\n // biome-ignore lint/correctness/noEmptyCharacterClassInRegex: [^] is valid JS — matches any char including newlines\n const re = /(defineElement\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*defineComponent\\([^]*\\))\\s*\\)/\n const replaced = code.replace(re, (_m, inner: string) => `${inner}, { shadowMode: '${mode}' })`)\n return replaced\n}\n\n/**\n * Light-DOM (`shadowMode:'none'`) recipes: redirect the authored `@style`\n * block's per-instance `host.adoptedStyleSheets = [__style__]` assignment to\n * `document.adoptedStyleSheets` so the recipe's class-scoped CSS reaches the\n * global cascade (a light-DOM host has no shadow root, making the original\n * setter a silent no-op). The module-level `__style__` is shared across\n * instances; the `includes` guard keeps the global adoption idempotent.\n *\n * @internal\n */\nexport function _globalizeAuthoredStyle(code: string): string {\n // The Rust codegen emits exactly: `(ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];`\n const re = /\\(ctx\\.host as ShadowRoot\\)\\.adoptedStyleSheets\\s*=\\s*\\[__style__\\];?/\n return code.replace(\n re,\n 'if (!document.adoptedStyleSheets.includes(__style__)) document.adoptedStyleSheets = [...document.adoptedStyleSheets, __style__];',\n )\n}\n\n/**\n * Classify the compiled output of a single `.aihu` module as either a\n * **static** island (no reactive state — purely declarative DOM) or an\n * **interactive** island (uses the signals reactivity system).\n *\n * The heuristic is intentionally conservative: any source-level reference\n * to a primitive that requires the `defineComponent` owner context flips\n * the file to `'interactive'`. False positives (e.g. a string literal\n * containing `signal(`) are tolerable — they only forfeit the static-island\n * optimisation. False negatives are forbidden: a static-classified file\n * MUST NOT depend on the signals runtime at execution time.\n *\n * Owner-requiring primitives covered:\n * - `signal(`, `computed(`, `effect(`, `setSignal(` (signals runtime)\n * - `onMount(`, `onCleanup(` (lifecycle hooks — throw `no owner` outside\n * `defineComponent` because they push into the active owner's mount/\n * cleanup queues)\n *\n * Plan 3.3 / acceptance criterion 1.\n *\n * @internal\n */\nexport function _classifyIsland(compiledCode: string): 'static' | 'interactive' {\n // Match call sites of the reactive + lifecycle primitives. Use word-boundary\n // anchors so identifiers like `mySignal(` or `__effect(` do not trip the\n // heuristic. The `(` is required so that bare imports of the names in an\n // unused `import { signal }` line do not flip an otherwise-static module.\n return /\\b(?:signal|computed|effect|setSignal|onMount|onCleanup)\\s*\\(/.test(compiledCode)\n ? 'interactive'\n : 'static'\n}\n\n/**\n * Extract the custom element tag name from compiler-emitted code.\n * The compiler always emits `defineElement('tag-name', ...)` as the\n * first call — pull the first string literal argument.\n * Returns `null` if no `defineElement` call is found.\n * @internal\n */\nfunction _extractElementTag(code: string): string | null {\n const m = /defineElement\\(\\s*['\"]([^'\"]+)['\"]/m.exec(code)\n return m ? (m[1] ?? null) : null\n}\n\n/**\n * Is `rawId` a layout SFC (a `.aihu` file under the configured layouts dir)?\n * Root-independent: matches the `<layoutsDir>/` segment anywhere in the path,\n * which is sufficient because the layouts dir is a project-relative convention.\n * @internal\n */\nexport function _isLayoutFile(rawId: string, layoutsDir: string): boolean {\n const ld = layoutsDir\n .replace(/\\\\/g, '/')\n .replace(/^\\.?\\//, '')\n .replace(/\\/+$/, '')\n if (!ld) return false\n return rawId.replace(/\\\\/g, '/').includes(`/${ld}/`)\n}\n\n/**\n * Layout custom-element tag for a filename stem. MUST match\n * `@aihu/router`'s `layoutTagFor()` so the generated `virtual:aihu-layouts`\n * map and the registered element agree on the tag.\n * @internal\n */\nexport function _layoutTag(stem: string): string {\n return `aihu-layout-${stem.toLowerCase()}`\n}\n\n/**\n * Collapse the reactive `<$outlet>` boundary the Rust codegen emits into a\n * passive `data-aihu-outlet` marker. Layout SFCs are rendered by `@aihu/app`'s\n * imperative client renderer, which fills the marker itself; the default\n * boundary's mount-time `effect()` reads `useRoute()` (null under the imperative\n * path) and clears the marker, which would wipe the page the renderer inserts.\n *\n * Anchors on the exact `const createOutletBoundary = () => { … return host; };`\n * block the codegen emits (`packages/compiler/src/codegen/emit.rs`). No-op when\n * the layout declares no `<$outlet>`.\n * @internal\n */\nexport function _passivizeOutlet(code: string): string {\n return code.replace(\n /const createOutletBoundary = \\(\\) => \\{[\\s\\S]*?return host;\\s*\\n\\};/,\n `const createOutletBoundary = () => branch('div', { 'data-aihu-outlet': '' }, []);`,\n )\n}\n\n/**\n * Instrument a compiled `.aihu` module with HMR support.\n *\n * The compiler always emits:\n *\n * import { defineComponent, defineElement } from '@aihu/runtime'\n * defineElement('tag', defineComponent((_ctx) => { ... }))\n *\n * This function:\n *\n * 1. Adds `_hmrReplace` to the `@aihu/runtime` import.\n * 2. Prepends a module-level slot variable `__aihu_setup__`.\n * 3. Rewrites the single `defineComponent(` call so the setup function\n * is captured via an assignment expression:\n * `defineComponent(__aihu_setup__ = ` (valid JS; assignment has\n * lower precedence than arrow fn, so `defineComponent` still\n * receives the function as its argument).\n * 4. Appends `export { __aihu_setup__ as default }` so that Vite's\n * `import.meta.hot.accept` callback receives the new setup via\n * `newModule.default` on hot reload.\n * 5. Appends the `import.meta.hot.accept` block, gated on `__DEV__`.\n *\n * The `__DEV__` guard ensures production bundlers (where they replace\n * `__DEV__` with `false`) dead-code-eliminate the entire HMR block.\n *\n * @internal\n */\nfunction _buildHmrCode(compiledCode: string, elementTag: string): string {\n // Step 1 — add _hmrReplace to the @aihu/runtime import.\n const withImport = compiledCode.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/runtime'/,\n (_m, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('_hmrReplace')) parts.push('_hmrReplace')\n return `import { ${parts.join(', ')} } from '@aihu/runtime'`\n },\n )\n\n // Step 2+3 — prepend slot variable and rewrite the defineComponent call.\n // Compiler emits exactly one `defineComponent(` followed by a function expr.\n // Rewrite: defineComponent(fn) → defineComponent(__aihu_setup__ = fn)\n // Assignment expression evaluates to `fn`, so defineComponent still\n // receives the setup function as its first argument unchanged.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const preamble = `let __aihu_setup__: ((ctx: any) => any) | undefined\\n`\n\n const patchedBody = withImport.replace(/\\bdefineComponent\\(/, 'defineComponent(__aihu_setup__ = ')\n\n const tag = JSON.stringify(elementTag)\n // Step 4+5 — postamble with default export and HMR acceptance.\n const postamble = `\nexport { __aihu_setup__ as default }\n\nif (typeof __DEV__ !== 'undefined' && __DEV__ && import.meta.hot) {\n import.meta.hot.accept((newModule) => {\n if (!newModule) return\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const newSetup = (newModule as any)['default']\n if (typeof newSetup !== 'function') return\n document.querySelectorAll(${tag}).forEach((el) => {\n _hmrReplace(el as HTMLElement, newSetup)\n })\n })\n}\n`\n\n return preamble + patchedBody + postamble\n}\n\n/**\n * Rewrite an interactive-island module so its `connectedCallback` waits\n * for the element to scroll into view before mounting. Plan 3.3 — applied\n * only when the consumer adds `defer` to the custom element tag (e.g.\n * `<my-counter defer>`); the runtime helper checks the attribute and\n * either mounts immediately or registers an `IntersectionObserver`.\n *\n * Implementation: the helper is added as a `_hydrateOnVisible` import\n * from `@aihu/runtime`, and the compiler-emitted `defineElement(...)`\n * call is wrapped in a `defineElement` that intercepts `connectedCallback`\n * to honour the `defer` attribute.\n *\n * The whole indirection is tree-shaken when no `.aihu` module reaches\n * this branch, because `_hydrateOnVisible` is exported from its own\n * sibling module inside `@aihu/runtime`.\n *\n * @internal\n */\nexport function _buildDeferredHydration(compiledCode: string, elementTag: string): string {\n // Add _hydrateOnVisible to the @aihu/runtime import.\n const withImport = compiledCode.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/runtime'/,\n (_m, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('_hydrateOnVisible')) parts.push('_hydrateOnVisible')\n return `import { ${parts.join(', ')} } from '@aihu/runtime'`\n },\n )\n\n // Wrap the class returned by defineComponent BEFORE defineElement\n // consumes it. The HTML spec caches lifecycle callbacks at\n // customElements.define() time, so we MUST mutate the prototype\n // before that call — not after. We accomplish this with a synchronous\n // helper invoked between defineComponent and defineElement.\n //\n // Source pattern (compiler-emitted):\n // defineElement('tag', defineComponent((_ctx) => { ... }))\n //\n // After this rewrite:\n // defineElement('tag', __aihu_wrap_defer__(defineComponent((_ctx) => { ... })))\n //\n // …with __aihu_wrap_defer__ defined in the appended preamble.\n const patched = withImport.replace(\n /defineElement\\(\\s*('[^']+'|\"[^\"]+\")\\s*,\\s*defineComponent\\(/,\n (_m, tagLit: string) => `defineElement(${tagLit}, __aihu_wrap_defer__(defineComponent(`,\n )\n // Match the closing `))` of the defineElement call. The HMR pass may\n // have inserted `__aihu_setup__ = ` before the inner function, but\n // the trailing `))` shape is unchanged. Replace exactly one occurrence\n // by anchoring on end-of-string trim; bail if the shape does not match.\n if (patched === withImport) {\n // The expected `defineElement(<tag>, defineComponent(` shape was not\n // present (e.g. compiler output changed). Skip defer wrapping rather\n // than emit broken code.\n return compiledCode\n }\n // Add a trailing `)` to balance the extra `(` from __aihu_wrap_defer__.\n // Source shape after _buildHmrCode is:\n // defineElement('tag', defineComponent(__aihu_setup__ = (_ctx) => {...}))\n // export { __aihu_setup__ as default }\n // if (typeof __DEV__ !== ...) { ... }\n // We must close BEFORE the export line. Match the first `))` followed\n // by a newline and `export` (or end-of-string for the unwrapped case).\n let balanced = patched.replace(/\\)\\s*\\)\\s*\\nexport\\s/, ')))\\nexport ')\n if (balanced === patched) {\n // No HMR postamble — the `))` is at end-of-string.\n balanced = patched.replace(/\\)\\s*\\)\\s*$/, ')))\\n')\n }\n if (balanced === patched) {\n // Could not find the matching `))` — bail out.\n return compiledCode\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const helper = `\n// Plan 3.3 (Islands) — defer attribute support. Wraps the constructor\n// returned by defineComponent so instances bearing the \\`defer\\` attribute\n// hydrate lazily via IntersectionObserver. Bare instances retain the\n// eager Plan 3.2 hydration path.\nfunction __aihu_wrap_defer__<T extends typeof HTMLElement>(Ctor: T): T {\n const orig = (Ctor.prototype as unknown as { connectedCallback?: () => void }).connectedCallback\n if (typeof orig !== 'function') return Ctor\n ;(Ctor.prototype as unknown as { connectedCallback: () => void }).connectedCallback = function (this: HTMLElement) {\n if (this.hasAttribute('defer')) {\n _hydrateOnVisible(this, () => orig.call(this))\n } else {\n orig.call(this)\n }\n }\n return Ctor\n}\n`\n void elementTag\n return helper + balanced\n}\n\n/**\n * Build a static-island shim for a compiled module.\n *\n * The compiled module emitted by the Rust codegen has the shape:\n *\n * import { branch, leaf, slot } from '@aihu/arbor'\n * import { defineComponent, defineElement } from '@aihu/runtime'\n * defineElement('tag', defineComponent((_ctx) => { return <tree> }))\n *\n * For a static island we know `<tree>` contains no `signal(`/`computed(`\n * calls. We can therefore:\n *\n * 1. Drop the `@aihu/runtime` import (saves ~600 B gz of defineComponent\n * + defineElement + bootstrap glue).\n * 2. Replace `defineElement(tag, defineComponent(setup))` with a tiny\n * inline class that mounts the tree directly via `mount()` (which the\n * arbor barrel already exports).\n * 3. Tag the file with a `// SCRIBE_STATIC_ISLAND` comment so consumers\n * can audit which routes shipped zero-JS-runtime.\n *\n * Falls back to the original code if the regex shape does not match\n * (defensive: a future compiler change must opt back into static-island\n * emission explicitly rather than silently break).\n *\n * @internal\n */\nexport function _buildStaticIsland(compiledCode: string, elementTag: string): string {\n // Confirm the shape we expect: a single defineElement(...) call wrapping\n // a single defineComponent(...) call. Bail out otherwise.\n const callRe = /defineElement\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*defineComponent\\(/\n if (!callRe.test(compiledCode)) return compiledCode\n\n // Strip the `@aihu/runtime` import line entirely — static islands\n // don't reference defineComponent/defineElement after the rewrite.\n const withoutRuntimeImport = compiledCode.replace(\n /^\\s*import\\s*\\{[^}]*\\}\\s*from\\s*'@aihu\\/runtime'\\s*;?\\s*$/m,\n '',\n )\n\n // Ensure `mount` is imported from @aihu/arbor (it already exposes\n // branch/leaf/slot, so we just append `mount` to the existing list).\n const withArborMount = withoutRuntimeImport.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/arbor'/,\n (_m, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('mount')) parts.push('mount')\n return `import { ${parts.join(', ')} } from '@aihu/arbor'`\n },\n )\n\n // Replace `defineElement('tag', defineComponent((_ctx) => { ... }))`\n // with an inline `customElements.define` whose connectedCallback mounts\n // the static tree. The setup function is captured verbatim by replacing\n // the wrapping calls with anonymous-IIFE bookends.\n const tagJson = JSON.stringify(elementTag)\n const rewritten = withArborMount\n .replace(\n /defineElement\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*defineComponent\\(/,\n `customElements.define(${tagJson}, class extends HTMLElement {\\n connectedCallback() {\\n const root = this.attachShadow({ mode: 'open' })\\n const __aihu_setup__ = (`,\n )\n .replace(\n /\\)\\s*\\)\\s*$/,\n `)\\n mount(__aihu_setup__({ host: root, element: this }), root)\\n }\\n})\\n`,\n )\n\n return `// SCRIBE_STATIC_ISLAND — zero @aihu/runtime references\\n${rewritten}`\n}\n\n/**\n * Compile a .aihu source string to TypeScript.\n * map is null — source maps are deferred to v1 (OQ-C8)\n *\n * B3b — when `sidecarOut` is provided, also writes the per-SFC `.aihu.ts`\n * sidecar at that path. Callers (e.g. the Vite plugin) typically pass\n * `<source-id>.ts` so `tsc --noEmit` discovers per-SFC template expressions.\n */\nexport function transform(\n source: string,\n id: string,\n options?: {\n sidecarOut?: string\n target?: 'client' | 'server' | 'universal'\n /** Override the registered custom-element tag (default: file stem). Used for layouts. */\n tag?: string\n },\n): { code: string; map: null } {\n const stem = basename(id, '.aihu')\n const args = ['--stdin', '--tag', options?.tag ?? stem, '--path', id]\n if (options?.sidecarOut) {\n args.push('--sidecar-out', options.sidecarOut)\n }\n // T6 (go-public demo) — thread the build target so a client bundle gets the\n // policy-free `@agent` dispatcher (and the per-instance registration the\n // capability bridge needs) instead of the server `__agentBinding`. Defaults to\n // the compiler's `universal` target when omitted (existing behaviour).\n if (options?.target) {\n args.push('--target', options.target)\n }\n const code = execFileSync(resolveBinPath(), args, {\n input: source,\n encoding: 'utf8',\n })\n return {\n code,\n map: null, // source maps deferred to v1 (OQ-C8)\n }\n}\n\n/**\n * Escape a CSS string for safe interpolation inside a JS template literal.\n * The Rust codegen places the authored `@style` body raw inside a backtick\n * literal, so it already assumes no backticks in `@style`. css-engine output\n * (theme tokens + utility rules) likewise never contains backticks, but we\n * escape `\\`, `` ` `` and `${` defensively so a future token value can't\n * break out of the literal.\n *\n * @internal\n */\nfunction _escapeForTemplateLiteral(css: string): string {\n return css.replace(/\\\\/g, '\\\\\\\\').replace(/`/g, '\\\\`').replace(/\\$\\{/g, '\\\\${')\n}\n\n/**\n * Fold css-engine-produced scoped CSS into a compiled `.aihu` module.\n *\n * The Rust codegen emits the authored `@style` block (when present) as:\n *\n * const __style__ = new CSSStyleSheet();\n * __style__.replaceSync(`<authored css>`);\n * defineElement('tag', defineComponent((ctx) => {\n * (ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];\n * return ...\n * }))\n *\n * css-engine's `compileSfc` output is the COMPLETE per-SFC stylesheet:\n * `:host` theme tokens, the variant-resolved utility-class rules, AND the\n * folded authored `@style` block (under an `authored @style` CSS comment).\n * So it is authoritative — we adopt it as the single shadow `<style>` and\n * the authored `@style` keeps emitting through it (acceptance: \"@style still\n * emits correctly alongside\").\n *\n * Two shapes are handled:\n *\n * 1. **SFC has an `@style` block** — the Rust codegen already declared\n * `__style__` with the raw `@style` body. We REPLACE that body with the\n * css-engine output (which already CONTAINS the `@style` block) so the\n * `@style` rules are not duplicated. The existing `adoptedStyleSheets`\n * assignment is reused unchanged.\n *\n * 2. **SFC has NO `@style` block** — there is no `__style__`. We inject a\n * fresh `__style__` declaration after the last import and an\n * `adoptedStyleSheets` assignment as the first statement of the setup\n * function. The compiler emits the setup param as `_ctx` in this case;\n * we rename it to `ctx` so the injected `ctx.host` reference resolves.\n *\n * Runs on the RAW compiled output BEFORE the island / HMR / auto-wiring\n * transforms so those passes operate on the folded module uniformly:\n * - The static-island shim calls `__aihu_setup__({ host: root, ... })`\n * where `root` is the shadow root, so `ctx.host` is valid there too.\n * - The HMR / defer passes only touch the `defineElement(...)` wrapper and\n * the runtime import; they do not disturb `__style__` or the setup body.\n *\n * No-ops (returns input unchanged) when `css` is empty/whitespace.\n *\n * @internal\n */\nexport function _foldCssEngineStyles(compiledCode: string, css: string): string {\n if (!css.trim()) return compiledCode\n const escaped = _escapeForTemplateLiteral(css)\n\n // Shape 1 — an authored @style block already declared __style__. css-engine\n // output already includes that @style block, so REPLACE the replaceSync body\n // (between the backticks) wholesale to avoid duplicating the @style rules.\n // The codegen emits `__style__.replaceSync(`<body>`);` as a single statement;\n // match the body non-greedily up to the closing backtick + paren.\n // biome-ignore lint/correctness/noEmptyCharacterClassInRegex: [^] matches any char incl. newlines\n const styleBodyRe = /(__style__\\.replaceSync\\(`)[^]*?(`\\);)/\n if (styleBodyRe.test(compiledCode)) {\n // Use a function replacer so any `$` in the CSS isn't read as a\n // replacement-pattern backreference.\n return compiledCode.replace(styleBodyRe, (_m, open: string, close: string) => {\n return `${open}${escaped}${close}`\n })\n }\n\n // Shape 2 — no @style block. Inject a fresh stylesheet + adoption.\n // Bail (no-op) if the expected defineComponent setup shape is absent.\n const setupRe = /defineComponent\\(\\s*\\((_ctx|ctx)\\)\\s*=>\\s*\\{/\n const m = setupRe.exec(compiledCode)\n if (m == null) return compiledCode\n\n // Inject the module-level stylesheet declaration after the last import line.\n const lines = compiledCode.split('\\n')\n let lastImportIdx = -1\n for (let i = lines.length - 1; i >= 0; i--) {\n const t = (lines[i] ?? '').trim()\n if (t.startsWith('import ') || t.startsWith('import{')) {\n lastImportIdx = i\n break\n }\n }\n const decl = `const __style__ = new CSSStyleSheet();\\n__style__.replaceSync(\\`${escaped}\\`);`\n if (lastImportIdx !== -1) {\n lines.splice(lastImportIdx + 1, 0, decl)\n } else {\n lines.unshift(decl)\n }\n let withDecl = lines.join('\\n')\n\n // Rename the setup param to `ctx` (codegen emits `_ctx` when no @style/ctx\n // usage) and inject the adoption as the first statement of the setup body.\n withDecl = withDecl.replace(\n /defineComponent\\(\\s*\\((?:_ctx|ctx)\\)\\s*=>\\s*\\{/,\n 'defineComponent((ctx) => {\\n (ctx.host as ShadowRoot).adoptedStyleSheets = [__style__];',\n )\n return withDecl\n}\n\n/**\n * Virtual-module prefix used by the `shadowMode === 'none'` branch to route\n * per-SFC utility CSS through Vite's built-in CSS pipeline. The plugin\n * (`aihuCompilerPlugin`) implements `resolveId` + `load` for ids matching\n * `VIRTUAL_UTILITY_PREFIX + '<hash>.css'`, returning the stored CSS body so\n * Vite hoists it into the bundle CSS asset (`dist/assets/*.css`) — NOT into\n * `host.adoptedStyleSheets`, which is a no-op when there is no shadow root.\n *\n * The trailing `.css` extension is mandatory: Vite's built-in CSS plugin keys\n * off the extension to know it should run the CSS pipeline on the module.\n *\n * @internal\n */\nexport const VIRTUAL_UTILITY_PREFIX = '\\0virtual:aihu-utility/'\n\n/**\n * Stable short hash for keying the virtual-CSS module per source-SFC id.\n *\n * djb2-style; collisions are tolerable here because (a) each entry stores its\n * own CSS body, so a hash collision would only matter if two distinct SFCs\n * hashed to the same key AND were processed concurrently; (b) collisions are\n * recoverable — Vite would simply load the wrong CSS for one SFC; we still\n * keyed on the unhashed id internally to avoid that. The hash only appears in\n * the bundled asset URL.\n *\n * @internal\n */\nexport function _hashIdForUtilityCss(id: string): string {\n let h = 5381\n for (let i = 0; i < id.length; i++) {\n h = ((h * 33) ^ id.charCodeAt(i)) >>> 0\n }\n return h.toString(36)\n}\n\n/**\n * Bug 6 — `shadowMode === 'none'` branch.\n *\n * Routes utility CSS to Vite's CSS pipeline (which folds CSS imports into the\n * bundled `dist/assets/*.css` asset) instead of to `host.adoptedStyleSheets`\n * (a no-op on an element with no shadow root). Returns a prelude `import` that\n * the plugin's `resolveId` + `load` hooks resolve to the stored CSS body.\n *\n * The `__style__` shadow path is NOT invoked here — utility CSS for a\n * cascade-mode component MUST hit the global stylesheet, not a per-element\n * stylesheet that would be silently dropped by `HTMLElement`'s setter.\n *\n * Authored `@style` blocks still emit through the Rust codegen's `<style>`\n * node and are unaffected. (If a component opts into `shadowMode: 'none'` and\n * authors an `@style` block, the codegen still wires it through the\n * non-shadow path — that is the runtime's contract, not this hook's.)\n *\n * @internal\n */\nexport function _foldCssEngineStylesGlobal(\n compiledCode: string,\n css: string,\n id: string,\n): { code: string; virtualId: string } | null {\n if (!css.trim()) return null\n const hash = _hashIdForUtilityCss(id)\n const virtualId = `${VIRTUAL_UTILITY_PREFIX}${hash}.css`\n // Prepend the CSS import as a side-effect-only import so Vite's CSS plugin\n // hoists it into the bundle. We use the NULL-byte virtual id form\n // (Rollup/Vite convention for \"owned by this plugin\"); other plugins will\n // skip it. The compiler's transform returns this prepended code, which the\n // downstream esbuild/oxc strip leaves untouched (it's just an import).\n const prelude = `import ${JSON.stringify(virtualId)};\\n`\n return { code: prelude + compiledCode, virtualId }\n}\n\n// ─── v1.0.10a — compiler AST-export hook ─────────────────────────────────────\n//\n// Thin TS wrapper over the `aihu-compile --ast-json` flag. Returns the parsed\n// `.aihu` SFC AST in a stable, serializable shape consumed by the CSS engine's\n// AST scanner (`css-2-ast-scanner`). Mirrors the typed contract in\n// `docs/superpowers/specs/compiler-ast-export-hook.md` §4.\n\n/** Top-level AST export — one per .aihu SFC. */\nexport interface SfcAst {\n /** Resolved custom-element tag name (meta.name → route.name → file stem). */\n tag: string\n /** AST schema version — bumped on any breaking shape change (semver-tied). */\n astVersion: 1\n /** The @style block, if the SFC declared one. */\n style: SfcStyleBlock | null\n /** Parsed template tree. null when the SFC has no @template block. */\n template: SfcNode[] | null\n /** SFC-level metadata. */\n meta: SfcMeta\n}\n\nexport interface SfcStyleBlock {\n /** Verbatim CSS body of the @style block (braces stripped, $global token removed). */\n content: string\n /** 'scoped' (default) or 'global' (@style { $global ... }). */\n scope: 'scoped' | 'global'\n}\n\nexport interface SfcMeta {\n /** From @meta { name } / @route { name } / file stem — never null after resolution. */\n name: string\n}\n\n/** Discriminated union mirroring Rust `TemplateNode`. */\nexport type SfcNode =\n | { kind: 'element'; tag: string; attrs: SfcAttr[]; children: SfcNode[] }\n | { kind: 'macroElement'; name: string; attrs: SfcAttr[]; children: SfcNode[] }\n | { kind: 'text'; value: string }\n | { kind: 'interpolation'; expr: string }\n | { kind: 'ifBlock'; branches: Array<{ cond: string; body: SfcNode[] }> }\n | {\n kind: 'eachBlock'\n list: string\n item: string\n idx: string | null\n key: string | null\n body: SfcNode[]\n emptyBody: SfcNode[] | null\n }\n | { kind: 'htmlBlock'; expr: string }\n\n/** Discriminated union mirroring Rust `Attr` — the three class-forms key on `kind`. */\nexport type SfcAttr =\n | { kind: 'static'; name: string; value: string } // Form A\n | { kind: 'binding'; name: string; expr: string } // Form B\n | { kind: 'macro'; name: string; value: SfcMacroValue } // Form C (and on:/bind:/emit:/if/each/…)\n\nexport type SfcMacroValue =\n | { form: 'quoted'; value: string }\n | { form: 'curly'; expr: string }\n | { form: 'boolean' }\n\n/**\n * Parse a .aihu source string to its structured AST.\n *\n * Thin wrapper over the Rust binary (mirrors `transform()`): spawns\n * `aihu-compile --stdin --tag <stem> --ast-json`, feeds `source` on stdin, and\n * `JSON.parse`s stdout. `id` is optional and only used to derive the tag stem\n * and the `--path` arg (for `@route` C500 checks), identical to `transform()`.\n *\n * Throws on parse failure — the Rust binary exits non-zero and `execFileSync`\n * surfaces the diagnostic (same error path as `transform()`).\n */\nexport function compileToAst(source: string, id?: string): SfcAst {\n const stem = id ? basename(id, '.aihu') : 'Component'\n const args = ['--stdin', '--tag', stem, '--ast-json']\n if (id) {\n args.push('--path', id)\n }\n const json = execFileSync(resolveBinPath(), args, {\n input: source,\n encoding: 'utf8',\n })\n return JSON.parse(json) as SfcAst\n}\n\n/**\n * Structured `@route` metadata (the `.route.json` sidecar shape). All fields\n * optional — only what the SFC's `@route` block declares is present. `head` is\n * left opaque here (the router owns its shape).\n */\nexport interface RouteMeta {\n pattern?: string\n name?: string\n layout?: string\n middleware?: string[]\n ssr?: boolean\n params?: string[]\n head?: unknown\n}\n\n/**\n * Parse a `.aihu` source string and return its `@route` metadata, or `null`\n * when the SFC declares no `@route` block.\n *\n * Thin wrapper over the Rust binary (mirrors {@link compileToAst}): spawns\n * `aihu-compile --stdin --tag <stem> --route-json`, feeds `source` on stdin,\n * and `JSON.parse`s stdout. This is how build tools recover full route\n * metadata (`head`/`middleware`/`params`/`ssr`/`layout`) for the SPA build\n * path, where no `.route.json` sidecar is written to disk.\n *\n * Throws on parse failure (same error path as `transform()`/`compileToAst()`).\n */\nexport function compileRouteMeta(source: string, id?: string): RouteMeta | null {\n const stem = id ? basename(id, '.aihu') : 'Component'\n const args = ['--stdin', '--tag', stem, '--route-json']\n if (id) {\n args.push('--path', id)\n }\n const out = execFileSync(resolveBinPath(), args, {\n input: source,\n encoding: 'utf8',\n }).trim()\n if (out === '' || out === 'null') return null\n return JSON.parse(out) as RouteMeta\n}\n\n/**\n * Inject `_setMount(mount)` + `_setSignal(signal)` auto-wiring into a compiled\n * `.aihu` module. Adds the necessary symbols to existing imports and inserts\n * the boot calls right after the last `import` statement.\n *\n * @internal\n */\nexport function _injectAutoWiring(code: string): string {\n // 1. Add `mount` to the @aihu/arbor import (or create it).\n let result: string\n if (code.includes(\"from '@aihu/arbor'\")) {\n result = code.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/arbor'/,\n (_m: string, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('mount')) parts.push('mount')\n return `import { ${parts.join(', ')} } from '@aihu/arbor'`\n },\n )\n } else {\n result = `import { mount } from '@aihu/arbor'\\n${code}`\n }\n\n // 2. Add `signal` to the non-type @aihu/signals import (or create it).\n // Note: `import\\s+\\{` does NOT match `import type {` (the regex needs `{` immediately\n // after whitespace, whereas `import type {` has `type` in between). No negation guard\n // is needed — the replace callback below already skips `import type` lines.\n if (/import\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals'/.test(result)) {\n // There IS a value import from signals — add `signal` if missing.\n result = result.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/signals'/,\n (_m: string, imports: string) => {\n // Skip type-only imports\n if (_m.startsWith('import type')) return _m\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('signal')) parts.push('signal')\n return `import { ${parts.join(', ')} } from '@aihu/signals'`\n },\n )\n } else if (!/import.*from\\s*'@aihu\\/signals'/.test(result)) {\n // No signals import at all — insert after arbor import\n result = result.replace(\n /import\\s*\\{[^}]*\\}\\s*from\\s*'@aihu\\/arbor'/,\n (m: string) => `${m}\\nimport { signal } from '@aihu/signals'`,\n )\n }\n // If only `import type { Signal }` exists, insert value import after it\n else if (\n /import\\s+type\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals'/.test(result) &&\n !result.match(/import\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals'/)\n ) {\n result = result.replace(\n /(import\\s+type\\s+\\{[^}]*\\}\\s+from\\s+'@aihu\\/signals')/,\n (_m: string, typeImport: string) => `${typeImport}\\nimport { signal } from '@aihu/signals'`,\n )\n }\n\n // 3. Add `_setMount`, `_setSignal` to the @aihu/runtime import.\n result = result.replace(\n /import\\s*\\{([^}]*)\\}\\s*from\\s*'@aihu\\/runtime'/,\n (_m: string, imports: string) => {\n const parts = imports\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean)\n if (!parts.includes('_setMount')) parts.push('_setMount')\n if (!parts.includes('_setSignal')) parts.push('_setSignal')\n return `import { ${parts.join(', ')} } from '@aihu/runtime'`\n },\n )\n\n // 4. Insert boot calls after the last `import` statement.\n const lines = result.split('\\n')\n let lastImportIdx = -1\n for (let i = lines.length - 1; i >= 0; i--) {\n const t = (lines[i] ?? '').trim()\n if (t.startsWith('import ') || t.startsWith('import{')) {\n lastImportIdx = i\n break\n }\n }\n if (lastImportIdx !== -1) {\n lines.splice(lastImportIdx + 1, 0, '_setMount(mount)', '_setSignal(signal)', '')\n result = lines.join('\\n')\n }\n\n return result\n}\n\n/**\n * Vite plugin that compiles .aihu files to TypeScript during build and dev.\n *\n * Use `enforce: 'pre'` so the hook fires before Vite/Rollup's built-in\n * parsers attempt to process the raw .aihu content as JavaScript.\n *\n * @example\n * // vite.config.ts\n * import { aihuCompilerPlugin } from '@aihu/compiler'\n * export default { plugins: [aihuCompilerPlugin()] }\n *\n * **Known Limitation — Bun + Rollup4 ESM incompatibility (v0):**\n *\n * `bun vite build` fails in the `fixtures/vite-counter` fixture with two\n * cascading errors:\n *\n * 1. **Missing devDependency:** `vite` is declared only as an optional\n * `peerDependency` in `packages/compiler/package.json`. Bun does not\n * install optional peers automatically, so `bun vite build` exits\n * immediately with `Cannot find package 'vite'`.\n *\n * 2. **Bun + Rollup4 bridge:** Even with Vite installed, Bun processes\n * `vite.config.ts` through its own internal bundler before handing off\n * to Rollup4. When `@aihu/compiler` is resolved from the workspace\n * symlink (`dist/index.js`), Bun's ESM loader evaluates the module at\n * config-load time. The subprocess call inside `transform()` depends on\n * the Rust binary being at `../bin/aihu-compile` relative to `dist/`\n * (written by the postinstall hook). In a dev workspace where postinstall\n * has not run, this path does not exist and `execFileSync` throws. Bun surfaces\n * the error as a config-load failure, not a per-file transform error,\n * causing the entire build to abort before any `.aihu` file is\n * processed.\n *\n * **Workaround (v0):** Use `bun run integrate.ts` directly from\n * `packages/compiler/fixtures/vite-counter/`. This script calls\n * `transform()` from `@aihu/compiler` without involving Vite or Rollup.\n * Preconditions: (1) `cargo build --release` in `packages/compiler/`,\n * (2) `bun install` at the repo root.\n *\n * **v1 resolution:** Add `vite` as a `devDependency` in\n * `packages/compiler/package.json`; add a WASM or pre-built binary\n * strategy so the Rust binary is bundled with the npm package and does not\n * require a separate `cargo build --release` step.\n */\n/**\n * Minimal structural type for the `@aihu/css-engine` module surface this\n * plugin uses. Declared locally so the compiler never type-imports the\n * css-engine package (which would create a compile-time edge against an\n * optional peer that may be absent).\n *\n * @internal\n */\ninterface CssEngineModule {\n compileSfc(source: string, id?: string): string\n}\n\n// Memoised resolution of the optional `@aihu/css-engine` peer. `undefined`\n// = not yet attempted; `null` = attempted and unavailable (no-op path);\n// a module object = available. The dynamic import is attempted once per\n// process — repeated absence does not re-pay the resolution cost.\nlet _cssEngine: CssEngineModule | null | undefined\n\n// Whether we've already surfaced a one-shot warning that css-engine resolved\n// but `compileSfc` threw (typically: native css-core binary unresolvable in\n// the consumer's install — e.g. lockfile pins the per-platform placeholder\n// version). The transform stays non-fatal, but going fully silent leaves users\n// chasing \"why did my utility classes never emit?\". One warn per process.\nlet _cssEngineWarned = false\n\n// The optional-peer module specifier, held in a VARIABLE so TypeScript never\n// statically resolves `@aihu/css-engine`'s declarations at typecheck time.\n// css-engine depends on @aihu/compiler for its AST, so the two form a\n// circular package relationship; under CI's frozen install + moon build\n// ordering, css-engine's `dist`/`.d.ts` are not guaranteed to exist when\n// `compiler:typecheck` runs. A literal `import('@aihu/css-engine')` makes the\n// compiler emit TS2307 in that window (the `as` cast affects the RESULT type\n// only, not whether TS attempts module resolution). Resolving through this\n// variable keeps the import fully dynamic — no compile-time edge on the peer.\nconst _CSS_ENGINE_SPECIFIER = '@aihu/css-engine'\n\n/**\n * Lazily resolve `@aihu/css-engine` and compile a `.aihu` source's utility\n * classes to scoped CSS. Returns `''` when css-engine is not installed\n * (the optional-peer no-op path) or when compilation fails for any reason —\n * a CSS-engine failure MUST NOT break an otherwise-valid `.aihu` build.\n *\n * Sets `process.env.SCRIBE_COMPILE_BIN` to this plugin's resolved compiler\n * binary before calling `compileSfc`: css-engine re-derives the SFC AST via\n * its own bundled copy of `compileToAst`, whose binary path is resolved\n * relative to the css-engine package — which does NOT ship the compiler\n * binary. Pointing it at our `binPath` guarantees the AST css-engine parses\n * is produced by the exact same compiler this build uses.\n *\n * @internal\n */\nasync function _maybeCompileUtilityCss(source: string, id: string): Promise<string> {\n if (_cssEngine === null) return ''\n // Ensure css-engine's bundled `compileToAst` spawns the SAME compiler\n // binary this plugin uses (it has no compiler binary of its own). Set\n // this BEFORE the dynamic import so that any module-load-time evaluation\n // of `process.env.SCRIBE_COMPILE_BIN` in css-engine's bundled dist (older\n // bundles capture this into a module-scope const at line 8 of\n // `packages/css-engine/dist/index.js`) sees the correct value. After Bug 6,\n // the source `compileToAst` resolves the bin lazily on each call, so once\n // css-engine is rebuilt this set-before-import is belt-and-braces.\n if (process.env.SCRIBE_COMPILE_BIN == null) {\n process.env.SCRIBE_COMPILE_BIN = resolveBinPath()\n }\n if (_cssEngine === undefined) {\n try {\n // Guarded, lazy, OPTIONAL — see the plugin transform for the rationale.\n // Importing via the `_CSS_ENGINE_SPECIFIER` variable (not a string\n // literal) keeps this fully dynamic: TS does NOT resolve the peer's\n // `.d.ts` at typecheck time, so `compiler:typecheck` passes even when\n // css-engine's `dist` has not been built (the CI build-order window).\n _cssEngine = (await import(_CSS_ENGINE_SPECIFIER)) as unknown as CssEngineModule\n } catch {\n _cssEngine = null\n return ''\n }\n }\n try {\n return _cssEngine.compileSfc(source, id)\n } catch (err) {\n // A css-engine compile failure is non-fatal: fall back to the no-op\n // path (utility classes don't emit) rather than aborting the build.\n // BUT — silently swallowing this means a user who clearly intends\n // css-engine to be active (the peer resolved) will never know their\n // utility classes are inert. Surface a one-shot warning with the\n // underlying error + an install/upgrade hint. Idempotent per process.\n if (!_cssEngineWarned) {\n _cssEngineWarned = true\n const msg = err instanceof Error ? err.message : String(err)\n console.warn(\n `[@aihu/compiler] @aihu/css-engine is installed but compileSfc() failed; ` +\n `utility classes will not emit. Original error: ${msg}\\n` +\n `Hint: ensure the native css-core binary is installed ` +\n `(install/upgrade @aihu/css-engine + its per-platform optional dep, ` +\n `or run \\`cargo build --release -p aihu-css-core\\` in a dev clone).`,\n )\n }\n return ''\n }\n}\n\nexport function aihuCompilerPlugin(options?: AihuCompilerPluginOptions): VitePlugin {\n const islandsEnabled = options?.islands !== false\n const shadowMode = options?.shadowMode\n const target = options?.target\n const layoutsDir = options?.layoutsDir ?? 'src/layouts'\n\n // Bug 6 — per-instance store of virtual utility-CSS modules. Keyed by the\n // full virtual id (NUL-prefixed). Populated by the transform hook when\n // `shadowMode === 'none'` produces utility CSS; drained by the `load` hook\n // when Vite's CSS pipeline asks for the module body. Lives on the plugin\n // instance so multiple `aihuCompilerPlugin()` calls in the same build don't\n // alias each other's css.\n const utilityCssStore = new Map<string, string>()\n\n return {\n name: 'aihu-compiler',\n enforce: 'pre',\n resolveId(source) {\n // Own all `\\0virtual:aihu-utility/<hash>.css` ids so Vite's resolver\n // doesn't try to find them on disk. Returning the id verbatim is the\n // Rollup convention for \"I'll handle the load.\"\n if (source.startsWith(VIRTUAL_UTILITY_PREFIX)) return source\n return null\n },\n load(id) {\n if (!id.startsWith(VIRTUAL_UTILITY_PREFIX)) return null\n // Vite's CSS pipeline runs on the returned source because the id ends\n // in `.css` — it parses, minifies (in build), and hoists into a CSS\n // asset chunk that lands in `dist/assets/<name>-<hash>.css`.\n return utilityCssStore.get(id) ?? null\n },\n transform(code, id) {\n // Strip Vite query strings (e.g. `?import`, `?t=...`) before checking the extension.\n const rawId = id.split('?')[0]!\n if (!rawId.endsWith('.aihu')) return\n return (async () => {\n // B3b — write per-SFC `.aihu.ts` sidecar adjacent to source so\n // `tsc --noEmit` over `**/*.aihu.ts` type-checks template\n // expressions end-to-end (Architect spec §7 path (i)).\n const sidecarOut = `${rawId}.ts`\n // Layout SFCs (under the layouts dir) compile in layout mode: a\n // namespaced `aihu-layout-<stem>` tag + a passive <$outlet> marker.\n const isLayout = _isLayoutFile(rawId, layoutsDir)\n const layoutTag = isLayout ? _layoutTag(basename(rawId, '.aihu')) : undefined\n const tOpts = {\n sidecarOut,\n ...(target ? { target } : {}),\n ...(layoutTag ? { tag: layoutTag } : {}),\n }\n const result = transform(code, rawId, tOpts)\n // §9.4 per-file shadow override: the Rust `$shadow` macro emits a leading\n // `// @aihu:shadow <mode>` marker; it wins over the plugin's global\n // shadowMode and drives BOTH _injectShadowMode and the css fold branch.\n const perFileShadow = /^\\/\\/ @aihu:shadow (open|closed|none)\\b/m.exec(result.code)?.[1] as\n | 'open'\n | 'closed'\n | 'none'\n | undefined\n const effectiveShadow = perFileShadow ?? shadowMode\n let compiled =\n effectiveShadow != null ? _injectShadowMode(result.code, effectiveShadow) : result.code\n // Light-DOM: the authored `@style` block compiled to a per-instance\n // `host.adoptedStyleSheets` assignment, but a light-DOM host has no\n // shadow root so that setter is a no-op. Redirect the module-level\n // sheet to `document.adoptedStyleSheets` (idempotent) so authored recipe\n // CSS reaches the global cascade alongside the css-engine utility CSS.\n if (effectiveShadow === 'none') compiled = _globalizeAuthoredStyle(compiled)\n if (isLayout) compiled = _passivizeOutlet(compiled)\n\n // ── css-engine hook (optional, lazy, no circular dep) ──────────────\n // @aihu/css-engine depends on @aihu/compiler (for its AST), so the\n // compiler MUST NOT hard-depend on it. It is declared an OPTIONAL\n // peerDependency and pulled in ONLY via this guarded dynamic import:\n // when present, we compile the SFC's utility classes to scoped CSS\n // and fold it into the component's shadow `<style>`; when absent the\n // import throws and we no-op (utility classes simply don't emit —\n // the pre-hook behaviour). This keeps css-engine an opt-in enhancement\n // with zero dependency cycle.\n const utilityCss = await _maybeCompileUtilityCss(code, rawId)\n if (utilityCss) {\n if (effectiveShadow === 'none') {\n // Bug 6 — no shadow root → `host.adoptedStyleSheets` is a no-op.\n // Route utility CSS through Vite's CSS pipeline via a virtual\n // `.css` import so it lands in `dist/assets/*.css` and reaches the\n // global cascade. The authored `@style` block (if any) still\n // emits via the Rust codegen's normal path and is unaffected.\n const folded = _foldCssEngineStylesGlobal(compiled, utilityCss, rawId)\n if (folded) {\n utilityCssStore.set(folded.virtualId, utilityCss)\n compiled = folded.code\n }\n } else {\n // `shadowMode: 'open' | 'closed'` (default): fold into the\n // per-component `CSSStyleSheet` adopted by the shadow root.\n compiled = _foldCssEngineStyles(compiled, utilityCss)\n }\n }\n\n const elementTag = _extractElementTag(compiled)\n\n let out: string\n\n // §9.4 — a base-extending recipe (`defineComponent({ base: X, ... })`)\n // MUST take the full defineComponent/defineElement path: the static\n // island shim inlines `class extends HTMLElement` and cannot honor a\n // base class. Force-classify it interactive regardless of signal usage.\n // biome-ignore lint/correctness/noEmptyCharacterClassInRegex: [^] is valid JS — matches any char including newlines\n const hasBase = /defineComponent\\(\\s*\\{[^]*?\\bbase\\s*:/.test(compiled)\n\n // Plan 3.3 — static-island fast path. Bypasses HMR injection because\n // a component with no signals has no setup state to hot-replace.\n // Static islands strip @aihu/runtime entirely — do NOT inject auto-wiring\n // (it would reference _setMount/_setSignal as undefined identifiers).\n if (\n islandsEnabled &&\n elementTag !== null &&\n !hasBase &&\n _classifyIsland(compiled) === 'static'\n ) {\n out = _buildStaticIsland(compiled, elementTag)\n } else if (elementTag !== null) {\n // Inject HMR instrumentation. The injected block is gated on\n // `typeof __DEV__ !== 'undefined' && __DEV__` so production\n // bundlers dead-code-eliminate it when they set __DEV__ = false.\n out = _buildHmrCode(compiled, elementTag)\n // Plan 3.3 — interactive islands also gain `defer` attribute\n // support so individual instances can opt into lazy hydration.\n out = _buildDeferredHydration(out, elementTag)\n // Inject auto-wiring so consumers don't need a manual main.ts bootstrap.\n out = _injectAutoWiring(out)\n } else {\n out = compiled\n // Inject auto-wiring so consumers don't need a manual main.ts bootstrap.\n out = _injectAutoWiring(out)\n }\n\n // The Rust compiler emits TypeScript (type casts, import type, etc.) and\n // the injected HMR / defer helpers also contain TS generics and casts.\n // Vite does NOT re-run its TS-strip step when a plugin returns code for a\n // non-.ts ID, so we must strip types ourselves before returning.\n //\n // Priority: always try transformWithEsbuild first — it strips types to\n // plain JS in both Vite 5 (via esbuild) and Vite 8 (deprecated wrapper).\n // Using moduleType:'ts' only as a last resort because `import('vite')`\n // resolves to the root node_modules vite (which may be v8 even when a\n // consumer project runs v5), causing v5's Rollup to receive raw TypeScript\n // and fail on import-type / as-casts.\n try {\n const vite = await import('vite')\n if ('transformWithEsbuild' in vite && typeof vite.transformWithEsbuild === 'function') {\n const stripped = await vite.transformWithEsbuild(out, 'component.ts', {\n target: 'esnext',\n sourcemap: false,\n })\n return { code: stripped.code, map: null }\n }\n // Fallback for future Vite versions where esbuild is fully removed:\n // return TS and let Rolldown strip types natively.\n // biome-ignore lint/suspicious/noExplicitAny: moduleType is rolldown API\n return { code: out, moduleType: 'ts', map: null } as any\n } catch {\n // If running outside Vite (e.g. tests, standalone transform), return as-is.\n return { code: out, map: null }\n }\n })()\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ //#region js/resolve-binary.d.ts
2
+ /**
3
+ * Whether `candidate` is a usable `aihu-compile` executable — NOT merely a
4
+ * present file.
5
+ *
6
+ * The per-platform packages (`@aihu/compiler-<platform>`) carry a placeholder
7
+ * `aihu-compile` in source; the real prebuilt binary is only injected by the
8
+ * release CI. Once those packages become resolvable in the workspace, a bare
9
+ * `existsSync` would happily return the non-executable placeholder, which then
10
+ * blows up with EACCES inside `spawnSync`/`execFileSync`. So we must verify the
11
+ * candidate is actually runnable before accepting it.
12
+ *
13
+ * POSIX: require the execute bit (X_OK). A zero-byte/text placeholder without
14
+ * +x fails here and we fall through to the dev `target/` fallback.
15
+ *
16
+ * Windows: there is no execute bit — `accessSync(_, X_OK)` is effectively
17
+ * always true — so we additionally require a non-empty regular file, which
18
+ * still rejects a zero-byte placeholder.
19
+ */
20
+ declare function isUsableExecutable(candidate: string): boolean;
21
+ /**
22
+ * Resolve the absolute path to the `aihu-compile` executable.
23
+ *
24
+ * Resolution order:
25
+ * 1. The per-platform optionalDependency package (`@aihu/compiler-<platform>`)
26
+ * shipped to npm consumers — resolved via
27
+ * `createRequire(...).resolve('<pkg>/package.json')` so it works in both
28
+ * ESM and CJS and respects the consumer's node_modules layout.
29
+ * 2. Dev fallback: the monorepo workspace `target/release|debug/` — only
30
+ * present in a dev clone with a Rust toolchain (`cargo build --release -p
31
+ * aihu-compile`). Kept so in-repo builds + tests work without publishing.
32
+ *
33
+ * If the current platform is SUPPORTED but neither path yields a binary, throws
34
+ * a structured error pointing at the missing optionalDependency. If the
35
+ * platform is UNSUPPORTED, the error lists the dev fallback so source builds
36
+ * still have a clear remedy.
37
+ */
38
+ declare function resolveCompilerBinary(): string;
39
+ //#endregion
40
+ export { isUsableExecutable, resolveCompilerBinary };
41
+ //# sourceMappingURL=resolve-binary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-binary.d.ts","names":[],"sources":["../js/resolve-binary.ts"],"mappings":";;AA2FA;;;;;AA4BA;;;;;;;;;;;;iBA5BgB,kBAAA,CAAmB,SAAA;;;;;;;;;;;;;;;;;;iBA4BnB,qBAAA,CAAA"}
@@ -0,0 +1,2 @@
1
+ import{dirname as e,join as t,resolve as n}from"node:path";import{accessSync as r,constants as i,existsSync as a,statSync as o}from"node:fs";import{createRequire as s}from"node:module";import{fileURLToPath as c}from"node:url";const l=e(c(import.meta.url));function u(){if(typeof process>`u`||!process.platform||!process.arch)return null;switch(`${process.platform}-${process.arch}`){case`darwin-arm64`:return{platformId:`darwin-arm64`,packageName:`@aihu/compiler-darwin-arm64`,binFile:`aihu-compile`};case`darwin-x64`:return{platformId:`darwin-x64`,packageName:`@aihu/compiler-darwin-x64`,binFile:`aihu-compile`};case`linux-x64`:return{platformId:`linux-x64-gnu`,packageName:`@aihu/compiler-linux-x64-gnu`,binFile:`aihu-compile`};case`linux-arm64`:return{platformId:`linux-arm64-gnu`,packageName:`@aihu/compiler-linux-arm64-gnu`,binFile:`aihu-compile`};case`win32-x64`:return{platformId:`win32-x64-msvc`,packageName:`@aihu/compiler-win32-x64-msvc`,binFile:`aihu-compile.exe`};default:return null}}let d=null;function f(e){try{let t=o(e);return!t.isFile()||t.size===0?!1:(r(e,i.X_OK),!0)}catch{return!1}}function p(){if(d!==null)return d;let r=u();if(r){let n=s(import.meta.url);try{let i=t(e(n.resolve(`${r.packageName}/package.json`)),r.binFile);if(f(i))return d=i,d}catch{}}let i=process.platform===`win32`?`.exe`:``,o=[n(l,`../../../target/release`,`aihu-compile${i}`),n(l,`../../../target/debug`,`aihu-compile${i}`),n(l,`../bin`,`aihu-compile${i}`)];for(let e of o)if(a(e))return d=e,d;throw m(r,o)}function m(e,t){return Error(e===null?`[@aihu/compiler] No prebuilt aihu-compile binary for this platform.\n\n Platform: ${typeof process<`u`?`${process.platform}-${process.arch}`:`unknown`}\n\n @aihu/compiler ships prebuilt binaries for darwin-arm64, darwin-x64,\n linux-x64-gnu (glibc), linux-arm64-gnu (glibc) and win32-x64-msvc.\n Your platform is not in that set.\n\n To build from source you need a Rust toolchain, then run from the repo root:\n cargo build --release -p aihu-compile\n\n Checked dev fallback paths: ${t.join(`, `)}`:`[@aihu/compiler] Native compiler binary not found for this platform.\n\n Platform: ${e.platformId}\n Expected package: ${e.packageName}\n Expected file: ${e.packageName}/${e.binFile}\n\n This binary is distributed as an optionalDependency of @aihu/compiler.\n Your package manager may have skipped it (optionalDependencies are\n silently dropped on install failure).\n\n To reinstall:\n npm install @aihu/compiler\n # or: pnpm install or: bun install\n\n If you are working in the aihu monorepo, build from source instead:\n cargo build --release -p aihu-compile\n Checked dev fallback paths: ${t.join(`, `)}`)}export{f as isUsableExecutable,p as resolveCompilerBinary};
2
+ //# sourceMappingURL=resolve-binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-binary.js","names":[],"sources":["../js/resolve-binary.ts"],"sourcesContent":["/**\n * resolve-binary.ts — call-time resolver for the `aihu-compile` Rust binary.\n *\n * A near-mechanical clone of packages/css-engine/src/index.ts's resolveBinary()\n * machinery, renamed for the compiler: the per-platform packages are\n * `@aihu/compiler-<platform>`, the executable is `aihu-compile[.exe]`, and the\n * dev/source fallback is the workspace-root `target/release|debug/aihu-compile`.\n *\n * Unlike @aihu/server (a napi `.node` addon loaded via require), the compiler\n * invokes `aihu-compile` as a CLI SUBPROCESS. So the platform package exposes a\n * raw executable file, and we resolve its absolute PATH — we never `require()`\n * the binary itself. The SAME order is used by the bin shim\n * (bin/aihu-compile.mjs) and the vite plugin (js/index.ts).\n */\nimport { accessSync, constants, existsSync, statSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\ninterface PlatformDescriptor {\n readonly platformId: string\n readonly packageName: string\n /** Executable filename inside the platform package. */\n readonly binFile: string\n}\n\nfunction detectPlatform(): PlatformDescriptor | null {\n if (typeof process === 'undefined' || !process.platform || !process.arch) {\n return null\n }\n const key = `${process.platform}-${process.arch}`\n switch (key) {\n case 'darwin-arm64':\n return {\n platformId: 'darwin-arm64',\n packageName: '@aihu/compiler-darwin-arm64',\n binFile: 'aihu-compile',\n }\n case 'darwin-x64':\n return {\n platformId: 'darwin-x64',\n packageName: '@aihu/compiler-darwin-x64',\n binFile: 'aihu-compile',\n }\n case 'linux-x64':\n // We only ship glibc; musl users fall through to the dev/source path.\n return {\n platformId: 'linux-x64-gnu',\n packageName: '@aihu/compiler-linux-x64-gnu',\n binFile: 'aihu-compile',\n }\n case 'linux-arm64':\n // glibc aarch64; musl users fall through to the dev/source path.\n return {\n platformId: 'linux-arm64-gnu',\n packageName: '@aihu/compiler-linux-arm64-gnu',\n binFile: 'aihu-compile',\n }\n case 'win32-x64':\n return {\n platformId: 'win32-x64-msvc',\n packageName: '@aihu/compiler-win32-x64-msvc',\n binFile: 'aihu-compile.exe',\n }\n default:\n return null\n }\n}\n\nlet _binPath: string | null = null\n\n/**\n * Whether `candidate` is a usable `aihu-compile` executable — NOT merely a\n * present file.\n *\n * The per-platform packages (`@aihu/compiler-<platform>`) carry a placeholder\n * `aihu-compile` in source; the real prebuilt binary is only injected by the\n * release CI. Once those packages become resolvable in the workspace, a bare\n * `existsSync` would happily return the non-executable placeholder, which then\n * blows up with EACCES inside `spawnSync`/`execFileSync`. So we must verify the\n * candidate is actually runnable before accepting it.\n *\n * POSIX: require the execute bit (X_OK). A zero-byte/text placeholder without\n * +x fails here and we fall through to the dev `target/` fallback.\n *\n * Windows: there is no execute bit — `accessSync(_, X_OK)` is effectively\n * always true — so we additionally require a non-empty regular file, which\n * still rejects a zero-byte placeholder.\n */\nexport function isUsableExecutable(candidate: string): boolean {\n try {\n const st = statSync(candidate)\n if (!st.isFile() || st.size === 0) return false\n accessSync(candidate, constants.X_OK)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Resolve the absolute path to the `aihu-compile` executable.\n *\n * Resolution order:\n * 1. The per-platform optionalDependency package (`@aihu/compiler-<platform>`)\n * shipped to npm consumers — resolved via\n * `createRequire(...).resolve('<pkg>/package.json')` so it works in both\n * ESM and CJS and respects the consumer's node_modules layout.\n * 2. Dev fallback: the monorepo workspace `target/release|debug/` — only\n * present in a dev clone with a Rust toolchain (`cargo build --release -p\n * aihu-compile`). Kept so in-repo builds + tests work without publishing.\n *\n * If the current platform is SUPPORTED but neither path yields a binary, throws\n * a structured error pointing at the missing optionalDependency. If the\n * platform is UNSUPPORTED, the error lists the dev fallback so source builds\n * still have a clear remedy.\n */\nexport function resolveCompilerBinary(): string {\n if (_binPath !== null) return _binPath\n\n const descriptor = detectPlatform()\n\n // 1. Per-platform optionalDependency package (the published-consumer path).\n //\n // Accept the candidate ONLY if it is a usable executable. A present-but-\n // non-executable placeholder (the in-source stub that becomes resolvable once\n // the per-platform packages are pinned in the lockfile) must NOT be returned —\n // doing so spawns a non-executable file and fails with EACCES. In that case we\n // deliberately fall THROUGH to the dev `target/` fallback below.\n if (descriptor) {\n const requireFn = createRequire(import.meta.url)\n try {\n const pkgJson = requireFn.resolve(`${descriptor.packageName}/package.json`)\n const candidate = join(dirname(pkgJson), descriptor.binFile)\n if (isUsableExecutable(candidate)) {\n _binPath = candidate\n return _binPath\n }\n } catch {\n // Package not installed (optionalDependency skipped for this platform, or\n // a partial install). Fall through to the dev/source path, then error.\n }\n }\n\n // 2. Dev fallback: monorepo workspace target/. Only exists in a dev clone.\n //\n // This module builds to packages/compiler/dist/resolve-binary.js, so the\n // workspace root is three levels up (dist → compiler → packages → root).\n // `aihu-compile` is a workspace member; cargo emits its [[bin]] to the\n // workspace-root ./target.\n const ext = process.platform === 'win32' ? '.exe' : ''\n const devCandidates = [\n resolve(__dirname, '../../../target/release', `aihu-compile${ext}`),\n resolve(__dirname, '../../../target/debug', `aihu-compile${ext}`),\n // Package-local staged binary: `packages/compiler/bin/aihu-compile`. This is\n // where CI and the release pipeline place a prebuilt binary that was built\n // or downloaded out-of-band — e.g. deploy-docs.yml's \"Build & deploy\" job\n // downloads the linux-x64 artifact here (without a cargo `target/`), and\n // release.yml's \"Stage compiler bin\" step does the same so prepublish builds\n // can compile. In published consumers this dir holds only the `.mjs` shim, so\n // this candidate simply doesn't exist there and we fall through.\n resolve(__dirname, '../bin', `aihu-compile${ext}`),\n ]\n for (const c of devCandidates) {\n if (existsSync(c)) {\n _binPath = c\n return _binPath\n }\n }\n\n throw buildMissingBinaryError(descriptor, devCandidates)\n}\n\nfunction buildMissingBinaryError(\n descriptor: PlatformDescriptor | null,\n devCandidates: string[],\n): Error {\n if (descriptor === null) {\n return new Error(\n `[@aihu/compiler] No prebuilt aihu-compile binary for this platform.\\n\\n` +\n ` Platform: ${typeof process !== 'undefined' ? `${process.platform}-${process.arch}` : 'unknown'}\\n\\n` +\n ` @aihu/compiler ships prebuilt binaries for darwin-arm64, darwin-x64,\\n` +\n ` linux-x64-gnu (glibc), linux-arm64-gnu (glibc) and win32-x64-msvc.\\n` +\n ` Your platform is not in that set.\\n\\n` +\n ` To build from source you need a Rust toolchain, then run from the repo root:\\n` +\n ` cargo build --release -p aihu-compile\\n\\n` +\n ` Checked dev fallback paths: ${devCandidates.join(', ')}`,\n )\n }\n return new Error(\n `[@aihu/compiler] Native compiler binary not found for this platform.\\n\\n` +\n ` Platform: ${descriptor.platformId}\\n` +\n ` Expected package: ${descriptor.packageName}\\n` +\n ` Expected file: ${descriptor.packageName}/${descriptor.binFile}\\n\\n` +\n ` This binary is distributed as an optionalDependency of @aihu/compiler.\\n` +\n ` Your package manager may have skipped it (optionalDependencies are\\n` +\n ` silently dropped on install failure).\\n\\n` +\n ` To reinstall:\\n` +\n ` npm install @aihu/compiler\\n` +\n ` # or: pnpm install or: bun install\\n\\n` +\n ` If you are working in the aihu monorepo, build from source instead:\\n` +\n ` cargo build --release -p aihu-compile\\n` +\n ` Checked dev fallback paths: ${devCandidates.join(', ')}`,\n )\n}\n"],"mappings":"kOAmBA,MAAM,EAAY,EAAQ,EAAc,OAAO,KAAK,IAAI,CAAC,CASzD,SAAS,GAA4C,CACnD,GAAI,OAAO,QAAY,KAAe,CAAC,QAAQ,UAAY,CAAC,QAAQ,KAClE,OAAO,KAGT,OAAQ,GADO,QAAQ,SAAS,GAAG,QAAQ,OAC3C,CACE,IAAK,eACH,MAAO,CACL,WAAY,eACZ,YAAa,8BACb,QAAS,eACV,CACH,IAAK,aACH,MAAO,CACL,WAAY,aACZ,YAAa,4BACb,QAAS,eACV,CACH,IAAK,YAEH,MAAO,CACL,WAAY,gBACZ,YAAa,+BACb,QAAS,eACV,CACH,IAAK,cAEH,MAAO,CACL,WAAY,kBACZ,YAAa,iCACb,QAAS,eACV,CACH,IAAK,YACH,MAAO,CACL,WAAY,iBACZ,YAAa,gCACb,QAAS,mBACV,CACH,QACE,OAAO,MAIb,IAAI,EAA0B,KAoB9B,SAAgB,EAAmB,EAA4B,CAC7D,GAAI,CACF,IAAM,EAAK,EAAS,EAAU,CAG9B,MAFI,CAAC,EAAG,QAAQ,EAAI,EAAG,OAAS,EAAU,IAC1C,EAAW,EAAW,EAAU,KAAK,CAC9B,SACD,CACN,MAAO,IAqBX,SAAgB,GAAgC,CAC9C,GAAI,IAAa,KAAM,OAAO,EAE9B,IAAM,EAAa,GAAgB,CASnC,GAAI,EAAY,CACd,IAAM,EAAY,EAAc,OAAO,KAAK,IAAI,CAChD,GAAI,CAEF,IAAM,EAAY,EAAK,EADP,EAAU,QAAQ,GAAG,EAAW,YAAY,eACtB,CAAC,CAAE,EAAW,QAAQ,CAC5D,GAAI,EAAmB,EAAU,CAE/B,MADA,GAAW,EACJ,OAEH,GAYV,IAAM,EAAM,QAAQ,WAAa,QAAU,OAAS,GAC9C,EAAgB,CACpB,EAAQ,EAAW,0BAA2B,eAAe,IAAM,CACnE,EAAQ,EAAW,wBAAyB,eAAe,IAAM,CAQjE,EAAQ,EAAW,SAAU,eAAe,IAAM,CACnD,CACD,IAAK,IAAM,KAAK,EACd,GAAI,EAAW,EAAE,CAEf,MADA,GAAW,EACJ,EAIX,MAAM,EAAwB,EAAY,EAAc,CAG1D,SAAS,EACP,EACA,EACO,CAaP,OAXa,MADT,IAAe,KAEf,6FACwB,OAAO,QAAY,IAAc,GAAG,QAAQ,SAAS,GAAG,QAAQ,OAAS,UAAU,sVAMxE,EAAc,KAAK,KAAK,GAI7D,+FACyB,EAAW,WAAW,wBACtB,EAAW,YAAY,wBACvB,EAAW,YAAY,GAAG,EAAW,QAAQ,8aASnC,EAAc,KAAK,KAAK,GAf1D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihu/compiler",
3
- "version": "0.9.6",
3
+ "version": "0.9.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,11 +13,11 @@
13
13
  }
14
14
  },
15
15
  "bin": {
16
- "aihu-compile": "./bin/aihu-compile"
16
+ "aihu-compile": "./bin/aihu-compile.mjs"
17
17
  },
18
18
  "files": [
19
+ "bin/aihu-compile.mjs",
19
20
  "dist",
20
- "js/postinstall.ts",
21
21
  "README.md",
22
22
  "LICENSE"
23
23
  ],
@@ -25,10 +25,16 @@
25
25
  "scripts": {
26
26
  "build": "rolldown -c",
27
27
  "typecheck": "tsc --noEmit",
28
- "postinstall": "bun run js/postinstall.ts",
29
28
  "prepublishOnly": "bun run build",
30
29
  "codemod:template-syntax": "bun js/codemods/template-syntax/run-migration.ts"
31
30
  },
31
+ "optionalDependencies": {
32
+ "@aihu/compiler-darwin-arm64": "0.1.0",
33
+ "@aihu/compiler-darwin-x64": "0.1.0",
34
+ "@aihu/compiler-linux-x64-gnu": "0.1.0",
35
+ "@aihu/compiler-linux-arm64-gnu": "0.1.0",
36
+ "@aihu/compiler-win32-x64-msvc": "0.1.0"
37
+ },
32
38
  "peerDependencies": {
33
39
  "vite": ">=5.0.0"
34
40
  },
package/bin/aihu-compile DELETED
Binary file
package/js/postinstall.ts DELETED
@@ -1,479 +0,0 @@
1
- /**
2
- * Postinstall hook for @aihu/compiler.
3
- *
4
- * Resolution order (first match wins):
5
- *
6
- * 1. SCRIBE_SKIP_POSTINSTALL=1 → no-op, exit 0.
7
- * 2. Binary already present at → arch-validate it; if compatible no-op
8
- * bin/aihu-compile<ext> OR exit 0, if incompatible (e.g. a Linux
9
- * target/release/aihu-compile<ext> ELF that leaked into the tarball on a
10
- * macOS host) delete it and fall through.
11
- * 3. SCRIBE_COMPILE_BIN=<path> → copy that path → bin/, exit 0.
12
- * 4. GitHub Releases download → bin/aihu-compile<ext>, verify SHA256,
13
- * exit 0. (arch-4 §4.3 — sidecar
14
- * verification implemented v1.1.)
15
- * 5. Local `cargo build --release` → target/release/aihu-compile<ext>,
16
- * exit 0.
17
- * 6. Everything failed → log warning, exit 0 anyway. The user
18
- * will need to either provide
19
- * SCRIBE_COMPILE_BIN or run
20
- * `cargo build --release` themselves
21
- * before invoking the compiler.
22
- *
23
- * The hard rule: this script MUST exit 0 in every "no binary acquired"
24
- * branch. A non-zero exit aborts `bun install`, which prevents workspace
25
- * symlinks from being created and breaks every downstream package that
26
- * imports a `@aihu/*` sibling. Compile-time failure (when the user
27
- * actually invokes the compiler without a binary) is acceptable and
28
- * recoverable; install-time failure is not.
29
- *
30
- * Hard-fail exit 1 is reserved for these cases:
31
- * - SCRIBE_COMPILE_BIN is set but points at a missing file (user error,
32
- * surface immediately rather than silently swallow).
33
- * - SHA256 verification of a downloaded binary fails (integrity violation —
34
- * do NOT run a tampered binary).
35
- * - Catastrophic unexpected exception (programming error in this script).
36
- *
37
- * Local dev override: if SCRIBE_COMPILE_BIN env var is set, that path is
38
- * used instead of downloading. This lets contributors who built from
39
- * source via `cargo build --release` point the compiler at their build.
40
- */
41
-
42
- import { spawnSync } from 'node:child_process'
43
- import { createHash } from 'node:crypto'
44
- import {
45
- chmodSync,
46
- closeSync,
47
- copyFileSync,
48
- existsSync,
49
- mkdirSync,
50
- openSync,
51
- readFileSync,
52
- readSync,
53
- unlinkSync,
54
- writeFileSync,
55
- } from 'node:fs'
56
- import { dirname, resolve } from 'node:path'
57
- import { fileURLToPath } from 'node:url'
58
-
59
- interface AssetMapping {
60
- asset: string
61
- ext: '' | '.exe'
62
- }
63
-
64
- function resolveAsset(platform: NodeJS.Platform, arch: string): AssetMapping | null {
65
- if (platform === 'darwin' && arch === 'arm64') {
66
- return { asset: 'aihu-compile-darwin-arm64', ext: '' }
67
- }
68
- if (platform === 'darwin' && arch === 'x64') {
69
- return { asset: 'aihu-compile-darwin-x64', ext: '' }
70
- }
71
- if (platform === 'linux' && arch === 'x64') {
72
- return { asset: 'aihu-compile-linux-x64', ext: '' }
73
- }
74
- // arch-4 §4.2 — aarch64-linux added in v1.1 release matrix.
75
- if (platform === 'linux' && arch === 'arm64') {
76
- return { asset: 'aihu-compile-linux-arm64', ext: '' }
77
- }
78
- if (platform === 'win32' && arch === 'x64') {
79
- return { asset: 'aihu-compile-windows-x64.exe', ext: '.exe' }
80
- }
81
- return null
82
- }
83
-
84
- const TAG = '[@aihu/compiler postinstall]'
85
-
86
- function info(msg: string): void {
87
- process.stdout.write(`${TAG} ${msg}\n`)
88
- }
89
-
90
- function warn(msg: string): void {
91
- process.stderr.write(`${TAG} WARN: ${msg}\n`)
92
- }
93
-
94
- function hardFail(msg: string): never {
95
- process.stderr.write(`${TAG} ERROR: ${msg}\n`)
96
- process.exit(1)
97
- }
98
-
99
- function softExit(msg: string): never {
100
- warn(msg)
101
- warn(
102
- 'bun install will continue. To enable the compiler later, set ' +
103
- 'SCRIBE_COMPILE_BIN to a built binary path or run ' +
104
- '`cargo build --release` from packages/compiler/ when a Rust ' +
105
- 'toolchain is available.',
106
- )
107
- process.exit(0)
108
- }
109
-
110
- interface DownloadResult {
111
- ok: boolean
112
- reason?: string
113
- status?: number
114
- }
115
-
116
- async function tryDownload(url: string, dest: string): Promise<DownloadResult> {
117
- let response: Response
118
- try {
119
- response = await fetch(url, { redirect: 'follow' })
120
- } catch (err) {
121
- const detail = err instanceof Error ? err.message : String(err)
122
- return { ok: false, reason: `network error: ${detail}` }
123
- }
124
- if (!response.ok) {
125
- return {
126
- ok: false,
127
- reason: `HTTP ${response.status} ${response.statusText}`,
128
- status: response.status,
129
- }
130
- }
131
- let buf: Buffer
132
- try {
133
- buf = Buffer.from(await response.arrayBuffer())
134
- } catch (err) {
135
- const detail = err instanceof Error ? err.message : String(err)
136
- return { ok: false, reason: `body read failed: ${detail}` }
137
- }
138
- if (buf.length === 0) {
139
- return { ok: false, reason: 'response body was empty' }
140
- }
141
- try {
142
- writeFileSync(dest, buf)
143
- } catch (err) {
144
- const detail = err instanceof Error ? err.message : String(err)
145
- return { ok: false, reason: `write failed: ${detail}` }
146
- }
147
- return { ok: true }
148
- }
149
-
150
- /**
151
- * Verify a downloaded binary's SHA256 digest against the matching `.sha256`
152
- * sidecar from GitHub Releases (arch-4 §4.3).
153
- *
154
- * Returns true on match, false on any failure (network, mismatch, parse).
155
- * The caller decides whether to fail hard or fall through; for download path
156
- * a mismatch is hardFail (integrity violation), other reasons soft-warn.
157
- */
158
- async function verifySha256(
159
- binaryPath: string,
160
- sidecarUrl: string,
161
- ): Promise<{ ok: boolean; reason?: string; expected?: string; actual?: string }> {
162
- let response: Response
163
- try {
164
- response = await fetch(sidecarUrl, { redirect: 'follow' })
165
- } catch (err) {
166
- const detail = err instanceof Error ? err.message : String(err)
167
- return { ok: false, reason: `sidecar network error: ${detail}` }
168
- }
169
- if (!response.ok) {
170
- return {
171
- ok: false,
172
- reason: `sidecar HTTP ${response.status} ${response.statusText}`,
173
- }
174
- }
175
- const sidecarText = (await response.text()).trim()
176
- // Sidecar format: hex digest (lowercase, 64 chars). Some tools prepend a filename;
177
- // accept either `<hash>` or `<hash> <name>` and extract the first whitespace-delimited token.
178
- const expected = sidecarText.split(/\s+/)[0]?.toLowerCase()
179
- if (!expected || !/^[0-9a-f]{64}$/i.test(expected)) {
180
- return {
181
- ok: false,
182
- reason: `malformed sidecar (expected 64-char hex, got "${sidecarText.slice(0, 80)}")`,
183
- }
184
- }
185
-
186
- let actual: string
187
- try {
188
- const fileBuf = readFileSync(binaryPath)
189
- actual = createHash('sha256').update(fileBuf).digest('hex')
190
- } catch (err) {
191
- const detail = err instanceof Error ? err.message : String(err)
192
- return { ok: false, reason: `local hash failed: ${detail}` }
193
- }
194
-
195
- if (actual !== expected) {
196
- return { ok: false, reason: 'digest mismatch', expected, actual }
197
- }
198
- return { ok: true }
199
- }
200
-
201
- /**
202
- * Inspect a binary's file-format magic bytes and (where cheaply available) its
203
- * architecture field. The point is to catch a wrong-arch binary sitting on disk
204
- * BEFORE we hand it to spawn() and ENOEXEC the user (see PR description: a
205
- * Linux x86-64 ELF can ship inside the tarball when the publisher's machine
206
- * left one in bin/, and arch-blind idempotency then traps it).
207
- *
208
- * Returns `null` if the file can't be read; format `'unknown'` for headers we
209
- * don't recognise (e.g. shell scripts, FAT/universal Mach-O — those callers
210
- * conservatively treat as compatible).
211
- */
212
- function inspectBinary(
213
- path: string,
214
- ): { format: 'elf' | 'macho' | 'macho-fat' | 'pe' | 'unknown'; arch: string | null } | null {
215
- let fd: number | null = null
216
- try {
217
- fd = openSync(path, 'r')
218
- const buf = Buffer.alloc(20)
219
- const bytesRead = readSync(fd, buf, 0, 20, 0)
220
- if (bytesRead < 20) return null
221
-
222
- // ELF: 0x7F 'E' 'L' 'F'
223
- if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) {
224
- // e_machine at offset 18 (u16, endianness per EI_DATA at offset 5).
225
- const littleEndian = buf[5] === 1
226
- const machine = littleEndian ? buf.readUInt16LE(18) : buf.readUInt16BE(18)
227
- const arch =
228
- machine === 0x3e ? 'x64' : machine === 0xb7 ? 'arm64' : machine === 0x03 ? 'ia32' : null
229
- return { format: 'elf', arch }
230
- }
231
-
232
- const magic = buf.readUInt32LE(0)
233
- // Mach-O 64-bit LE-on-disk magic. cputype at offset 4 (u32 LE).
234
- if (magic === 0xfeedfacf) {
235
- const cputype = buf.readUInt32LE(4)
236
- const arch = cputype === 0x01000007 ? 'x64' : cputype === 0x0100000c ? 'arm64' : null
237
- return { format: 'macho', arch }
238
- }
239
- // Mach-O FAT/universal (multi-arch). On-disk bytes are big-endian per spec;
240
- // a little-endian read yields 0xBEBAFECA.
241
- if (magic === 0xbebafeca || magic === 0xcafebabe) {
242
- return { format: 'macho-fat', arch: null }
243
- }
244
-
245
- // PE (Windows): 'MZ' at offset 0. Skip detailed machine parse — Windows
246
- // arch mismatches are rare and not the bug we're fixing here.
247
- if (buf[0] === 0x4d && buf[1] === 0x5a) {
248
- return { format: 'pe', arch: null }
249
- }
250
-
251
- return { format: 'unknown', arch: null }
252
- } catch {
253
- return null
254
- } finally {
255
- if (fd !== null) {
256
- try {
257
- closeSync(fd)
258
- } catch {
259
- /* swallow */
260
- }
261
- }
262
- }
263
- }
264
-
265
- function expectedFormatFor(platform: NodeJS.Platform): 'elf' | 'macho' | 'pe' | null {
266
- if (platform === 'darwin') return 'macho'
267
- if (platform === 'win32') return 'pe'
268
- if (platform === 'linux') return 'elf'
269
- return null
270
- }
271
-
272
- /**
273
- * Decide whether an on-disk binary is safe to keep for the current host.
274
- * Returns `null` when compatible; otherwise a short reason string suitable for
275
- * a warn-level log. `'unknown'` format is treated as compatible (avoid breaking
276
- * exotic but legitimate setups — e.g. a shell wrapper a dev placed here).
277
- */
278
- function incompatibilityReason(
279
- path: string,
280
- platform: NodeJS.Platform,
281
- arch: string,
282
- ): string | null {
283
- const probe = inspectBinary(path)
284
- if (!probe) return null
285
- if (probe.format === 'unknown') return null
286
- // FAT/universal Mach-O ships multiple slices; trust it on darwin, reject elsewhere.
287
- if (probe.format === 'macho-fat') {
288
- return platform === 'darwin' ? null : `universal Mach-O on ${platform}`
289
- }
290
- const expected = expectedFormatFor(platform)
291
- if (expected !== null && probe.format !== expected) {
292
- return `${probe.format} binary on ${platform} (expected ${expected})`
293
- }
294
- if (probe.arch !== null && probe.arch !== arch) {
295
- return `${probe.arch} binary on ${platform}/${arch}`
296
- }
297
- return null
298
- }
299
-
300
- function tryLocalBuild(pkgDir: string): boolean {
301
- // Check for cargo first — quick probe without spawning a build.
302
- const probe = spawnSync('cargo', ['--version'], {
303
- stdio: 'ignore',
304
- shell: false,
305
- })
306
- if (probe.error || probe.status !== 0) {
307
- info('cargo not available; skipping local Rust build fallback.')
308
- return false
309
- }
310
- info('attempting local `cargo build --release` (Rust toolchain detected)…')
311
- const build = spawnSync('cargo', ['build', '--release'], {
312
- cwd: pkgDir,
313
- stdio: 'inherit',
314
- shell: false,
315
- })
316
- if (build.error || build.status !== 0) {
317
- warn(
318
- `local cargo build failed (status=${build.status ?? 'unknown'}). The compiler binary was not built.`,
319
- )
320
- return false
321
- }
322
- info('local cargo build succeeded.')
323
- return true
324
- }
325
-
326
- async function main(): Promise<void> {
327
- if (process.env.SCRIBE_SKIP_POSTINSTALL) {
328
- info('SCRIBE_SKIP_POSTINSTALL set; skipping all binary-acquisition steps.')
329
- return
330
- }
331
-
332
- const platform = process.platform
333
- const arch = process.arch
334
-
335
- const mapping = resolveAsset(platform, arch)
336
- if (!mapping) {
337
- softExit(
338
- `unsupported platform: ${platform}/${arch} ` +
339
- '(supported: darwin/arm64, darwin/x64, linux/x64, linux/arm64, win32/x64).',
340
- )
341
- }
342
-
343
- const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
344
- const binDir = resolve(pkgDir, 'bin')
345
- const binPath = resolve(binDir, `aihu-compile${mapping.ext}`)
346
- const targetReleaseBin = resolve(pkgDir, 'target', 'release', `aihu-compile${mapping.ext}`)
347
-
348
- // Ensure target directory exists before any write operations.
349
- if (!existsSync(binDir)) {
350
- mkdirSync(binDir, { recursive: true })
351
- }
352
-
353
- // Idempotency: nothing to do if a usable binary is already in place at
354
- // either the released-asset path (bin/) or the local-build path
355
- // (target/release). "Usable" means the magic bytes match the host
356
- // platform/arch — without that arch probe a wrong-arch binary sitting in
357
- // the tarball (e.g. a Linux ELF that leaked from the publisher's machine)
358
- // short-circuits the download path and ENOEXECs the user at spawn time.
359
- if (existsSync(binPath)) {
360
- const reason = incompatibilityReason(binPath, platform, arch)
361
- if (reason === null) {
362
- info(`bin already present at ${binPath}, skipping.`)
363
- return
364
- }
365
- warn(`existing ${binPath} is incompatible (${reason}); removing and re-acquiring.`)
366
- try {
367
- unlinkSync(binPath)
368
- } catch (err) {
369
- const detail = err instanceof Error ? err.message : String(err)
370
- warn(
371
- `could not remove incompatible binary at ${binPath}: ${detail}. Continuing — download will overwrite.`,
372
- )
373
- }
374
- }
375
- if (existsSync(targetReleaseBin)) {
376
- const reason = incompatibilityReason(targetReleaseBin, platform, arch)
377
- if (reason === null) {
378
- info(`local cargo build already present at ${targetReleaseBin}, skipping.`)
379
- return
380
- }
381
- warn(
382
- `existing ${targetReleaseBin} is incompatible (${reason}); ignoring and acquiring a fresh binary.`,
383
- )
384
- }
385
-
386
- // Local dev override — copy a locally built binary instead of downloading.
387
- const override = process.env.SCRIBE_COMPILE_BIN
388
- if (override) {
389
- if (!existsSync(override)) {
390
- // User explicitly pointed at a path that doesn't exist — fail loudly.
391
- hardFail(`SCRIBE_COMPILE_BIN points to ${override} but that file does not exist.`)
392
- }
393
- copyFileSync(override, binPath)
394
- if (platform !== 'win32') {
395
- chmodSync(binPath, 0o755)
396
- }
397
- info(`copied ${override} -> ${binPath} (SCRIBE_COMPILE_BIN override).`)
398
- return
399
- }
400
-
401
- // Strategy A — try the GitHub Releases `latest/download` redirect.
402
- // On any failure (404 because no release exists yet, network unavailable,
403
- // empty response, write failure), fall through to Strategy B without
404
- // aborting the install.
405
- const baseUrl = `https://github.com/fellwork/aihu/releases/latest/download/${mapping.asset}`
406
- const sidecarUrl = `${baseUrl}.sha256`
407
- info(`fetching ${baseUrl}`)
408
- const downloaded = await tryDownload(baseUrl, binPath)
409
- if (downloaded.ok) {
410
- // Verify SHA256 against sidecar before trusting the binary (arch-4 §4.3).
411
- info(`verifying SHA256 against ${sidecarUrl}`)
412
- const verified = await verifySha256(binPath, sidecarUrl)
413
- if (verified.ok) {
414
- if (platform !== 'win32') {
415
- chmodSync(binPath, 0o755)
416
- }
417
- info(`installed binary at ${binPath} (SHA256 verified).`)
418
- return
419
- }
420
- if (verified.reason === 'digest mismatch') {
421
- // Integrity violation — DO NOT leave the bad binary on disk.
422
- try {
423
- unlinkSync(binPath)
424
- } catch {
425
- /* swallow — the next strategy will overwrite */
426
- }
427
- hardFail(
428
- `binary hash verification failed for ${mapping.asset}.\n` +
429
- ` expected: ${verified.expected ?? '(unknown)'}\n` +
430
- ` actual: ${verified.actual ?? '(unknown)'}\n` +
431
- 'Refusing to install a tampered binary. Re-run after the release ' +
432
- 'workflow republishes the asset, or set SCRIBE_COMPILE_BIN to a trusted local build.',
433
- )
434
- }
435
- // Sidecar fetch/parse failures: warn but don't hard-fail (the binary itself downloaded).
436
- // This lets pre-v1.1 releases (no sidecars) continue to install.
437
- warn(`SHA256 verification skipped: ${verified.reason}.`)
438
- if (platform !== 'win32') {
439
- chmodSync(binPath, 0o755)
440
- }
441
- info(`installed binary at ${binPath} (UNVERIFIED — sidecar unavailable).`)
442
- return
443
- }
444
-
445
- // 404 on aarch64-linux pre-v1.1 release publish — graceful fallthrough.
446
- if (downloaded.status === 404 && platform === 'linux' && arch === 'arm64') {
447
- info(
448
- 'aarch64-linux binary not yet published for this release; falling through to source build.',
449
- )
450
- } else {
451
- warn(`release-binary download from ${baseUrl} failed: ${downloaded.reason}.`)
452
- }
453
-
454
- // Strategy B — attempt a local Rust build. The Rust crate lives at
455
- // packages/compiler/Cargo.toml; `cargo build --release` produces the
456
- // binary at packages/compiler/target/release/aihu-compile<ext>, which
457
- // is where js/index.ts looks via SCRIBE_COMPILE_BIN ?? '../target/release/...'.
458
- if (tryLocalBuild(pkgDir)) {
459
- if (existsSync(targetReleaseBin)) {
460
- info(`compiler binary built locally at ${targetReleaseBin}.`)
461
- return
462
- }
463
- warn(`cargo build reported success but ${targetReleaseBin} is missing; falling through.`)
464
- }
465
-
466
- // Strategy C — give up, but DO NOT fail the install. The compiler may be
467
- // unused in this workspace (e.g. consumers only depend on @aihu/runtime
468
- // and @aihu/signals). Compile-time invocation will surface a clear
469
- // error if/when the user actually tries to compile a .aihu file.
470
- softExit(
471
- 'no compiler binary available after release-download and local-build fallbacks. ' +
472
- 'This is fine if you do not need to compile .aihu files in this workspace.',
473
- )
474
- }
475
-
476
- main().catch((err: unknown) => {
477
- const detail = err instanceof Error ? (err.stack ?? err.message) : String(err)
478
- hardFail(`Unexpected failure: ${detail}`)
479
- })