Migration
This page consolidates every breaking change in the .aihu surface and maps each old form to its replacement. Sources written against a retired surface will not compile against the current @aihu/compiler.
Codemod first. Most of these are mechanical. Run
npx aihu migrate --v2 <files…>(add--dry-runto preview), then read on for the cases it flags but cannot resolve. The passes are idempotent — re-running on migrated source is a no-op.
Current: aihu.config.ts → vite.config.ts
vite.config.ts is the canonical home for app and build config. The standalone aihu.config.ts is a legacy fallback and is being removed. If you have one, inline it:
// before — aihu.config.ts
import { defineConfig } from '@aihu/app'
export default defineConfig({
output: 'static',
site: { url: 'https://example.com' },
css: { shadowMode: 'light' },
})
// vite.config.ts
import aihuConfig from './aihu.config.ts'
export default defineConfig({ plugins: [viteAihuPlugin(aihuConfig)] })// after — one file
import { viteAihuPlugin } from '@aihu/app'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
viteAihuPlugin({
output: 'static',
site: { url: 'https://example.com' },
css: { shadowMode: 'light' },
}),
],
})Two things to check when you delete the file: your tsconfig.json include list probably names it, and comments elsewhere may point readers at it for site.url.
One exception. defineAihuConfig / aihu.config.ts remains the live registration path for @aihu/plugin-shaped compiler plugins. viteAihuPlugin()'s own plugins field takes Vite plugins — a different thing — so a compiler plugin still belongs in the standalone config. See Authoring Plugins.
Block framing — no HTML tags (C107)
v0 SFCs used HTML-tag framing. Blocks are @name { … }:
// before (v0)
<script setup>
const [count, setCount] = signal(0)
</script>
<template>
<button>{count}</button>
</template>
// after
@state {
import { signal } from '@aihu/signals'
const [count, setCount] = signal(0)
}
@template {
<button>{count}</button>
}The recognized blocks are @state, @template, @style, @agent, @route, and @meta. Any other @name block is C204 — including @props, whose hint steers you to the prop macro inside @state.
Template grammar v2 — the prefix-less template (C601–C611)
One rule: naked keywords, naked HTML attributes, naked framework vocabulary.
{expr}braces mean expression; quoted strings mean static; the dollar prefix retreats to@statemacros only. Every retired form is a hard compile error with a precisefix:hint — there is no deprecation period.
| Retired form | Code | Write instead |
|---|---|---|
{#if e}…{:else}…{/if} |
C601 | if={e} on the governed element; elseif={e} / else on the immediately following siblings; wrap multi-element branches in <group> |
{#each list as item}…{/each} |
C602 | each={item, i of list} key={keyExpr} on the repeated element (or <group>); the empty body moves to an empty sibling |
{@html expr} |
C603 | the html={expr} attribute |
{{ident}} double-brace |
C604 | single braces {ident}; an expression starting with an object literal needs a space |
<$if> / <$else> |
C605 | if={…} / else attributes |
$if= / $each= / $let= |
C606 | if={e}; each={item of list} — item-first, of-separated |
any other dollar-attribute ($on.click, $bind.value, $class:x, $key, $show, $html, $ref, …) |
C607 | on:click={h} (modifiers: on:click.prevent), bind:value={x}, class:x={c}, key={…}, show={…}, html={…}, ref={…}, bare once/raw, plain class={…} |
<$link> |
C608 | <a href={…} prefetch="…"> — carries SPA navigation, prefetch, replace, aria-current; auto-opts out for target="_blank", download, external origins and non-http(s) schemes; add reload to force a document load |
| any other dollar-element | C609 | the naked word: <slot>, <suspense>, <shield>, <outlet>, <router>, <navigate>, <guard>, <warp>, plus <group> |
elseif/else/empty not the immediate sibling |
C610 | move the branch element directly after its chain head; only whitespace and comments may sit between |
| unknown non-hyphenated element | C611 | fix the typo, or hyphenate the component tag |
Advisory lints: W601 — keyless each whose body contains components or stateful elements (add key={…}); W602 — non-empty static string on a boolean attribute (disabled="false" is truthy in HTML).
Macro collection form (C440 / C500)
Per-declaration macros inside @state collapsed into collection form — one object per macro kind:
// before
$lifecycle.mount: {
connect()
}
$lifecycle.dispose(() => disconnect())
// after
$lifecycle: {
mount: () => {
connect()
},
dispose: () => disconnect(),
}Agent metadata folded in the same way: per-name macros are retired in favour of describe: / expose: on @state collection entries, and the @agent block is dropped entirely when nothing but scope and rate-limit remain.
Cases the codemod cannot resolve
- The action colon form — rewrite by hand into a collection entry carrying
describe,exposeandhandler. - Agent metadata naming a plain
signal()binding —expose:anddescribe:attach to collection entries. A rawconst [x, setX] = signal(…)has no entry to carry them; wrap the value in a computed entry, or accept that the name is not agent-exposed. - Stale template spellings the codemod does not own — dot-form attribute bindings and dot-form class toggles.
The binary shadow API (the DA4 flip)
Breaking, one change with two faces. The shadow value set collapsed to a binary
'light' | 'shadow'—'open','closed'and'none'are gone — and pages and layouts now default to'light'.
Token migration is mechanical: 'open' → 'shadow', 'none' → 'light', 'closed' → 'shadow' (it never actually encapsulated). This applies to the shadow macro, the plugin-global css: { shadowMode }, the runtime's defineElement(tag, Ctor, { shadowMode }), and the CLI's --shadow flag.
'closed' is gone rather than renamed for a concrete reason: a closed root makes this.shadowRoot === null, which is exactly how aihu detects light DOM — so a closed root was misclassified and its content rendered into the host anyway.
Why light-DOM pages. AI crawlers do not execute JavaScript, so a page's primary content must reach them as server-rendered light DOM. Declarative Shadow DOM does not reliably fix this — spec-compliant extractors read a <template shadowrootmode> subtree as empty.
What to check after upgrading.
- Retired tokens fail loudly: the old shadow values are a C471 compile error, an old
css.shadowModethrows at config validation, and an old--shadowwarns and falls back. - To put a page back in shadow DOM, pin it in
@state. The pin outranks everything, including plugin-global config. - Page
@styleblocks now join the global cascade. A light-DOM page's styles are no longer trapped in a shadow root, so bare element selectors (h1 { … }) apply app-wide. Scope them under a page root class.
See Styling for how light-DOM components are isolated with @scope.
Type-checking after migration
Type-checking .aihu files compiles each to a virtual TypeScript sidecar with @aihu/compiler — and that compiler must be the same version your app builds with. Otherwise aihu-tsc may resolve an older compiler from its own dependency tree and reject perfectly valid new-grammar files, reporting only:
N .aihu file(s) could not be compiled, so nothing in them was type-checkedAlign the versions and the message goes away.
See also
- Authoring Components — the current SFC surface
- Styling · Theming
- Composition & Injection — tag naming and C450