app()
Declares the application. Same module on server and client; .run() dispatches — on the server it starts Bun.serve, on the client it installs caps and hydrates.
Signature
app(config: AppConfig).run()
Full example
import { app } from '@place/component'
import { pathRouter, RouterCap } from '@place/routing'
import { rootLayout } from './layouts/root.layout'
import home from './pages/home.page'
import about from './pages/about.page'
export default app({
name: '@my-org/site',
pages: [home, about],
layout: rootLayout,
theme: tokens,
tailwind: true,
security: 'standard',
viewTransitions: true,
clientEntry: `${import.meta.dir}/app.ts`,
caps: [[RouterCap, pathRouter]],
}).run()
Options
pages (required)
The explicit list of page values. Order is irrelevant for routing; the framework matches by path. Duplicate paths throw at startup.
layout
Default layout chain wrapping every page that doesn't override it. Single layout or array; chains compose outside-in.
caps
Per-app capability provisions. Two shapes:
caps: [
[RouterCap, pathRouter], // function form (client only)
[NoteStoreCap, { // object form (per-runtime)
server: () => inMemoryStore(seed),
client: () => localStorageStore(),
}],
]
The function form runs only on the runtime where the cap is used (typically client for clientOnly caps). The object form lets you ship distinct server and client impls without conditionals.
theme
A themeTokens() result. The active theme class auto-prefixes the <html> element; the framework reads the theme cookie per-request to avoid FOUC.
tailwind
true opts into Tailwind v4 inline compilation; CSS is compiled once at startup and inlined into every page (hash-stable for CSP). Or pass a config object for content globs and a custom base.
security
Per-route security headers. 'standard' ships a strict CSP (nonce-bound scripts, hashed inline styles, frame-ancestors none), HSTS, X-Content-Type-Options, and same-origin defaults. Or pass an object for fine control.
viewTransitions
true appends the @view-transition { navigation: auto } rule gated behind prefers-reduced-motion: no-preference. Browsers without cross-document VT navigate normally.
clientEntry
Absolute path to the file whose bundle ships to the browser. Almost always ${import.meta.dir}/app.ts — the same file that exports the app config. On the server, this is what Bun.build targets.
port
Explicit port, or omit to read process.env.PORT, or fall back to 5174. The client-side .run() ignores this.
.run()
Dispatches:
- Server (no
window): installs server caps, callsserve(), returns theBun.Serverpromise. - Client: installs client caps, calls
boot(), returnsundefined.
app.ts is the entry on both sides. Bun runs it as the server; the framework's bundler builds the same file for the browser, dropping server-only code via the per-runtime cap factory split..build({ outDir }) — static export
Instead of .run(), call .build({ outDir }) to pre-render the whole app to a static site. It runs the full server setup — Tailwind compile, island discovery and bundling, theme resolution — then writes index.html per route, the island chunks, and a _headers file (Cloudflare strict CSP) to outDir. The exported site is fully interactive; it's the right shape for CDN static hosts. Server-side only.
// app().build({ outDir }) — pre-render to a static site. Runs the
// full server setup (Tailwind compile, island discovery + bundling,
// theme resolution) then, instead of starting a server, writes the
// complete static site to outDir:
//
// <outDir>/index.html, <outDir>/about/index.html, …
// <outDir>/islands/<name>-<hash>.js (+ shared chunks)
// <outDir>/_headers (Cloudflare strict CSP)
//
// The exported site is fully interactive — island bundles ship and
// SPA-nav works. Server-side only; for CDN static hosts.
import { app } from '@place/component'
import { pages } from './pages'
await app({ pages, theme: tokens }).build({ outDir: 'dist' })
discoverPages(dir)
An async helper that imports every *.page.tsx (and subdirectory index.ts barrel) under a directory and returns a flat Page[] — feed it straight into pages with top-level await. It does not derive routes from file paths: each page's page('/path', def) declaration stays the single source of truth for its route.
// discoverPages(dir) — async helper that imports every *.page.tsx
// (plus subdir index.ts barrels) under a directory and returns a
// flat Page[]. It does NOT derive routes from file paths — each
// page's page('/path', def) declaration stays the source of truth.
import { app, discoverPages } from '@place/component'
export default app({
pages: await discoverPages('./src/pages'),
}).run()
routes(prefix, pages, opts?)
A pure value transform — prefixes every page's path and optionally applies a shared layout (pages with their own layout keep theirs). No registration, no side effects; composes recursively. Use it to group feature folders, then spread the groups into app().
// routes(prefix, pages, opts?) — a pure value transform: prefixes
// every page's path and (optionally) applies a shared layout. Used
// to group feature folders. No registration, no side effects.
import { routes } from '@place/component'
// admin/index.ts
export default routes('/admin', [dashboard, users, settings], {
layout: adminLayout,
})
// app.ts — compose the groups into one app:
app({ pages: [home, ...adminRoutes, ...postRoutes] }).run()