Top-level app integration — wires runtime, router, and adapters into a Vite app.
assertOutletPresentfunction assertOutletPresent(template: string, outletId: string): string | nullcollectAihuModulesfunction collectAihuModules(plugins: ReadonlyArray<unknown>): Map<string, unknown>Collect every aihu module's resolved options from a plugin array.
createAppfunction createApp(config?: AppConfig): AppHandlecreateSsrDocumentfunction createSsrDocument(config: SsrDocumentConfig): SsrDocumentWrapperBuild the request-time document wrapper.
criticalPathfunction criticalPath(opts: CriticalPathOptions = {}): PlugindeclareAihuModulefunction declareAihuModule<TOptions, TPlugins extends readonly unknown[]>( aihuModule: string, options: TOptions, plugins: TPlugins, ): TPluginsAttach the module contract to a plugin (or plugin array).
defineConfigfunction defineConfig(config: AihuConfig): AihuConfigDefine the aihu application configuration.
injectIntoOutletIdfunction injectIntoOutletId(html: string, content: string, outletId: string): string | nullInject rendered route content into the outlet element of an HTML template.
loadAihuConfigasync 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.
ssrOutDirForfunction ssrOutDirFor(clientOutDir: string): stringWhere the `'ssr'` environment writes: a SIBLING of the client outDir, never inside it.
validateAihuConfigfunction validateAihuConfig(config: AihuConfig): voidValidate a config object without the `defineConfig` ceremony.
viteAihuPluginfunction viteAihuPlugin(config?: AihuConfig): PluginOption[]viteAihuPlugin() — composed Vite plugin for aihu SPA projects.
AIHU_CONFIG_KEYSconst AIHU_CONFIG_KEYS: ReadonlyArray<string>Keys aihu owns, derived from the schema rather than hand-listed.
AIHU_CONFIG_PLUGINconst AIHU_CONFIG_PLUGINPlugin name carrying the config handle.
DEFAULT_OUTLET_IDconst DEFAULT_OUTLET_IDThe outlet element id every aihu surface agrees on when nothing overrides it.
AihuConfigErrorclass AihuConfigError extends ErrorThrown by `defineConfig` when configuration validation fails.
AdapterContextinterface 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.
AihuAdapterinterface 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.
AihuConfiginterface 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
}AihuModuleApiinterface 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.
AihuPluginApiinterface AihuPluginApi {
/** The config object the user passed to `viteAihuPlugin()`. */
getAihuConfig(): AihuConfig
}The public API handle attached to aihu's marker plugin.
AppConfiginterface 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().
AppHandleinterface 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>
}AppHeadConfiginterface 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.
CreateHandlerSourceOptionsinterface 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().
CriticalPathOptionsinterface 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
}CriticalPathRuleinterface 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.
DirConfiginterface 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
}HeadConfiginterface 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>>
}LoadedAihuConfiginterface 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>
}RouterConfiginterface 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).
RuntimeConfiginterface 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.
SiteConfiginterface 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.
SsrDocumentConfiginterface 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
}SsrDocumentRouteinterface 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.
AgentReadinessConfigtype AgentReadinessConfig = import('@aihu-plugin/agent-readiness').AgentReadinessConfigType-only import — not bundled when agentReadiness is absent.
AihuPlugintype AihuPlugin = PluginA Aihu plugin is structurally identical to a Vite plugin (V0).
AppRenderingModetype AppRenderingMode = 'ssr' | 'spa' | 'hybrid'Rendering mode passed from the server config into the client bootstrap.
OutputModetype OutputMode = 'spa' | 'static' | 'ssr'Build output mode.
ServerEntryContexttype ServerEntryContextSsrDocumentWrappertype SsrDocumentWrapper = (response: Response, route?: SsrDocumentRoute) => Promise<Response>Wrap one `handle()` response into a full document.
VitePassthroughtype VitePassthrough = Omit<UserConfig, 'plugins'>Vite config fields that can be safely merged (excludes plugins — use AihuConfig.plugins).