G · 17 guides

Guides / Migration

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-run to 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.tsvite.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:

governedtsts326 B
// 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)] })
governedtsts285 B
// 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 { … }:

governedaihuaihu268 B
// 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 @state macros only. Every retired form is a hard compile error with a precise fix: 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:

governedaihuaihu175 B
// 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, expose and handler.
  • Agent metadata naming a plain signal() bindingexpose: and describe: attach to collection entries. A raw const [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.shadowMode throws at config validation, and an old --shadow warns and falls back.
  • To put a page back in shadow DOM, pin it in @state. The pin outranks everything, including plugin-global config.
  • Page @style blocks 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:

governedaihuaihu74 B
N .aihu file(s) could not be compiled, so nothing in them was type-checked

Align the versions and the message goes away.

See also