@aihu/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/bin/aihu-compile +0 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/js/postinstall.ts +354 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fellwork
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @aihu/compiler
|
|
2
|
+
|
|
3
|
+
> Single File Component (.aihu) compiler — Rust binary + JS glue.
|
|
4
|
+
|
|
5
|
+
⚠ **Native binary required.** This package downloads a pre-built `aihu-compile` binary at install time via `js/postinstall.ts` (see [WASM.md](https://github.com/fellwork/aihu/blob/main/packages/compiler/WASM.md)). Binaries are published per-platform from the `release.yml` workflow on every `v*` tag. SHA256-verified per arch-4 §4.3.
|
|
6
|
+
|
|
7
|
+
Part of the [aihu](https://github.com/fellwork/aihu) framework — agentic discovery and interaction, for human purpose.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @aihu/compiler
|
|
13
|
+
# or
|
|
14
|
+
bun add @aihu/compiler
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// vite.config.ts
|
|
21
|
+
import { defineConfig } from 'vite';
|
|
22
|
+
import { aihuCompiler } from '@aihu/compiler';
|
|
23
|
+
|
|
24
|
+
export default defineConfig({ plugins: [aihuCompiler()] });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Status
|
|
28
|
+
|
|
29
|
+
Early access (`0.1.x`). API may evolve before v1.1 GA. See the [v1.1 roadmap](https://github.com/fellwork/aihu/tree/main/docs/roadmap) for stability commitments.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT — see [LICENSE](https://github.com/fellwork/aihu/blob/main/LICENSE).
|
package/bin/aihu-compile
ADDED
|
Binary file
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//#region js/index.d.ts
|
|
2
|
+
interface VitePlugin {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
enforce?: 'pre' | 'post';
|
|
5
|
+
transform?: (code: string, id: string) => Promise<{
|
|
6
|
+
code: string;
|
|
7
|
+
map: null;
|
|
8
|
+
}> | {
|
|
9
|
+
code: string;
|
|
10
|
+
map: null;
|
|
11
|
+
} | null | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Options for `aihuCompilerPlugin()` (Plan 3.3 — Islands).
|
|
15
|
+
*/
|
|
16
|
+
interface AihuCompilerPluginOptions {
|
|
17
|
+
/**
|
|
18
|
+
* When `true` (default), components classified as `'static'` by
|
|
19
|
+
* `_classifyIsland()` are emitted with a minimal HTML-only registration
|
|
20
|
+
* shim that ships **zero** `@aihu/runtime` and `@aihu/signals` JS to
|
|
21
|
+
* the browser. Components classified as `'interactive'` retain the
|
|
22
|
+
* full runtime path.
|
|
23
|
+
*
|
|
24
|
+
* Setting `islands: false` opts every component back into the unified
|
|
25
|
+
* runtime path (Plan 3.2 baseline behaviour).
|
|
26
|
+
*/
|
|
27
|
+
islands?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Project-wide shadow-DOM mode applied to every `.aihu` SFC compiled
|
|
30
|
+
* by this plugin instance. When set, the plugin post-processes the
|
|
31
|
+
* compiled JS to inject `, { shadowMode: '<mode>' }` as the third arg
|
|
32
|
+
* to the emitted `defineElement(tag, defineComponent(...))` call.
|
|
33
|
+
*
|
|
34
|
+
* - `'open'` — default browser behaviour (shadow root, externally readable).
|
|
35
|
+
* - `'closed'` — shadow root, externally hidden.
|
|
36
|
+
* - `'none'` — **no shadow root.** The component mounts into its own
|
|
37
|
+
* element. Required for global utility-class CSS frameworks
|
|
38
|
+
* like Tailwind, UnoCSS, Pico that rely on the cascade.
|
|
39
|
+
*
|
|
40
|
+
* Per-component override is not yet supported via SFC syntax (post-v1).
|
|
41
|
+
* For per-component control today, hand-author the component with
|
|
42
|
+
* `defineElement(tag, Ctor, { shadowMode: '...' })`.
|
|
43
|
+
*/
|
|
44
|
+
shadowMode?: 'open' | 'closed' | 'none';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Inject `{ shadowMode: '...' }` as the third argument to the emitted
|
|
48
|
+
* `defineElement('tag', defineComponent(...))` call. The compiler emits
|
|
49
|
+
* exactly two arguments today; this rewrites the closing of the
|
|
50
|
+
* defineElement call to include the options object. Idempotent — leaves
|
|
51
|
+
* code untouched when the closer is not in the expected shape.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
declare function _injectShadowMode(code: string, mode: 'open' | 'closed' | 'none'): string;
|
|
56
|
+
/**
|
|
57
|
+
* Classify the compiled output of a single `.aihu` module as either a
|
|
58
|
+
* **static** island (no reactive state — purely declarative DOM) or an
|
|
59
|
+
* **interactive** island (uses the signals reactivity system).
|
|
60
|
+
*
|
|
61
|
+
* The heuristic is intentionally conservative: any source-level reference
|
|
62
|
+
* to `signal(`, `computed(`, `effect(`, or `setSignal(` flips the file to
|
|
63
|
+
* `'interactive'`. False positives (e.g. a string literal containing
|
|
64
|
+
* `signal(`) are tolerable — they only forfeit the static-island
|
|
65
|
+
* optimisation. False negatives are forbidden: a static-classified file
|
|
66
|
+
* MUST NOT depend on the signals runtime at execution time.
|
|
67
|
+
*
|
|
68
|
+
* Plan 3.3 / acceptance criterion 1.
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
declare function _classifyIsland(compiledCode: string): 'static' | 'interactive';
|
|
73
|
+
/**
|
|
74
|
+
* Rewrite an interactive-island module so its `connectedCallback` waits
|
|
75
|
+
* for the element to scroll into view before mounting. Plan 3.3 — applied
|
|
76
|
+
* only when the consumer adds `defer` to the custom element tag (e.g.
|
|
77
|
+
* `<my-counter defer>`); the runtime helper checks the attribute and
|
|
78
|
+
* either mounts immediately or registers an `IntersectionObserver`.
|
|
79
|
+
*
|
|
80
|
+
* Implementation: the helper is added as a `_hydrateOnVisible` import
|
|
81
|
+
* from `@aihu/runtime`, and the compiler-emitted `defineElement(...)`
|
|
82
|
+
* call is wrapped in a `defineElement` that intercepts `connectedCallback`
|
|
83
|
+
* to honour the `defer` attribute.
|
|
84
|
+
*
|
|
85
|
+
* The whole indirection is tree-shaken when no `.aihu` module reaches
|
|
86
|
+
* this branch, because `_hydrateOnVisible` is exported from its own
|
|
87
|
+
* sibling module inside `@aihu/runtime`.
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
declare function _buildDeferredHydration(compiledCode: string, elementTag: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Build a static-island shim for a compiled module.
|
|
94
|
+
*
|
|
95
|
+
* The compiled module emitted by the Rust codegen has the shape:
|
|
96
|
+
*
|
|
97
|
+
* import { branch, leaf, slot } from '@aihu/arbor'
|
|
98
|
+
* import { defineComponent, defineElement } from '@aihu/runtime'
|
|
99
|
+
* defineElement('tag', defineComponent((_ctx) => { return <tree> }))
|
|
100
|
+
*
|
|
101
|
+
* For a static island we know `<tree>` contains no `signal(`/`computed(`
|
|
102
|
+
* calls. We can therefore:
|
|
103
|
+
*
|
|
104
|
+
* 1. Drop the `@aihu/runtime` import (saves ~600 B gz of defineComponent
|
|
105
|
+
* + defineElement + bootstrap glue).
|
|
106
|
+
* 2. Replace `defineElement(tag, defineComponent(setup))` with a tiny
|
|
107
|
+
* inline class that mounts the tree directly via `mount()` (which the
|
|
108
|
+
* arbor barrel already exports).
|
|
109
|
+
* 3. Tag the file with a `// SCRIBE_STATIC_ISLAND` comment so consumers
|
|
110
|
+
* can audit which routes shipped zero-JS-runtime.
|
|
111
|
+
*
|
|
112
|
+
* Falls back to the original code if the regex shape does not match
|
|
113
|
+
* (defensive: a future compiler change must opt back into static-island
|
|
114
|
+
* emission explicitly rather than silently break).
|
|
115
|
+
*
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
declare function _buildStaticIsland(compiledCode: string, elementTag: string): string;
|
|
119
|
+
/**
|
|
120
|
+
* Compile a .aihu source string to TypeScript.
|
|
121
|
+
* map is null — source maps are deferred to v1 (OQ-C8)
|
|
122
|
+
*/
|
|
123
|
+
declare function transform(source: string, id: string): {
|
|
124
|
+
code: string;
|
|
125
|
+
map: null;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Inject `_setMount(mount)` + `_setSignal(signal)` auto-wiring into a compiled
|
|
129
|
+
* `.aihu` module. Adds the necessary symbols to existing imports and inserts
|
|
130
|
+
* the boot calls right after the last `import` statement.
|
|
131
|
+
*
|
|
132
|
+
* @internal
|
|
133
|
+
*/
|
|
134
|
+
declare function _injectAutoWiring(code: string): string;
|
|
135
|
+
/**
|
|
136
|
+
* Vite plugin that compiles .aihu files to TypeScript during build and dev.
|
|
137
|
+
*
|
|
138
|
+
* Use `enforce: 'pre'` so the hook fires before Vite/Rollup's built-in
|
|
139
|
+
* parsers attempt to process the raw .aihu content as JavaScript.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* // vite.config.ts
|
|
143
|
+
* import { aihuCompilerPlugin } from '@aihu/compiler'
|
|
144
|
+
* export default { plugins: [aihuCompilerPlugin()] }
|
|
145
|
+
*
|
|
146
|
+
* **Known Limitation — Bun + Rollup4 ESM incompatibility (v0):**
|
|
147
|
+
*
|
|
148
|
+
* `bun vite build` fails in the `fixtures/vite-counter` fixture with two
|
|
149
|
+
* cascading errors:
|
|
150
|
+
*
|
|
151
|
+
* 1. **Missing devDependency:** `vite` is declared only as an optional
|
|
152
|
+
* `peerDependency` in `packages/compiler/package.json`. Bun does not
|
|
153
|
+
* install optional peers automatically, so `bun vite build` exits
|
|
154
|
+
* immediately with `Cannot find package 'vite'`.
|
|
155
|
+
*
|
|
156
|
+
* 2. **Bun + Rollup4 bridge:** Even with Vite installed, Bun processes
|
|
157
|
+
* `vite.config.ts` through its own internal bundler before handing off
|
|
158
|
+
* to Rollup4. When `@aihu/compiler` is resolved from the workspace
|
|
159
|
+
* symlink (`dist/index.js`), Bun's ESM loader evaluates the module at
|
|
160
|
+
* config-load time. The subprocess call inside `transform()` depends on
|
|
161
|
+
* the Rust binary being at `../bin/aihu-compile` relative to `dist/`
|
|
162
|
+
* (written by the postinstall hook). In a dev workspace where postinstall
|
|
163
|
+
* has not run, this path does not exist and `execFileSync` throws. Bun surfaces
|
|
164
|
+
* the error as a config-load failure, not a per-file transform error,
|
|
165
|
+
* causing the entire build to abort before any `.aihu` file is
|
|
166
|
+
* processed.
|
|
167
|
+
*
|
|
168
|
+
* **Workaround (v0):** Use `bun run integrate.ts` directly from
|
|
169
|
+
* `packages/compiler/fixtures/vite-counter/`. This script calls
|
|
170
|
+
* `transform()` from `@aihu/compiler` without involving Vite or Rollup.
|
|
171
|
+
* Preconditions: (1) `cargo build --release` in `packages/compiler/`,
|
|
172
|
+
* (2) `bun install` at the repo root.
|
|
173
|
+
*
|
|
174
|
+
* **v1 resolution:** Add `vite` as a `devDependency` in
|
|
175
|
+
* `packages/compiler/package.json`; add a WASM or pre-built binary
|
|
176
|
+
* strategy so the Rust binary is bundled with the npm package and does not
|
|
177
|
+
* require a separate `cargo build --release` step.
|
|
178
|
+
*/
|
|
179
|
+
declare function aihuCompilerPlugin(options?: AihuCompilerPluginOptions): VitePlugin;
|
|
180
|
+
//#endregion
|
|
181
|
+
export { AihuCompilerPluginOptions, _buildDeferredHydration, _buildStaticIsland, _classifyIsland, _injectAutoWiring, _injectShadowMode, aihuCompilerPlugin, transform };
|
|
182
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../js/index.ts"],"mappings":";UAoBU,UAAA;EAAA,SACC,IAAA;EACT,OAAA;EACA,SAAA,IACE,IAAA,UACA,EAAA,aACG,OAAA;IAAU,IAAA;IAAc,GAAA;EAAA;IAAiB,IAAA;IAAc,GAAA;EAAA;AAAA;;;;UAM7C,yBAAA;EANgD;;AAMjE;;;;;AAyCA;;;EA9BE,OAAA;EA8B8E;AA0BhF;;;;;AA8GA;;;;;AA2GA;;;;;EA/PE,UAAA;AAAA;;;;;;;;;AAmUF;iBAvTgB,iBAAA,CAAkB,IAAA,UAAc,IAAA;;;;AA2bhD;;;;;;;;;;;;;iBAjagB,eAAA,CAAgB,YAAA;;;;;;;;;;;;;;;;;;;iBA8GhB,uBAAA,CAAwB,YAAA,UAAsB,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2G9C,kBAAA,CAAmB,YAAA,UAAsB,UAAA;;;;;iBAiDzC,SAAA,CAAU,MAAA,UAAgB,EAAA;EAAe,IAAA;EAAc,GAAA;AAAA;;;;;;;;iBAmBvD,iBAAA,CAAkB,IAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoIlB,kBAAA,CAAmB,OAAA,GAAU,yBAAA,GAA4B,UAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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`:``,o=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/\b(?:signal|computed|effect|setSignal)\s*\(/.test(e)?`interactive`:`static`}function l(e){let t=/defineElement\(\s*['"]([^'"]+)['"]/m.exec(e);return t?t[1]??null:null}function u(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
|
+
export { __aihu_setup__ as default }
|
|
3
|
+
|
|
4
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && import.meta.hot) {
|
|
5
|
+
import.meta.hot.accept((newModule) => {
|
|
6
|
+
if (!newModule) return
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
const newSetup = (newModule as any)['default']
|
|
9
|
+
if (typeof newSetup !== 'function') return
|
|
10
|
+
document.querySelectorAll(${JSON.stringify(t)}).forEach((el) => {
|
|
11
|
+
_hmrReplace(el as HTMLElement, newSetup)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
`;return`let __aihu_setup__: ((ctx: any) => any) | undefined
|
|
16
|
+
`+n+r}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(`_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
|
+
export `);return i===r&&(i=r.replace(/\)\s*\)\s*$/,`)))
|
|
18
|
+
`)),i===r?e:`
|
|
19
|
+
// Plan 3.3 (Islands) — defer attribute support. Wraps the constructor
|
|
20
|
+
// returned by defineComponent so instances bearing the \`defer\` attribute
|
|
21
|
+
// hydrate lazily via IntersectionObserver. Bare instances retain the
|
|
22
|
+
// eager Plan 3.2 hydration path.
|
|
23
|
+
function __aihu_wrap_defer__<T extends typeof HTMLElement>(Ctor: T): T {
|
|
24
|
+
const orig = (Ctor.prototype as unknown as { connectedCallback?: () => void }).connectedCallback
|
|
25
|
+
if (typeof orig !== 'function') return Ctor
|
|
26
|
+
;(Ctor.prototype as unknown as { connectedCallback: () => void }).connectedCallback = function (this: HTMLElement) {
|
|
27
|
+
if (this.hasAttribute('defer')) {
|
|
28
|
+
_hydrateOnVisible(this, () => orig.call(this))
|
|
29
|
+
} else {
|
|
30
|
+
orig.call(this)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Ctor
|
|
34
|
+
}
|
|
35
|
+
`+i}function f(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
|
+
mount(__aihu_setup__({ host: root, element: this }), root)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
`)}`}function p(n,r){return{code:e(o,[`--stdin`,`--tag`,t(r,`.aihu`),`--path`,r],{input:n,encoding:`utf8`}),map:null}}function m(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'
|
|
40
|
+
`+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(`
|
|
41
|
+
`),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(`
|
|
42
|
+
`)),t}function h(e){let t=e?.islands!==!1,n=e?.shadowMode;return{name:`aihu-compiler`,enforce:`pre`,transform(e,r){let i=r.split(`?`)[0];if(i.endsWith(`.aihu`))return(async()=>{let r=p(e,i),a=n==null?r.code:s(r.code,n),o=l(a),h;t&&o!==null&&c(a)===`static`?h=f(a,o):o===null?(h=a,h=m(h)):(h=u(a,o),h=d(h,o),h=m(h));try{let{transformWithEsbuild:e}=await import(`vite`);return{code:(await e(h,`component.ts`,{target:`esnext`,sourcemap:!1})).code,map:null}}catch{return{code:h,map:null}}})()}}}export{d as _buildDeferredHydration,f as _buildStaticIsland,c as _classifyIsland,m as _injectAutoWiring,s as _injectShadowMode,h as aihuCompilerPlugin,p as transform};
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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]).\nconst ext = process.platform === 'win32' ? '.exe' : ''\nconst binPath: string =\n process.env.SCRIBE_COMPILE_BIN ??\n resolve(dirname(fileURLToPath(import.meta.url)), `../bin/aihu-compile${ext}`)\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 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/**\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 * 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 `signal(`, `computed(`, `effect(`, or `setSignal(` flips the file to\n * `'interactive'`. False positives (e.g. a string literal containing\n * `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 * Plan 3.3 / acceptance criterion 1.\n *\n * @internal\n */\nexport function _classifyIsland(compiledCode: string): 'static' | 'interactive' {\n // Match call sites of the four reactive 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)\\s*\\(/.test(compiledCode) ? 'interactive' : '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 * 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 */\nexport function transform(source: string, id: string): { code: string; map: null } {\n const stem = basename(id, '.aihu')\n const code = execFileSync(binPath, ['--stdin', '--tag', stem, '--path', id], {\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 * 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 */\nexport function aihuCompilerPlugin(options?: AihuCompilerPluginOptions): VitePlugin {\n const islandsEnabled = options?.islands !== false\n const shadowMode = options?.shadowMode\n return {\n name: 'aihu-compiler',\n enforce: 'pre',\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 const result = transform(code, rawId)\n const compiled =\n shadowMode != null ? _injectShadowMode(result.code, shadowMode) : result.code\n const elementTag = _extractElementTag(compiled)\n\n let out: string\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 (islandsEnabled && elementTag !== null && _classifyIsland(compiled) === 'static') {\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 esbuild TypeScript-strip step when a plugin\n // returns code for a non-.ts ID — so we strip types here ourselves using\n // Vite's own transformWithEsbuild API (always available in a Vite context).\n try {\n const { transformWithEsbuild } = await import('vite')\n const stripped = await transformWithEsbuild(out, 'component.ts', {\n target: 'esnext',\n sourcemap: false,\n })\n return { code: stripped.code, map: null }\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,354 @@
|
|
|
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 → no-op, exit 0.
|
|
8
|
+
* bin/aihu-compile<ext> OR
|
|
9
|
+
* target/release/aihu-compile<ext>
|
|
10
|
+
* 3. SCRIBE_COMPILE_BIN=<path> → copy that path → bin/, exit 0.
|
|
11
|
+
* 4. GitHub Releases download → bin/aihu-compile<ext>, verify SHA256,
|
|
12
|
+
* exit 0. (arch-4 §4.3 — sidecar
|
|
13
|
+
* verification implemented v1.1.)
|
|
14
|
+
* 5. Local `cargo build --release` → target/release/aihu-compile<ext>,
|
|
15
|
+
* exit 0.
|
|
16
|
+
* 6. Everything failed → log warning, exit 0 anyway. The user
|
|
17
|
+
* will need to either provide
|
|
18
|
+
* SCRIBE_COMPILE_BIN or run
|
|
19
|
+
* `cargo build --release` themselves
|
|
20
|
+
* before invoking the compiler.
|
|
21
|
+
*
|
|
22
|
+
* The hard rule: this script MUST exit 0 in every "no binary acquired"
|
|
23
|
+
* branch. A non-zero exit aborts `bun install`, which prevents workspace
|
|
24
|
+
* symlinks from being created and breaks every downstream package that
|
|
25
|
+
* imports a `@aihu/*` sibling. Compile-time failure (when the user
|
|
26
|
+
* actually invokes the compiler without a binary) is acceptable and
|
|
27
|
+
* recoverable; install-time failure is not.
|
|
28
|
+
*
|
|
29
|
+
* Hard-fail exit 1 is reserved for these cases:
|
|
30
|
+
* - SCRIBE_COMPILE_BIN is set but points at a missing file (user error,
|
|
31
|
+
* surface immediately rather than silently swallow).
|
|
32
|
+
* - SHA256 verification of a downloaded binary fails (integrity violation —
|
|
33
|
+
* do NOT run a tampered binary).
|
|
34
|
+
* - Catastrophic unexpected exception (programming error in this script).
|
|
35
|
+
*
|
|
36
|
+
* Local dev override: if SCRIBE_COMPILE_BIN env var is set, that path is
|
|
37
|
+
* used instead of downloading. This lets contributors who built from
|
|
38
|
+
* source via `cargo build --release` point the compiler at their build.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { spawnSync } from 'node:child_process'
|
|
42
|
+
import { createHash } from 'node:crypto'
|
|
43
|
+
import {
|
|
44
|
+
chmodSync,
|
|
45
|
+
copyFileSync,
|
|
46
|
+
existsSync,
|
|
47
|
+
mkdirSync,
|
|
48
|
+
readFileSync,
|
|
49
|
+
unlinkSync,
|
|
50
|
+
writeFileSync,
|
|
51
|
+
} from 'node:fs'
|
|
52
|
+
import { dirname, resolve } from 'node:path'
|
|
53
|
+
import { fileURLToPath } from 'node:url'
|
|
54
|
+
|
|
55
|
+
interface AssetMapping {
|
|
56
|
+
asset: string
|
|
57
|
+
ext: '' | '.exe'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveAsset(platform: NodeJS.Platform, arch: string): AssetMapping | null {
|
|
61
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
62
|
+
return { asset: 'aihu-compile-darwin-arm64', ext: '' }
|
|
63
|
+
}
|
|
64
|
+
if (platform === 'darwin' && arch === 'x64') {
|
|
65
|
+
return { asset: 'aihu-compile-darwin-x64', ext: '' }
|
|
66
|
+
}
|
|
67
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
68
|
+
return { asset: 'aihu-compile-linux-x64', ext: '' }
|
|
69
|
+
}
|
|
70
|
+
// arch-4 §4.2 — aarch64-linux added in v1.1 release matrix.
|
|
71
|
+
if (platform === 'linux' && arch === 'arm64') {
|
|
72
|
+
return { asset: 'aihu-compile-linux-arm64', ext: '' }
|
|
73
|
+
}
|
|
74
|
+
if (platform === 'win32' && arch === 'x64') {
|
|
75
|
+
return { asset: 'aihu-compile-windows-x64.exe', ext: '.exe' }
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const TAG = '[@aihu/compiler postinstall]'
|
|
81
|
+
|
|
82
|
+
function info(msg: string): void {
|
|
83
|
+
process.stdout.write(`${TAG} ${msg}\n`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function warn(msg: string): void {
|
|
87
|
+
process.stderr.write(`${TAG} WARN: ${msg}\n`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hardFail(msg: string): never {
|
|
91
|
+
process.stderr.write(`${TAG} ERROR: ${msg}\n`)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function softExit(msg: string): never {
|
|
96
|
+
warn(msg)
|
|
97
|
+
warn(
|
|
98
|
+
'bun install will continue. To enable the compiler later, set ' +
|
|
99
|
+
'SCRIBE_COMPILE_BIN to a built binary path or run ' +
|
|
100
|
+
'`cargo build --release` from packages/compiler/ when a Rust ' +
|
|
101
|
+
'toolchain is available.',
|
|
102
|
+
)
|
|
103
|
+
process.exit(0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface DownloadResult {
|
|
107
|
+
ok: boolean
|
|
108
|
+
reason?: string
|
|
109
|
+
status?: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function tryDownload(url: string, dest: string): Promise<DownloadResult> {
|
|
113
|
+
let response: Response
|
|
114
|
+
try {
|
|
115
|
+
response = await fetch(url, { redirect: 'follow' })
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
118
|
+
return { ok: false, reason: `network error: ${detail}` }
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
reason: `HTTP ${response.status} ${response.statusText}`,
|
|
124
|
+
status: response.status,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let buf: Buffer
|
|
128
|
+
try {
|
|
129
|
+
buf = Buffer.from(await response.arrayBuffer())
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
132
|
+
return { ok: false, reason: `body read failed: ${detail}` }
|
|
133
|
+
}
|
|
134
|
+
if (buf.length === 0) {
|
|
135
|
+
return { ok: false, reason: 'response body was empty' }
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
writeFileSync(dest, buf)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
141
|
+
return { ok: false, reason: `write failed: ${detail}` }
|
|
142
|
+
}
|
|
143
|
+
return { ok: true }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Verify a downloaded binary's SHA256 digest against the matching `.sha256`
|
|
148
|
+
* sidecar from GitHub Releases (arch-4 §4.3).
|
|
149
|
+
*
|
|
150
|
+
* Returns true on match, false on any failure (network, mismatch, parse).
|
|
151
|
+
* The caller decides whether to fail hard or fall through; for download path
|
|
152
|
+
* a mismatch is hardFail (integrity violation), other reasons soft-warn.
|
|
153
|
+
*/
|
|
154
|
+
async function verifySha256(
|
|
155
|
+
binaryPath: string,
|
|
156
|
+
sidecarUrl: string,
|
|
157
|
+
): Promise<{ ok: boolean; reason?: string; expected?: string; actual?: string }> {
|
|
158
|
+
let response: Response
|
|
159
|
+
try {
|
|
160
|
+
response = await fetch(sidecarUrl, { redirect: 'follow' })
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
163
|
+
return { ok: false, reason: `sidecar network error: ${detail}` }
|
|
164
|
+
}
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: `sidecar HTTP ${response.status} ${response.statusText}`,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const sidecarText = (await response.text()).trim()
|
|
172
|
+
// Sidecar format: hex digest (lowercase, 64 chars). Some tools prepend a filename;
|
|
173
|
+
// accept either `<hash>` or `<hash> <name>` and extract the first whitespace-delimited token.
|
|
174
|
+
const expected = sidecarText.split(/\s+/)[0]?.toLowerCase()
|
|
175
|
+
if (!expected || !/^[0-9a-f]{64}$/i.test(expected)) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
reason: `malformed sidecar (expected 64-char hex, got "${sidecarText.slice(0, 80)}")`,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let actual: string
|
|
183
|
+
try {
|
|
184
|
+
const fileBuf = readFileSync(binaryPath)
|
|
185
|
+
actual = createHash('sha256').update(fileBuf).digest('hex')
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
188
|
+
return { ok: false, reason: `local hash failed: ${detail}` }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (actual !== expected) {
|
|
192
|
+
return { ok: false, reason: 'digest mismatch', expected, actual }
|
|
193
|
+
}
|
|
194
|
+
return { ok: true }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function tryLocalBuild(pkgDir: string): boolean {
|
|
198
|
+
// Check for cargo first — quick probe without spawning a build.
|
|
199
|
+
const probe = spawnSync('cargo', ['--version'], {
|
|
200
|
+
stdio: 'ignore',
|
|
201
|
+
shell: false,
|
|
202
|
+
})
|
|
203
|
+
if (probe.error || probe.status !== 0) {
|
|
204
|
+
info('cargo not available; skipping local Rust build fallback.')
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
info('attempting local `cargo build --release` (Rust toolchain detected)…')
|
|
208
|
+
const build = spawnSync('cargo', ['build', '--release'], {
|
|
209
|
+
cwd: pkgDir,
|
|
210
|
+
stdio: 'inherit',
|
|
211
|
+
shell: false,
|
|
212
|
+
})
|
|
213
|
+
if (build.error || build.status !== 0) {
|
|
214
|
+
warn(
|
|
215
|
+
`local cargo build failed (status=${build.status ?? 'unknown'}). The compiler binary was not built.`,
|
|
216
|
+
)
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
info('local cargo build succeeded.')
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function main(): Promise<void> {
|
|
224
|
+
if (process.env.SCRIBE_SKIP_POSTINSTALL) {
|
|
225
|
+
info('SCRIBE_SKIP_POSTINSTALL set; skipping all binary-acquisition steps.')
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const platform = process.platform
|
|
230
|
+
const arch = process.arch
|
|
231
|
+
|
|
232
|
+
const mapping = resolveAsset(platform, arch)
|
|
233
|
+
if (!mapping) {
|
|
234
|
+
softExit(
|
|
235
|
+
`unsupported platform: ${platform}/${arch} ` +
|
|
236
|
+
'(supported: darwin/arm64, darwin/x64, linux/x64, linux/arm64, win32/x64).',
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
241
|
+
const binDir = resolve(pkgDir, 'bin')
|
|
242
|
+
const binPath = resolve(binDir, `aihu-compile${mapping.ext}`)
|
|
243
|
+
const targetReleaseBin = resolve(pkgDir, 'target', 'release', `aihu-compile${mapping.ext}`)
|
|
244
|
+
|
|
245
|
+
// Ensure target directory exists before any write operations.
|
|
246
|
+
if (!existsSync(binDir)) {
|
|
247
|
+
mkdirSync(binDir, { recursive: true })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Idempotency: nothing to do if a binary is already in place at either
|
|
251
|
+
// the released-asset path (bin/) or the local-build path (target/release).
|
|
252
|
+
if (existsSync(binPath)) {
|
|
253
|
+
info(`bin already present at ${binPath}, skipping.`)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
if (existsSync(targetReleaseBin)) {
|
|
257
|
+
info(`local cargo build already present at ${targetReleaseBin}, skipping.`)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Local dev override — copy a locally built binary instead of downloading.
|
|
262
|
+
const override = process.env.SCRIBE_COMPILE_BIN
|
|
263
|
+
if (override) {
|
|
264
|
+
if (!existsSync(override)) {
|
|
265
|
+
// User explicitly pointed at a path that doesn't exist — fail loudly.
|
|
266
|
+
hardFail(`SCRIBE_COMPILE_BIN points to ${override} but that file does not exist.`)
|
|
267
|
+
}
|
|
268
|
+
copyFileSync(override, binPath)
|
|
269
|
+
if (platform !== 'win32') {
|
|
270
|
+
chmodSync(binPath, 0o755)
|
|
271
|
+
}
|
|
272
|
+
info(`copied ${override} -> ${binPath} (SCRIBE_COMPILE_BIN override).`)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Strategy A — try the GitHub Releases `latest/download` redirect.
|
|
277
|
+
// On any failure (404 because no release exists yet, network unavailable,
|
|
278
|
+
// empty response, write failure), fall through to Strategy B without
|
|
279
|
+
// aborting the install.
|
|
280
|
+
const baseUrl = `https://github.com/fellwork/aihu/releases/latest/download/${mapping.asset}`
|
|
281
|
+
const sidecarUrl = `${baseUrl}.sha256`
|
|
282
|
+
info(`fetching ${baseUrl}`)
|
|
283
|
+
const downloaded = await tryDownload(baseUrl, binPath)
|
|
284
|
+
if (downloaded.ok) {
|
|
285
|
+
// Verify SHA256 against sidecar before trusting the binary (arch-4 §4.3).
|
|
286
|
+
info(`verifying SHA256 against ${sidecarUrl}`)
|
|
287
|
+
const verified = await verifySha256(binPath, sidecarUrl)
|
|
288
|
+
if (verified.ok) {
|
|
289
|
+
if (platform !== 'win32') {
|
|
290
|
+
chmodSync(binPath, 0o755)
|
|
291
|
+
}
|
|
292
|
+
info(`installed binary at ${binPath} (SHA256 verified).`)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (verified.reason === 'digest mismatch') {
|
|
296
|
+
// Integrity violation — DO NOT leave the bad binary on disk.
|
|
297
|
+
try {
|
|
298
|
+
unlinkSync(binPath)
|
|
299
|
+
} catch {
|
|
300
|
+
/* swallow — the next strategy will overwrite */
|
|
301
|
+
}
|
|
302
|
+
hardFail(
|
|
303
|
+
`binary hash verification failed for ${mapping.asset}.\n` +
|
|
304
|
+
` expected: ${verified.expected ?? '(unknown)'}\n` +
|
|
305
|
+
` actual: ${verified.actual ?? '(unknown)'}\n` +
|
|
306
|
+
'Refusing to install a tampered binary. Re-run after the release ' +
|
|
307
|
+
'workflow republishes the asset, or set SCRIBE_COMPILE_BIN to a trusted local build.',
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
// Sidecar fetch/parse failures: warn but don't hard-fail (the binary itself downloaded).
|
|
311
|
+
// This lets pre-v1.1 releases (no sidecars) continue to install.
|
|
312
|
+
warn(`SHA256 verification skipped: ${verified.reason}.`)
|
|
313
|
+
if (platform !== 'win32') {
|
|
314
|
+
chmodSync(binPath, 0o755)
|
|
315
|
+
}
|
|
316
|
+
info(`installed binary at ${binPath} (UNVERIFIED — sidecar unavailable).`)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 404 on aarch64-linux pre-v1.1 release publish — graceful fallthrough.
|
|
321
|
+
if (downloaded.status === 404 && platform === 'linux' && arch === 'arm64') {
|
|
322
|
+
info(
|
|
323
|
+
'aarch64-linux binary not yet published for this release; falling through to source build.',
|
|
324
|
+
)
|
|
325
|
+
} else {
|
|
326
|
+
warn(`release-binary download from ${baseUrl} failed: ${downloaded.reason}.`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Strategy B — attempt a local Rust build. The Rust crate lives at
|
|
330
|
+
// packages/compiler/Cargo.toml; `cargo build --release` produces the
|
|
331
|
+
// binary at packages/compiler/target/release/aihu-compile<ext>, which
|
|
332
|
+
// is where js/index.ts looks via SCRIBE_COMPILE_BIN ?? '../target/release/...'.
|
|
333
|
+
if (tryLocalBuild(pkgDir)) {
|
|
334
|
+
if (existsSync(targetReleaseBin)) {
|
|
335
|
+
info(`compiler binary built locally at ${targetReleaseBin}.`)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
warn(`cargo build reported success but ${targetReleaseBin} is missing; falling through.`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Strategy C — give up, but DO NOT fail the install. The compiler may be
|
|
342
|
+
// unused in this workspace (e.g. consumers only depend on @aihu/runtime
|
|
343
|
+
// and @aihu/signals). Compile-time invocation will surface a clear
|
|
344
|
+
// error if/when the user actually tries to compile a .aihu file.
|
|
345
|
+
softExit(
|
|
346
|
+
'no compiler binary available after release-download and local-build fallbacks. ' +
|
|
347
|
+
'This is fine if you do not need to compile .aihu files in this workspace.',
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
main().catch((err: unknown) => {
|
|
352
|
+
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err)
|
|
353
|
+
hardFail(`Unexpected failure: ${detail}`)
|
|
354
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aihu/compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"aihu-compile": "./bin/aihu-compile"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"js/postinstall.ts",
|
|
21
|
+
"bin",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rolldown -c",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"postinstall": "bun run js/postinstall.ts",
|
|
30
|
+
"prepublishOnly": "bun run build"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"vite": ">=5.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"vite": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"description": "Single File Component (.aihu) compiler — Rust binary + JS glue.",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/fellwork/aihu.git",
|
|
44
|
+
"directory": "packages/compiler"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/fellwork/aihu/tree/main/packages/compiler#readme",
|
|
47
|
+
"bugs": "https://github.com/fellwork/aihu/issues",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|