API / @aihu/app

@aihu/app

App & routing

Top-level app integration — wires runtime, router, and adapters into a Vite app.

version
10.0.0
exports
42
values
16
types
26
01

assertOutletPresent

function
function assertOutletPresent(template: string, outletId: string): string | null
02

collectAihuModules

function
function collectAihuModules(plugins: ReadonlyArray<unknown>): Map<string, unknown>

Collect every aihu module's resolved options from a plugin array.

03

createApp

function
function createApp(config?: AppConfig): AppHandle
04

createSsrDocument

function
function createSsrDocument(config: SsrDocumentConfig): SsrDocumentWrapper

Build the request-time document wrapper.

05

criticalPath

function
function criticalPath(opts: CriticalPathOptions = {}): Plugin
06

declareAihuModule

function
function declareAihuModule<TOptions, TPlugins extends readonly unknown[]>( aihuModule: string, options: TOptions, plugins: TPlugins, ): TPlugins

Attach the module contract to a plugin (or plugin array).

07

defineConfig

function
function defineConfig(config: AihuConfig): AihuConfig

Define the aihu application configuration.

08

injectIntoOutletId

function
function injectIntoOutletId(html: string, content: string, outletId: string): string | null

Inject rendered route content into the outlet element of an HTML template.

09

loadAihuConfig

function
async function loadAihuConfig( root: string, options: { readonly mode?: string; readonly command?: 'build' | 'serve' } = {}, ): Promise<LoadedAihuConfig | null>

Load an aihu project's config from its Vite config file.

10

ssrOutDirFor

function
function ssrOutDirFor(clientOutDir: string): string

Where the `'ssr'` environment writes: a SIBLING of the client outDir, never inside it.

11

validateAihuConfig

function
function validateAihuConfig(config: AihuConfig): void

Validate a config object without the `defineConfig` ceremony.

12

viteAihuPlugin

function
function viteAihuPlugin(config?: AihuConfig): PluginOption[]

viteAihuPlugin() — composed Vite plugin for aihu SPA projects.

13

AIHU_CONFIG_KEYS

const
const AIHU_CONFIG_KEYS: ReadonlyArray<string>

Keys aihu owns, derived from the schema rather than hand-listed.

14

AIHU_CONFIG_PLUGIN

const
const AIHU_CONFIG_PLUGIN

Plugin name carrying the config handle.

15

DEFAULT_OUTLET_ID

constagent
const DEFAULT_OUTLET_ID

The outlet element id every aihu surface agrees on when nothing overrides it.

16

AihuConfigError

class
class AihuConfigError extends Error

Thrown by `defineConfig` when configuration validation fails.

17

AdapterContext

interface
interface AdapterContext {
  /** Absolute path to Vite's output directory (resolved build.outDir). */
  readonly outDir: string
  /** Absolute path to the project root (Vite's config.root). */
  readonly root: string
  /**
   * Route definitions derived from the pages directory scan.
   * Contains pattern, segments, and name — module() is irrelevant at adapt() time.
   */
  readonly routes: ReadonlyArray<RouteDefinition>
  /** The resolved AihuConfig passed to viteAihuPlugin(). */
  readonly config: import('./config.ts').AihuConfig

  /**
   * Emit a file relative to outDir. Creates parent directories as needed.
   * path is relative to outDir.
   */
  emitFile(path: string, content: string): Promise<void>

  /**
   * Copy a file or directory (absolute paths). Recursive. Overwrites existing.
   */
  copy(src: string, dest: string): Promise<void>

  /**
   * Write a file at an absolute path. Creates parent directories as needed.
   */
  writeFile(absolutePath: string, content: string): Promise<void>

  /**
   * Generate the source text of a server handler module.
   *
   * Returns a JS string that imports routes and createRequestRouter,
   * wires the handler, and exports `{ handler }`. The adapter appends
   * its platform-specific export wrapper.
   *
   * @deprecated Use `AihuAdapter.serverEntry` with `output: 'ssr'`.
   *
   * This produces a module that is written to disk AFTER Vite finishes, so it
   * lives outside the build graph: it cannot import `virtual:aihu-routes`, and
   * the `routes` it is handed carry
   * `module: () => Promise.resolve({ default: null })`. Every route it wires
   * therefore 404s by construction. Retained so existing `ssr: true` adapter
   * configs keep building; slated for deletion in a follow-up.
   */
  createHandlerSource(options?: CreateHandlerSourceOptions): string
}

