/** * Core types for gonia. * * @packageDocumentation */ import type { ContextKey } from './context-registry.js'; /** * Execution mode for the framework. */ export declare enum Mode { /** Server-side rendering */ SERVER = "server", /** Client-side hydration/runtime */ CLIENT = "client" } declare const __brand: unique symbol; /** * A branded string type for expressions. * * @remarks * This prevents arbitrary strings from being passed as expressions, * reducing the risk of eval injection. Only the framework's parser * should create Expression values. */ export type Expression = string & { [__brand]: 'expression'; }; /** * Function to evaluate an expression against state. */ export type EvalFn = (expr: Expression) => T; /** * Registry of framework-provided injectables. * * @remarks * For provider contexts, use the second type parameter of `Directive` instead: * * ```ts * const themed: Directive<['$element', 'theme'], {theme: ThemeContext}> = ($el, theme) => { * // theme is typed as ThemeContext * }; * ``` */ export interface InjectableRegistry { /** The expression string from the directive attribute */ $expr: Expression; /** The target DOM element */ $element: Element; /** Function to evaluate expressions against state */ $eval: EvalFn; /** Local reactive state object (isolated per element) */ $scope: Record; /** Root reactive state object (shared across all elements) */ $rootState: Record; /** Template registry for g-template directive */ $templates: { get(name: string): Promise; }; /** Current execution mode (server or client) */ $mode: Mode; /** Force fallback rendering from within an async directive (throws FallbackSignal) */ $fallback: () => never; } /** * Options for server-side rendering of async directives. */ export interface RenderOptions { /** Global timeout in ms for async operations (await mode) */ timeout?: number; /** Max nesting depth for await mode (default: 10) */ maxDepth?: number; } /** * Extract the value type from a ContextKey. */ type ContextKeyValue = K extends ContextKey ? V : never; /** * Maps a tuple of injectable names or context keys to their corresponding types. * * @typeParam K - Tuple of injectable names (strings) or ContextKey objects * @typeParam T - Type map (defaults to InjectableRegistry) * * @example * ```ts * type Args = MapInjectables<['$element', '$scope']>; * // => [Element, Record] * * // With typed scope override * type Args2 = MapInjectables<['$scope'], { $scope: { count: number } }>; * // => [{ count: number }] * * // With context keys * const MyContext = createContextKey<{ value: number }>('MyContext'); * type Args3 = MapInjectables<['$element', typeof MyContext]>; * // => [Element, { value: number }] * ``` */ type MergedRegistry = { [K in keyof InjectableRegistry]: K extends keyof T ? T[K] : InjectableRegistry[K]; } & Omit; export type MapInjectables)[], T extends Record = {}> = { [I in keyof K]: K[I] extends ContextKey ? ContextKeyValue : K[I] extends keyof MergedRegistry ? MergedRegistry[K[I]] : unknown; }; /** * Evaluation context passed to directives. */ export interface Context { /** Current execution mode */ readonly mode: Mode; /** * Evaluate an expression against the current state. * * @typeParam T - Expected return type * @param expr - The expression to evaluate * @returns The evaluated result */ eval(expr: Expression): T; /** * Get a value from the context by key. * * @param key - The key to look up * @returns The value, or undefined */ get(key: string): T | undefined; /** * Create a child context with additional values. * * @param additions - Values to add to the child context * @returns A new child context */ child(additions: Record): Context; } /** * Directive priority levels. * * @remarks * Higher priority directives run first. Structural directives * (like g-if, g-for) need to run before behavioral ones. */ export declare enum DirectivePriority { /** Conditional structural directives (g-if) — runs before iterative ones */ STRUCTURAL_CONDITIONAL = 1100, /** Iterative structural directives (g-for) */ STRUCTURAL = 1000, /** Template/transclusion directives */ TEMPLATE = 500, /** Normal behavioral directives */ NORMAL = 0 } /** * Static metadata for a directive. * * @remarks * Declared on the directive function before registration. * Used to determine processing order and behavior. * * @typeParam T - Type map for injectable dependencies */ export interface DirectiveMeta { /** * Whether this directive transcludes content. * * @remarks * If true, children are saved before the directive runs. */ transclude?: boolean; /** * Dependencies to inject into the directive. * * @remarks * Available injectables: * - `$expr`: The expression string from the attribute * - `$element`: The target DOM element * - `$eval`: Function to evaluate expressions: `(expr) => value` * - `$scope`: Local reactive state object (isolated per element) * - Any registered service names * - Any `ContextKey` for typed context resolution * - Any names provided by ancestor directives via `$context` * * @example * ```ts * myDirective.$inject = ['$element', '$scope']; * ``` * * For typed context keys, use the `using` option on directive registration: * ```ts * directive('my-directive', myDirective, { using: [SlotContentContext] }); * ``` */ $inject?: readonly string[]; /** * Names this directive exposes as context to descendants. * * @remarks * When a directive declares `$context`, its `$scope` becomes * available to descendant directives under those names. * Useful for passing state through isolate scope boundaries. * * @example * ```ts * const themeProvider: Directive = ($scope) => { * $scope.mode = 'dark'; * }; * themeProvider.$inject = ['$scope']; * themeProvider.$context = ['theme']; * * // Descendants can inject 'theme' * const button: Directive = ($element, theme) => { * console.log(theme.mode); * }; * button.$inject = ['$element', 'theme']; * ``` */ $context?: string[]; /** * Processing priority. Higher runs first. * * @defaultValue DirectivePriority.NORMAL */ priority?: number; } /** * A directive function with typed parameters based on injectable keys. * * @remarks * Use this type annotation to get contextual typing for directive parameters. * The tuple of keys maps to parameter types from InjectableRegistry or ContextKey types. * * @typeParam K - Tuple of injectable key names or ContextKey objects * @typeParam T - Optional custom type map to extend InjectableRegistry * * @example * ```ts * // Basic usage - $element is typed as Element * const myDirective: Directive<['$element']> = ($element) => { * $element.textContent = 'hello'; * }; * * // Multiple injectables * const text: Directive<['$expr', '$element', '$eval']> = ($expr, $element, $eval) => { * $element.textContent = String($eval($expr) ?? ''); * }; * * // With typed context keys * const slot: Directive<['$element', typeof SlotContentContext]> = ($element, content) => { * // content is typed as SlotContent * console.log(content.slots); * }; * * // With custom types (extend InjectableRegistry first) * declare module 'gonia' { * interface InjectableRegistry { * myService: { doThing(): void }; * } * } * const custom: Directive<['$element', 'myService']> = ($el, svc) => { * svc.doThing(); * }; * ``` */ export type Directive)[] = readonly (string & keyof InjectableRegistry)[], T extends Record = {}> = ((...args: MapInjectables) => void | (() => void) | Promise void)>) & DirectiveMeta; /** * Convenience type for directives with a typed scope. * * @typeParam K - Tuple of injectable names * @typeParam S - The scope shape * * @example * ```ts * const counter: ScopedDirective<['$element', '$scope'], { count: number }> = ($el, $scope) => { * $scope.count = 0; * $el.addEventListener('click', () => $scope.count++); * }; * ``` */ export type ScopedDirective)[], S extends Record> = Directive; /** * Attributes passed to template functions. */ export interface TemplateAttrs { /** The element's innerHTML before transformation */ children: string; /** All attributes from the element */ [attr: string]: string; } /** * Template can be a string or a function that receives element attributes and the element. */ export type TemplateOption = string | ((attrs: TemplateAttrs, el: Element) => string | Promise); /** * Fallback content shown while an async directive is loading. * Same shape as TemplateOption. */ export type FallbackOption = string | ((attrs: TemplateAttrs, el: Element) => string | Promise); /** * Options for directive registration. */ export interface DirectiveOptions { /** * Whether to create a new scope for this directive. * * @remarks * When true, the directive creates a new scope that inherits * from the parent scope via prototype chain. * * @defaultValue false */ scope?: boolean; /** * Template for the directive's content. * * @remarks * Can be a string or a function that receives the element's * attributes and children, returning HTML. * * @example * ```ts * // Static template * directive('my-modal', handler, { * template: '' * }); * * // Dynamic template with props * directive('fancy-heading', null, { * template: ({ level, children }) => `${children}` * }); * * // Async template (e.g., dynamic import) * directive('lazy-component', handler, { * template: () => import('./template.html?raw').then(m => m.default) * }); * ``` */ template?: TemplateOption; /** * DI provider overrides for descendants. * * @remarks * Maps injectable names to values. Descendants requesting these * names via `$inject` will receive the provided values instead * of global services. * * Useful for testing (mock services) or scoping services to a subtree. * * @example * ```ts * // Test harness with mock services * directive('test-harness', null, { * scope: true, * provide: { * '$http': mockHttpClient, * 'apiUrl': 'http://localhost:3000/test' * } * }); * * // Descendants get mock values * directive('api-consumer', ($http, apiUrl) => { * $http.get(apiUrl + '/users'); * }); * ``` */ provide?: Record; /** * Context keys this directive uses. * * @remarks * Declares which contexts the directive depends on. The resolved * context values are appended to the directive function parameters * in the order they appear in this array. * * This enables: * - Static analysis of context dependencies * - Automatic `$inject` generation by the Vite plugin * - Type-safe context access without manual `resolveContext` calls * * @example * ```ts * const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme'); * const UserContext = createContextKey<{ name: string }>('User'); * * directive('themed-greeting', ($element, $scope, theme, user) => { * // theme and user are resolved from the using array * $element.textContent = `Hello ${user.name}!`; * $element.className = theme.mode; * }, { * using: [ThemeContext, UserContext] * }); * ``` */ using?: ContextKey[]; /** * Fallback content shown while an async directive is loading. * * @remarks * Only meaningful when the directive function is async. * Rendered immediately on the server in fallback/stream modes, * and on the client while the async function resolves. * * @example * ```ts * directive('heavy-widget', async ($scope) => { * const mod = await import('./heavy-widget.js'); * mod.setup($scope); * }, { * scope: true, * template: '
...
', * fallback: '
Loading...
', * }); * ``` */ fallback?: FallbackOption; /** * Server-side rendering mode for async directives. * * @remarks * - `'await'` (default): Run the async fn on the server, wait for it, * render the template. Includes depth and timeout safety. * - `'fallback'`: Render fallback immediately on the server without * running the async fn. Client loads and swaps. * - `'stream'`: Render fallback with a unique ID, then stream a * replacement `