/** * `createApp` — composition root. Constructs a Runtime, registers plugins, * and exposes the fluent verbs that move the App's typed capability set: * * - `App` is generic. Each `.with(plugin)` intersects the plugin's * `__mark` into `TCaps`, so handler ctx narrows automatically. * * - `.with(plugin)` is the typed plugin-install verb. Equivalent to * `runtime.registerPlugin(plugin)` at runtime but moves the type. * * - `.subscribe(sub | subs)` registers `Subscription` values — closures * produced by the module-level `subscribe(fn)` primitive. The pattern * for organised listener files under `app/listeners/`. * * - `.when(event, fn)` is the inline shortcut over `runtime.when`. * * Plugins passed via `createApp({ plugins })` install identically to those * installed via `.with()`. */ import { type Container } from "@nwire/container/awilix"; import { type Logger } from "@nwire/logger"; import { type Binding, type HandlerDef, type Interface, type WireCtxBuilder } from "@nwire/wires"; import type { EventDefinition, EventPayload } from "@nwire/messages"; import type { AppHooks, ListenerContext } from "@nwire/runtime"; import { type PluginDefinition, type Runtime } from "./runtime/index.js"; import { type PluginContributions } from "./lifecycle.js"; import { type Subscription } from "./subscribe.js"; import { type RegistryInput } from "./registry.js"; import { type AppPublicSurface } from "./app-public-surface.js"; export interface DiscoverOptions { /** * Root directory to scan for app primitives. Defaults to `app/` relative * to `process.cwd()`. Pass an absolute path or a path relative to CWD. * * The scanner walks the directory recursively, imports every `.ts` / `.js` * module it finds, and classifies exports by their `$kind` discriminator: * - `$kind: "handler"` (incl. actions + queries) → registered as handlers * - `$kind: "actor"` → passed to `actorsPlugin` * - `$kind: "projection"` → passed to `projectionsPlugin` * - `$kind: "workflow"` → passed to `workflowsPlugin` * * Explicit `handlers` / `plugins` passed in `createApp` MERGE with * discovered values; explicit wins on name collision. * * **Two paths, same result.** At runtime (dev, vitest, plain `node`) discovery * walks this directory with `node:fs` + dynamic `import()`. When the app is * built with the `@nwire/scan` plugin (`nwire build` wires it automatically), * the build bakes the same primitives into a static registry * (`virtual:nwire-registry`) that `createApp` prefers — so a production bundle * boots with zero filesystem scan and zero dynamic import, and works in * bundled edge runtimes (Cloudflare Workers, Deno Deploy) where `node:fs` is * absent. Both paths share the same file-collection and classification rules, * so they register the identical set. */ readonly root?: string; } export interface CreateAppOptions { /** App name — surfaces in framework-hook payloads + dev logger output. */ readonly appName: string; /** * Plugins to install at construction. Setup runs synchronously. Equivalent * to chaining `.with(plugin)` after `createApp({...})`; the array form is * untyped (TCaps stays `{}`), so prefer `.with(plugin)` when the App's * capability set should narrow handler ctx. */ readonly plugins?: readonly PluginDefinition[]; /** * Handlers registered on the runtime at construction so any plugin can * pick them up during boot. Each handler must satisfy the structural * shape `runtime.registerHandler` accepts: `name`, `input`, and * `run(ctx, opts?)` returning `{ result }`. */ readonly handlers?: THandlers; /** * When present, `createApp` scans the given directory (or `app/` relative * to CWD) for all `defineHandler` / `defineAction` / `defineQuery` / * `defineActor` / `defineProjection` / `defineWorkflow` exports and registers * them automatically. Explicit `handlers` and forge `plugins` merge with * what is discovered; explicit wins on name conflict. * * Forge plugins (`actorsPlugin`, `projectionsPlugin`, `queriesPlugin`, * `workflowsPlugin`, `actionsPlugin`) are auto-installed when the * corresponding primitives are found — no manual `forgePlugins({...})` call * needed. Requires `@nwire/forge` to be installed as a package dependency. * * Discovery runs at `app.start()` time (before plugin setup), so it is * fully async and does not block the synchronous `createApp` call. */ readonly discover?: DiscoverOptions | true; /** * The app's primitive registry — every `define*` export grouped by kind. * This is the canonical registration channel: the userland entry imports its * per-app virtual module and passes it straight through — * * import { registry } from "virtual:nwire-registry/app" * createApp({ appName: "app", registry, plugins: [...] }) * * Registration stays explicit and visible in userland; the build supplies the * list in the exact shape the runtime wants (resolved through the bundler, so * tsconfig paths / extensionless imports resolve in dev and build alike). All * kinds register into the one runtime — handlers dispatch, actors/projections/ * workflows land in `byKind` for forge to scan, commands in `byKind("command")` * for `nwire please`. No primitive is ever passed to a plugin. * * Registered synchronously at construction so plugins (forge et al.) see every * primitive at setup. Wins over `discover` on name collision. * * Config is NOT a `createApp` option — it is the deployment's *values*, not * the app's structure, so it flows through the boot channel: `app.boot(config)` * (or `endpoint().mount(app, config)`), which binds `"config"` before * register/setup. A plugin that needs config at construction imports the * frozen config module directly (`virtual:nwire-config/`). * * The registry's own type is preserved (`defineRegistry` keeps it), so the * `.public()` primitives in `handlers` and `events` shape the app's typed * public surface — see {@link AppPublicSurface}. */ readonly registry?: TReg; /** Override the container — defaults to a fresh `createContainer()`. */ readonly container?: Container; /** Override the runtime — defaults to a fresh `createRuntime({...})`. */ readonly runtime?: Runtime; /** Logger override. */ readonly logger?: Logger; } /** * The App's verbs, lifecycle, and introspection surface. `TCaps` is the * phantom carrier of every capability contributed by plugins via `.with()`; * `TPublic` is the record of `.public()` primitives the app exposes as * destructurable properties. Consumers use the {@link App} alias, which * intersects this shape with `TPublic` itself. */ export interface AppBase { readonly $nwireApp: true; readonly $kind: "app"; readonly appName: string; /** Alias for appName — foundation-spec name field. */ readonly name: string; readonly container: Container; readonly runtime: Runtime; /** * App / plugin lifecycle hook registry (`App*` / `Plugin*` slots). Owned by * the App, not the runtime — the runtime is a stateless dispatch substrate. * Plugins observe lifecycle via `ctx.on(...)`; advanced callers can `.use()` * a chain step (e.g. veto shutdown) directly on these slots. */ readonly appHooks: AppHooks; /** * Wire collection — the same shape as `createInterface()` standalone. * Adopters consume `app.interface.forAdapter(kind)`; foundation-form * apps wire bindings via `app.wire(b, h)` sugar. */ readonly interface: Interface; readonly plugins: readonly PluginDefinition[]; /** * Per-plugin contribution attribution captured at boot — which DI bindings, * capabilities, stages, handlers, and hooks each plugin added. Empty until * the plugin phases have run. Read by `captureTopology` to emit the * `contributes` graph edge (Studio's Plugins page). */ pluginContributions(): readonly PluginContributions[]; /** Phantom marker — never read at runtime. */ readonly __caps?: TCaps; /** * Install a plugin and advance the App's type. Same runtime effect as * passing the plugin in `createApp({plugins})`; the difference is the * type-level move — `TCaps` intersects the plugin's __mark so handler * ctx narrows to the new shape. The public surface rides along. */ with(plugin: PluginDefinition): App; with(plugin: PluginDefinition): App; /** * Register Subscription values. Each subscription's `install(when)` is * called with the App's `runtime.when`. Pass a single sub or an array. */ subscribe(sub: Subscription | readonly Subscription[]): this; /** * Inline listener shortcut over `runtime.when`. Use for one-offs where * pulling a dedicated listener file feels heavy. For organised codebases, * prefer `.subscribe([...])` from `app/listeners/`. */ when(event: E, fn: (payload: EventPayload, ctx: ListenerContext) => Promise | void): this; /** Sugar for `app.interface.wire(b, h)`. Returns the app for chaining. */ wire(binding: Binding, handler: HandlerDef): this; /** Sugar for `app.interface.provide(builder)`. Returns the app for chaining. */ provide(builder: WireCtxBuilder): this; /** Current lifecycle phase. */ readonly state: "idle" | "starting" | "running" | "stopping" | "stopped"; /** * Boot the app. Optional `appConfig` is bound on the container as * `"config"` so plugin boot callbacks can resolve it. Idempotent — * concurrent / repeat calls return the same Promise. */ start(appConfig?: unknown): Promise; stop(reason?: string): Promise; /** Endpoint alias for {@link start}. */ boot(appConfig?: unknown): Promise; /** Endpoint alias for {@link stop}. */ shutdown(): Promise; /** * Run a built-in framework hook by registry-slot name. Resolves `false` * when a chain step vetoed (skipped `next()` or threw); `true` otherwise. */ dispatchFrameworkEvent(slot: string, payload: unknown): Promise; } /** * The App. `TCaps` carries plugin capabilities (`.with()`); `TPublic` is the * app's public surface — every `.public()` primitive as a typed, * destructurable property (`const { listCourses } = coursesApp`). Only * `$public` definitions appear here; internal primitives don't type-check. */ export type App = AppBase & TPublic; export declare function createApp(options: CreateAppOptions): App<{}, AppPublicSurface>;