Context provided to adapter.adapt() after Vite's closeBundle completes.

18

AihuAdapter

interface
interface AihuAdapter {
  /**
   * Unique adapter name. Used in log output and error messages.
   * Convention: '<platform>' e.g. 'cloudflare', 'vercel', 'node'.
   */
  readonly name: string

  /**
   * Called by viteAihuPlugin's closeBundle hook after Vite finishes
   * writing all output files. The adapter reads from context.outDir,
   * transforms the build output into the platform's required format,
   * and writes the final deployment artifact.
   *
   * Under `output: 'ssr'` this fires ONCE, for the client environment only —
   * `closeBundle` is a per-environment hook and an unguarded adapter would
   * otherwise run twice per build.
   */
  adapt(context: AdapterContext): Promise<void>

  /**
   * Contribute the platform wrapper for `virtual:aihu-server-entry`
   * (`output: 'ssr'` only). Returns JS SOURCE, appended verbatim after the
   * framework prelude, and expected to carry the platform's own
   * `export default`.
   *
   * This replaces `AdapterContext.createHandlerSource`, and the difference is
   * not stylistic. `createHandlerSource` text is written to disk after the
   * build; this text is INSIDE the build graph, so `ctx.handler` is a real
   * router over real route chunks rather than a manifest of 404 placeholders.
   *
   * Optional: an adapter without it still works for `'spa'` / `'static'`, and
   * an `'ssr'` build without one falls back to the prelude's bare named
   * `handler` export (usable by hand, not deployable as-is).
   *
   * @example
   * serverEntry: ({ handler }) => `export default {
   *   async fetch(request, env) {
   *     const res = await ${handler}(request)
   *     return res.status === 404 ? env.ASSETS.fetch(request) : res
   *   },
   * }`
   */
  serverEntry?(context: ServerEntryContext): string
}

The AihuAdapter interface.

19

AihuConfig

interface
interface AihuConfig {
  /** Directory layout overrides. */
  readonly dir?: DirConfig
  /**
   * Output mode. Supports `'spa'` (default), `'static'` (SSG prerender) and
   * `'ssr'` (client bundle + a request-time server bundle).
   * defineConfig throws AihuConfigError for any other value.
   *
   * `'ssr'` additionally REQUIRES `css.shadowMode` — see
   * `requireShadowModeForSsr` below for why that is an error and not a
   * warning.
   */
  readonly output?: OutputMode
  /**
   * Site-level configuration. `site.url` is the absolute base URL used by the
   * `'static'` output mode to resolve relative canonical/OG/Twitter URLs.
   */
  readonly site?: SiteConfig
  /**
   * Aihu plugins. Order is preserved.
   * Appended after the three framework plugins (compiler, router, agent-readiness).
   */
  readonly plugins?: ReadonlyArray<AihuPlugin>
  /** Runtime configuration split — public values are inlined in the client bundle. */
  readonly runtimeConfig?: RuntimeConfig
  /**
   * App-level values made available to all components as bare identifiers.
   * Declared here for documentation and future build-time validation; the
   * values are hoisted into globalThis by createApp() at runtime.
   *
   * @example
   * export default defineConfig({ provide: { supabase, checkAuth } })
   */
  readonly provide?: Record<string, unknown>
  /** HTML <head> metadata. */
  readonly app?: AppHeadConfig
  /** Passthrough to Vite's UserConfig. Merged via Vite's config() hook. */
  readonly vite?: VitePassthrough
  /**
   * Opt-in agent-readiness integration.
   * Requires { name: string } at minimum.
   * When absent or false, a no-op plugin is substituted.
   */
  readonly agentReadiness?: AgentReadinessConfig | false
  /**
   * Deployment adapter. Transforms the Vite build output into the target
   * platform's required format. Called after vite build completes.
   * When absent, no post-build transformation is applied (manual deployment).
   */
  readonly adapter?: AihuAdapter
  /**
   * Router-related app config (arch-5 M1).
   *
   * WARNING: `router.viewTransitions` is declared but NOT wired — nothing
   * forwards it from here to the router runtime, so setting it has no effect.
   * `defineConfig` warns when you do. The working lever is the
   * `<router viewTransitions>` prop. Tracked for wiring or removal.
   */
  readonly router?: RouterConfig
  /** Compiler options forwarded to `aihuCompilerPlugin`. */
  readonly compiler?: CompilerConfig
  /** `aihu dev` options. Read by the CLI, not by Vite. */
  readonly dev?: DevConfig
  /** `aihu build` / `aihu dev` build options. Read by the CLI, not by Vite. */
  readonly build?: BuildConfig
  /** `aihu-tsc` options. Read by the CLI, not by Vite. */
  readonly typecheck?: TypecheckConfig
  /**
   * CSS / styling integration. Currently surfaces the project-wide
   * `shadowMode` forwarded to the compiler. Set to `{ shadowMode: 'light' }`
   * when using a cascade-dependent CSS framework (Tailwind, UnoCSS, Pico).
   */
  readonly css?: CssConfig
}
20

