mancha
Version:
Javscript HTML rendering engine
418 lines (417 loc) • 17.1 kB
TypeScript
//#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 };