UNPKG

mancha

Version:

Javscript HTML rendering engine

418 lines (417 loc) 17.1 kB
//#region src/store.d.ts /** * Internal store properties that are always present. These are managed by the framework * and should not be set directly by users. */ type InternalStoreState = { /** Reference to the parent store in a hierarchy. */ $parent?: SignalStore; /** Reference to the root renderer instance. */ $rootRenderer?: SignalStore; /** Reference to the root DOM node. */ $rootNode?: Node; } & { [key: `$$${string}`]: string | null; }; /** * Base type for user-defined store state. Uses `any` intentionally to allow flexible * user-defined state types without requiring explicit index signatures. */ type StoreState = Record<string, any>; /** Type for expression evaluation function. */ type EvalFunction = (thisArg: SignalStoreProxy, args: Record<string, unknown>) => unknown; /** Type for observer entries that include the store context for proper binding. */ type ObserverEntry = { observer: Observer<unknown>; store: SignalStore; computedKey?: string; }; /** * Internal proxy type used within the store implementation. Uses `any` for dynamic property access. */ type SignalStoreProxy = SignalStore & InternalStoreState & { [key: string]: any; }; /** * The reactive context type exposed to effects and computed functions. * Includes the store's typed state T, internal state, and an index signature for dynamic access. */ type ReactiveContext<T extends StoreState = StoreState> = SignalStore<T> & InternalStoreState & T & Record<string, unknown>; type Observer<T> = (this: SignalStoreProxy) => T; type KeyValueHandler = (this: SignalStoreProxy, key: string, value: unknown) => void; /** Symbol used to identify computed value markers. */ /** Function type for computed value definitions. Receives reactive context as `$` parameter. */ type ComputedFn<T extends StoreState, R> = (this: ReactiveContext<T>, $: ReactiveContext<T>) => R; declare class SignalStore<T extends StoreState = StoreState> { protected readonly evalkeys: string[]; protected readonly expressionCache: Map<string, EvalFunction>; protected readonly observers: Map<string, Set<ObserverEntry>>; protected readonly keyHandlers: Map<RegExp, Set<KeyValueHandler>>; readonly _store: Map<string, unknown>; _lock: Promise<void>; /** * Notification state per key. Value is a pending timeout, or "executing" * when observers are running. Used to debounce and prevent infinite loops. */ private readonly _notify; /** * Tracks nested computed evaluation depth. When > 0, we're inside a computed * function and writes to reactive properties should trigger a warning. */ private _computedDepth; constructor(data?: T); private wrapObject; watch<T>(key: string, observer: Observer<T>): void; addKeyHandler(pattern: RegExp, handler: KeyValueHandler): void; /** * Tags all observer entries matching the given observer function with a computed key. * Called after effect runs to mark which observers belong to which computed. */ private tagObserversForComputed; /** * Synchronously marks all computeds that depend on this key as dirty. * Uses the computedKey field on observer entries for O(1) key lookup. * Cascades through computed chains (if A depends on B, and B is marked dirty, * then A is also marked dirty). */ private markDependentComputedsDirty; notify(key: string, debounceMillis?: number): Promise<void>; get<T>(key: string, observer?: Observer<T>): unknown; private setupComputed; /** * Sets a value in the store. * @param key - The key to set. * @param value - The value to set (can be a computed marker). * @param local - If true, sets directly on this store bypassing ancestor lookup. * Use for creating local scope variables that shadow ancestors. */ set(key: string, value: unknown, local?: boolean): Promise<void>; del(key: string): Promise<void>; /** * Disposes this store by clearing all observers. * Call this when the store is no longer needed to prevent memory leaks. * Also removes any observers this store registered on ancestor stores. */ dispose(): void; keys(): string[]; /** * Checks if a key exists in THIS store only (not ancestors). * Use `get(key) !== null` to check if a key exists anywhere in the chain. */ has(key: string): boolean; /** * Returns observer statistics for performance reporting. */ getObserverStats(): { totalKeys: number; totalObservers: number; byKey: Record<string, number>; }; effect<R>(observer: (this: ReactiveContext<T>) => R, _meta?: EffectMeta): R; /** * Creates a computed property that automatically updates when its dependencies change. * The function is evaluated in a reactive effect, and the result is stored. When any * reactive property accessed within the function changes, it re-evaluates and updates. * * **Important:** This method returns a marker object at runtime, but is typed as * returning `R` to enable ergonomic property assignment without type casts. The return * value must be assigned to a store property (via `set()` or `$.prop =`) - do not use * it directly as a value. * * @example * // Using function() to access reactive `this`: * store.set('double', store.$computed(function() { return this.count * 2 })); * * // Using arrow function with $ parameter (for templates): * store.set('double', store.$computed(($) => $.count * 2)); * * // Direct property assignment (ergonomic typing): * store.$.doubled = store.$computed(($) => $.count * 2); */ $computed<R>(fn: ComputedFn<T, R>): R; private proxify; get $(): SignalStore<T> & InternalStoreState & T; /** * Creates an evaluation function for the provided expression. * @param expr The expression to be evaluated. * @returns The evaluation function. */ private makeEvalFunction; /** * Retrieves or creates a cached expression function for the provided expression. * @param expr - The expression to retrieve or create a cached function for. * @returns The cached expression function. */ private cachedExpressionFunction; eval(expr: string, args?: Record<string, unknown>): unknown; /** * Executes an async function and returns a reactive state object that tracks the result. * * @param fn - The async function to execute. * @param options - Optional arguments to pass to the function. * @returns A reactive state object with $pending, $result, and $error properties. * * @example * // In :data attribute - executes on mount * :data="{ users: $resolve(api.listUsers) }" * * // With options * :data="{ user: $resolve(api.getUser, { path: { id: userId } }) }" * * // In :on:click - executes on click * :on:click="result = $resolve(api.deleteUser, { path: { id } })" */ $resolve<T, O = unknown>(fn: (options?: O) => Promise<T>, options?: O): { $pending: boolean; $result: T | null; $error: Error | null; }; } //#endregion //#region src/renderer.d.ts /** * Represents an abstract class for rendering and manipulating HTML content. * Extends the `ReactiveProxyStore` class. * * @template T - The type of the store state. Defaults to `StoreState`. */ declare abstract class IRenderer<T extends StoreState = StoreState> extends SignalStore<T> { abstract readonly impl: string; private _debugLevel; protected readonly dirpath: string; /** Performance data collected during rendering. Reset on each mount(). */ private _perfData; /** Debug level ordering for comparison. */ private static readonly DEBUG_LEVELS; readonly _skipNodes: Set<Node>; readonly _customElements: Map<string, Node>; /** * Queue for retrying failed element.value assignments. * * Some DOM elements (notably <select>) silently fail when setting .value if * the required child elements don't exist yet. For example, setting * select.value = "banana" does nothing if no <option value="banana"> exists. * * This happens when :bind on a parent element runs before :for on child * elements creates those children (due to BFS traversal order). * * The fix: after setting .value, check if it actually worked. If not, queue * a retry callback. These callbacks are executed at the end of renderNode() * after all child elements have been created. */ readonly _pendingValueRetries: Array<() => void>; abstract parseHTML(content: string, params?: ParserParams): Document | DocumentFragment; abstract serializeHTML(root: DocumentFragment | Node): string; abstract createElement(tag: string, owner?: Document | null): Element; abstract createComment(content: string, owner?: Document | null): Node; abstract textContent(node: Node, tag: string): void; /** * Sets the debug level for the current instance. * * @param flag - Boolean for backwards compat (true -> 'lifecycle') or a DebugLevel. * @returns The current instance of the class. */ debug(flag: boolean | DebugLevel): this; /** * Returns whether debugging is enabled (any level except 'off'). */ get debugging(): boolean; /** * Checks if the current debug level is at least the specified level. */ private shouldLog; /** * Clears accumulated performance data. Called automatically at the start of `mount()`. * * Use this to reset performance tracking before measuring a specific user flow. * After clearing, only effects and lifecycle events from subsequent operations * will be included in the next `getPerformanceReport()` call. * * @example * ```js * // Setup phase * $.debug('lifecycle'); * await $.mount(document.body); * * // Clear to start fresh measurement * $.clearPerformanceReport(); * * // Perform user flow to measure * $.items = generateLargeList(); * * // Get report for just this flow * const report = $.getPerformanceReport(); * ``` */ clearPerformanceReport(): void; /** * Generates a DOM path for an element (e.g., "html>body>div>ul>li:nth-child(2)"). */ private getNodePath; /** * Builds an effect identifier from metadata. */ buildObserverId(meta?: EffectMeta): string; /** * Records an effect execution for performance tracking. */ recordObserverExecution(meta: EffectMeta | undefined, duration: number): void; /** * Returns a structured performance report. */ getPerformanceReport(): PerformanceReport; /** * Override effect() to add performance tracking. * Wraps the observer so re-executions via notify() are also tracked. */ effect<T>(observer: () => T, meta?: EffectMeta): T; /** * Fetches the remote file at the specified path and returns its content as a string. * @param fpath - The path of the remote file to fetch. * @param params - Optional parameters for the fetch operation. * @returns A promise that resolves to the content of the remote file as a string. */ fetchRemote(fpath: string, params?: RenderParams): Promise<string>; /** * Fetches a local path and returns its content as a string. * * @param fpath - The file path of the resource. * @param params - Optional render parameters. * @returns A promise that resolves to the fetched resource as a string. */ fetchLocal(fpath: string, params?: RenderParams): Promise<string>; /** * Preprocesses a string content with optional rendering and parsing parameters. * * @param content - The string content to preprocess. * @param params - Optional rendering and parsing parameters. * @returns A promise that resolves to a DocumentFragment representing the preprocessed content. */ preprocessString(content: string, params?: RenderParams & ParserParams): Promise<Document | DocumentFragment>; /** * Preprocesses a remote file by fetching its content and applying preprocessing steps. * @param fpath - The path to the remote file. * @param params - Optional parameters for rendering and parsing. * @returns A Promise that resolves to a DocumentFragment representing the preprocessed content. */ preprocessRemote(fpath: string, params?: RenderParams & ParserParams): Promise<Document | DocumentFragment>; /** * Preprocesses a local file by fetching its content and applying preprocessing steps. * @param fpath - The path to the local file. * @param params - Optional parameters for rendering and parsing. * @returns A promise that resolves to the preprocessed document fragment. */ preprocessLocal(fpath: string, params?: RenderParams & ParserParams): Promise<Document | DocumentFragment>; /** * Creates a subrenderer from the current renderer instance. * @returns A new instance of the renderer with the same state as the original. */ subrenderer(): IRenderer; /** * Logs the provided arguments if verbose debugging is enabled. * @param args - The arguments to be logged. */ log(...args: unknown[]): void; /** * Preprocesses a node by applying all the registered preprocessing plugins. * * @template T - The type of the input node. * @param {T} root - The root node to preprocess. * @param {RenderParams} [params] - Optional parameters for preprocessing. * @returns {Promise<T>} - A promise that resolves to the preprocessed node. */ preprocessNode<T extends Document | DocumentFragment | Node>(root: T, params?: RenderParams): Promise<T>; /** * Renders the node and applies all the registered rendering plugins. * * @template T - The type of the root node (Document, DocumentFragment, or Node). * @param {T} root - The root node to render. * @param {RenderParams} [params] - Optional parameters for rendering. * @returns {Promise<T>} - A promise that resolves to the fully rendered root node. */ renderNode<T extends Document | DocumentFragment | Node>(root: T, params?: RenderParams): Promise<T>; /** * Mounts the Mancha application to a root element in the DOM. * * @param root - The root element to mount the application to. * @param params - Optional parameters for rendering the application. * @returns A promise that resolves when the mounting process is complete. */ mount(root: Document | DocumentFragment | Node, params?: RenderParams): Promise<void>; } //#endregion //#region src/interfaces.d.ts interface ParserParams { /** Whether the file parsed is a root document, or a document fragment. */ rootDocument?: boolean; /** Encoding to use when processing local files. */ encoding?: "ascii" | "utf8"; } /** The RendererParams interface defines the parameters that can be passed to the renderer. */ interface RenderParams { /** The current directory of the file being rendered. */ dirpath?: string; /** Maximum level of recursion allowed when resolving includes. */ maxdepth?: number; /** Cache policy used when resolving remote paths. */ cache?: RequestCache | null; /** Whether the current node is the root used in Mancha.moun(...). */ rootNode?: Node; } type RendererPlugin = (this: IRenderer, node: ChildNode, params?: RenderParams) => void | Promise<void>; /** Debug level for controlling performance tracking and logging verbosity. */ type DebugLevel = "off" | "lifecycle" | "effects" | "verbose"; /** Metadata for identifying effects in performance tracking. */ type EffectMeta = { /** The directive that created this effect (e.g., 'class', 'bind', 'for'). */ directive: string; /** The DOM element associated with this effect, if any. */ element?: Element; /** The expression being evaluated by this effect. */ expression?: string; /** Direct identifier for effects without a DOM element (e.g., computed property key). */ id?: string; }; /** Statistics for a tracked effect. */ type EffectStats = { /** Effect identifier (e.g., "bind:my-input:user.name"). */ id: string; /** Number of times this effect has executed. */ executionCount: number; /** Total execution time in milliseconds. */ totalTime: number; /** Average execution time per invocation in milliseconds. */ avgTime: number; }; /** Structured performance report returned by getPerformanceReport(). */ type PerformanceReport = { /** Timing data for lifecycle methods. */ lifecycle: { mountTime?: number; preprocessTime?: number; renderTime?: number; }; /** Effect execution statistics. */ effects: { /** Total number of unique effects tracked. */ total: number; /** Aggregate stats grouped by directive type. */ byDirective: Record<string, { count: number; totalTime: number; }>; /** Top 10 slowest effects by total time. */ slowest: EffectStats[]; }; /** Observer registration statistics. */ observers: { /** Number of keys with registered observers. */ totalKeys: number; /** Total number of observer registrations. */ totalObservers: number; /** Observer count per key. */ byKey: Record<string, number>; }; }; //#endregion export { StoreState as a, IRenderer as i, RenderParams as n, RendererPlugin as r, ParserParams as t };