AihuModuleApi

interface
interface AihuModuleApi<TOptions = unknown> {
  /**
   * Stable module id — the package name, e.g. `'@aihu/ui'`.
   *
   * Keyed on this rather than the plugin `name` because a package may
   * contribute several plugins (Vite has no dedupe and a factory returning an
   * array is the norm), and consumers want the package, not each plugin.
   */
  readonly aihuModule: string
  /** The resolved options for this module, after its own defaults. */
  getOptions(): TOptions
}

The contract EVERY aihu package that contributes build behaviour satisfies.

21

AihuPluginApi

interface
interface AihuPluginApi {
  /** The config object the user passed to `viteAihuPlugin()`. */
  getAihuConfig(): AihuConfig
}

The public API handle attached to aihu's marker plugin.

22

AppConfig

interface
interface AppConfig {
  /** Id of the outlet element in index.html. Default: 'outlet' */
  outletId?: string
  /**
   * App-level values hoisted into globalThis before any component runs.
   * Use this for singletons (db clients, auth helpers, i18n) that are
   * referenced as bare identifiers inside @state blocks.
   *
   * NOTE — this is NOT `@aihu/context`'s `provide()`. Despite the shared name
   * it is a different mechanism: values land on `globalThis`, not in a context
   * token, and `inject(Token)` will never see them. For real token-based DI at
   * the app root, use {@link AppConfig.context} below.
   *
   * @example
   * createApp({ provide: { supabase, checkAuth } })
   */
  provide?: Record<string, unknown>
  /**
   * App-root context scope. Runs ONCE at bootstrap, inside a real
   * `@aihu/context` scope owned by the outlet element — so every
   * `provide(Token, value)` made here is visible to `inject(Token)` in every
   * page, layout and nested component the app renders.
   *
   * This is the app-root seam that several packages' docs already assume
   * exists ("provide at app root" — `@aihu/magna`'s `MagnaFetchToken`,
   * `@aihu-plugin/data`'s `ResourceStoreToken`). Without it, `provide()` at
   * bootstrap lands in no scope at all and `inject()` silently returns the
   * token default forever — `inject` falls back rather than throwing, so the
   * failure is invisible. `@aihu/app` installs the router's own `RouteContext`
   * through the same scope, immediately BEFORE this callback runs, so an app
   * may also deliberately override it.
   *
   * Must be synchronous: `provide()` only writes to the active scope, and the
   * scope is torn down when the callback returns. Providing from an `await`ed
   * continuation is a silent no-op.
   *
   * @example
   * import { provide } from '@aihu/context'
   * import { MagnaFetchToken, createMagnaFetch } from '@aihu/magna'
   *
   * createApp({
   *   context: () => provide(MagnaFetchToken, createMagnaFetch({ url })),
   * })
   */
  context?: () => void
  /**
   * Rendering mode from the server config. Controls whether the client
   * wires the hydration function into the runtime.
   *
   * - 'ssr' | 'hybrid' (default): wires _setHydrate so the client can
   *   take over from server-rendered HTML without re-creating DOM.
   * - 'spa': skips _setHydrate — no SSR HTML to hydrate, mount-only.
   *
   * Pass `defineAihuConfig(…).rendering?.mode` from your server config.
   * Default: 'ssr' (hydration wired).
   */
  rendering?: { mode?: AppRenderingMode }
  /**
   * Site-level config. `site.url` is the absolute base URL used to resolve
   * relative per-route `canonical` / `og:*` / `twitter:*` values into absolute
   * URLs as the head is applied on client navigation (mirrors the SSG path's
   * `AihuConfig.site.url`). When absent, relative values are emitted unchanged.
   */
  site?: { url?: string }
  /**
   * Global `<head>` defaults (typically `aihu.config.ts`'s `app.head`). On every
   * navigation these defaults are folded under the active route's head
   * (`routeHeadToSsrHead`'s `globalHead`) and re-applied — so a route that omits
   * a field falls back to the global default, and global tags persist across
   * navigations while route-only tags are cleaned up.
   */
  head?: HeadConfig
}

Inline runtime configuration accepted by createApp().

23

AppHandle

interface
interface AppHandle {
  /**
   * Switch the active layout on the current route without navigating.
   * `setLayout(name)` forces that layout; `setLayout(null)` forces none. The
   * override is reset on the next navigation. Wire it to a UI toggle or expose
   * it to an `@agent` action (e.g. `setLayout("compact")`).
   */
  setLayout(name: string | null): Promise<void>
}
24

AppHeadConfig

interface
interface AppHeadConfig {
  readonly head?: HeadConfig
  /**
   * Id of the outlet element in `index.html` — the element every render path
   * puts the page into. Default: `'outlet'`.
   *
   * Declared here because it is a fact about the DOCUMENT, and three separate
   * things need it: `createApp()` mounts into it, the SSG prerender splices
   * into it, and the `output: 'ssr'` Worker splices into it. Before this key
   * existed only the client could be told, via `createApp({ outletId })` in a
   * hand-written `src/main.ts` — and the two build-time paths hardcoded
   * `'outlet'`, so changing it silently emptied every prerendered page.
   *
   * `viteAihuPlugin` also threads this into the VIRTUAL client entry
   * (`createApp({ outletId })`), so a project with no `src/main.ts` needs to
   * state it exactly once. A project that ejected to its own `src/main.ts`
   * passes the same value to `createApp` itself — the virtual entry is not in
   * play there.
   */
  readonly outletId?: string
}

The `app` section of the aihu config.

25

CreateHandlerSourceOptions

interface
interface CreateHandlerSourceOptions {
  /**
   * Import specifier for the compiled routes manifest module.
   * Default: './routes-manifest.js'
   */
  routesSpecifier?: string
  /**
   * Import specifier for @aihu/server.
   * Default: '@aihu/server'
   * Adapters that bundle server deps may override this to a relative path.
   */
  serverSpecifier?: string
}

Options for AdapterContext.createHandlerSource().

26

CriticalPathOptions

interface
interface CriticalPathOptions {
  /** Modules forbidden in the critical path. */
  readonly deny?: readonly CriticalPathRule[]
  /**
   * Max gzipped size of the critical path (all statically-entry-reachable
   * chunks combined). Gzip, not raw, because that is what crosses the wire —
   * and it matches `scripts/size.ts`'s existing convention.
   */
  readonly maxBytes?: number
  /** Report without failing the build. Default `false`. */
  readonly warnOnly?: boolean
}
27

CriticalPathRule

interface
interface CriticalPathRule {
  /** Tested against the module id (absolute path, posix-normalized). */
  pattern: RegExp
  /**
   * Printed on violation. Say WHY it must stay out and what to do instead —
   * this message is the whole value of the rule when it fires months later.
   */
  reason: string
}

A module pattern that must never become statically reachable from an entry.

28

DirConfig

interface
interface DirConfig {
  /** Directory to scan for page routes. Default: 'pages' */
  readonly pages?: string
  /** Directory to scan for layout files. Default: 'src/layouts' */
  readonly layouts?: string
  /** Public static assets directory. Default: 'public' */
  readonly public?: string
  /**
   * Directory to scan for components. Default: 'src/components'
   *
   * `@aihu/router`'s `componentsDir` has always existed but was unreachable
   * from here: `viteAihuPlugin` forwarded only `pagesDir` and `layoutsDir`, so
   * changing it meant calling `viteRouterIntegration()` yourself — i.e.
   * abandoning `viteAihuPlugin` entirely.
   */
  readonly components?: string
}
29

HeadConfig

interface
interface HeadConfig {
  readonly title?: string
  /** Default: 'UTF-8' */
  readonly charset?: string
  /** Default: 'width=device-width, initial-scale=1' */
  readonly viewport?: string
  readonly meta?: ReadonlyArray<Record<string, string>>
}
30

LoadedAihuConfig

interface
interface LoadedAihuConfig {
  /** The evaluated config. `{}` when the plugin was called with no argument. */
  readonly config: AihuConfig
  /** Absolute path of the Vite config file it came from. */
  readonly configFile: string
  /**
   * Files the config depends on, from Vite's own dependency tracking. A watcher
   * should invalidate when any of these change — this is what makes a dev
   * server restart on config edits.
   */
  readonly dependencies: ReadonlyArray<string>
  /**
   * Every aihu module registered in the Vite config, keyed by `aihuModule`.
   *
   * This is what gives the CLI coverage that grows on its own: a new package
   * that ships a plugin with an `AihuModuleApi` handle shows up here with no
   * change to `@aihu/app`, to this function, or to any consumer.
   */
  readonly modules: ReadonlyMap<string, unknown>
}
31

RouterConfig

interface
interface RouterConfig {
  /**
   * When `true`, `<a>` navigation wraps in `document.startViewTransition()`
   * if the browser supports the View Transitions API. No-op in unsupported
   * browsers (graceful degradation). Default: `false`.
   *
   * SSR safety: the wrapping is browser-only — server-rendered HTML is
   * unchanged, and hydration is unaffected.
   */
  readonly viewTransitions?: boolean
}

Router-related app config (arch-5 M1, RFC-A5-012).

32

RuntimeConfig

interface
interface RuntimeConfig {
  readonly public?: Record<string, unknown>
  /** V0: accepted but ignored at runtime (server-side enforcement deferred to V1). */
  readonly private?: Record<string, unknown>
}

Runtime configuration split.

33

SiteConfig

interface
interface SiteConfig {
  /**
   * Absolute base URL of the deployed site (e.g. `https://example.com`).
   * Used by the `'static'` (SSG) output mode to resolve relative per-route
   * `canonical` / `og:*` / `twitter:*` URLs into absolute URLs (passed as
   * `siteUrl` to @aihu/server's `routeHeadToSsrHead`). When absent, relative
   * URLs are emitted unchanged.
   */
  readonly url?: string
}

Site-level configuration.

34

SsrDocumentConfig

interface
interface SsrDocumentConfig {
  /**
   * The built client `index.html`, verbatim. Carries Vite's hashed
   * `<script type="module">`, its modulepreloads, its stylesheet links and the
   * `app.head` the `aihu-head` plugin already applied at build time.
   *
   * An empty string means the build could not read one; {@link createSsrDocument}
   * then passes every response through untouched, which is the pre-existing
   * fragment behaviour rather than a broken document.
   */
  readonly template: string
  /** Resolved outlet id — `app.outletId` from the aihu config, or the default. */
  readonly outletId: string
  /** `site.url`, for resolving relative canonical/OG/Twitter URLs. */
  readonly siteUrl?: string
  /** `app.head`, folded UNDER each route's own head (same as SSG and client nav). */
  readonly globalHead?: HeadConfig
}
35

SsrDocumentRoute

interface
interface SsrDocumentRoute {
  /** The route pattern — the memo key for the head-applied template. */
  readonly pattern: string
  /** The compiled `@route { head }` block, if the route declares one. */
  readonly head?: RouteHead
}

The subset of a matched route this module reads.

36

AgentReadinessConfig

typeagent
type AgentReadinessConfig = import('@aihu-plugin/agent-readiness').AgentReadinessConfig

Type-only import — not bundled when agentReadiness is absent.

37

AihuPlugin

type
type AihuPlugin = Plugin

A Aihu plugin is structurally identical to a Vite plugin (V0).

38

AppRenderingMode

type
type AppRenderingMode = 'ssr' | 'spa' | 'hybrid'

Rendering mode passed from the server config into the client bootstrap.

39

OutputMode

type
type OutputMode = 'spa' | 'static' | 'ssr'

Build output mode.

40

ServerEntryContext

type
type ServerEntryContext
41

SsrDocumentWrapper

type
type SsrDocumentWrapper = (response: Response, route?: SsrDocumentRoute) => Promise<Response>

Wrap one `handle()` response into a full document.

42

VitePassthrough

type
type VitePassthrough = Omit<UserConfig, 'plugins'>

Vite config fields that can be safely merged (excludes plugins — use AihuConfig.plugins).