diff --git a/src/MiniReact.ts b/src/MiniReact.ts index e4863dd..63a5b19 100644 --- a/src/MiniReact.ts +++ b/src/MiniReact.ts @@ -10,34 +10,28 @@ export type { FunctionalComponent, ElementType, MiniReactElement, -} from "./core/types"; - -// Hooks -export { - useState, - useEffect, - useReducer, - useRef, - useMemo, - useCallback, -} from "./hooks"; -export type { - UseStateHook, - UseEffectHook, - UseReducerHook, - UseRefHook, EffectCallback, DependencyList, Reducer, MutableRefObject, -} from "./hooks/types"; +} from "./core/types"; + +// Fiber-based hooks +export { + useStateFiber as useState, + useEffectFiber as useEffect, + useReducerFiber as useReducer, + useRefFiber as useRef, + useMemoFiber as useMemo, + useCallbackFiber as useCallback, +} from "./fiber"; // Context export { createContext, useContext } from "./context"; export type { MiniReactContext } from "./context/types"; // Fragments -export { Fragment } from "./fragments"; +export { FRAGMENT as Fragment } from "./core/types"; // Portals export { createPortal } from "./portals"; @@ -57,18 +51,17 @@ export { jsx, jsxs, jsxDEV } from "./jsx-runtime"; export type { SyntheticEvent } from "./events/types"; import { createContext, useContext } from "./context"; -// Higher-order components import { createElement, render } from "./core"; -import type { FunctionalComponent, VDOMInstance } from "./core/types"; -import { Fragment } from "./fragments"; +import { FRAGMENT as Fragment } from "./core/types"; +import type { FunctionalComponent } from "./core/types"; import { - useCallback, - useEffect, - useMemo, - useReducer, - useRef, - useState, -} from "./hooks"; + useCallbackFiber as useCallback, + useEffectFiber as useEffect, + useMemoFiber as useMemo, + useReducerFiber as useReducer, + useRefFiber as useRef, + useStateFiber as useState, +} from "./fiber"; import { jsx, jsxDEV, jsxs } from "./jsx-runtime"; import { getPerformanceMetrics, @@ -90,7 +83,7 @@ export function memo

>( }; // Store the comparison function and original component for reconciliation - (MemoizedComponent as unknown as Record).__memo = { + (MemoizedComponent as unknown as Record)["__memo"] = { Component, areEqual: areEqual || shallowEqual, }; @@ -122,30 +115,7 @@ function shallowEqual

>( return true; } -import { - popContextValues, - pushContextValues, - setContextRenderInstance, -} from "./context"; -// Set up cross-module dependencies -import { scheduleEffect, setHookContext } from "./hooks"; -import { - setContextHooks, - setHookContext as setReconcilerHookContext, - setScheduleEffect, -} from "./reconciler"; - -// Initialize cross-module dependencies -setScheduleEffect(scheduleEffect); -setContextHooks(pushContextValues, popContextValues); -// Connect context render instance to hooks render instance -setReconcilerHookContext((instance: VDOMInstance | null) => { - setHookContext(instance); - setContextRenderInstance(instance); -}); - -// Export internal utilities for advanced usage -export { setHookContext } from "./hooks"; +// Export event system for advanced usage export { eventSystem } from "./events"; // Constants diff --git a/src/core/index.ts b/src/core/index.ts index 7c8aafc..f9b825a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,22 +2,21 @@ /* Core Functionality */ /* ****************** */ -import { eventSystem } from "../events"; -import { trackRenderEnd, trackRenderStart } from "../performance"; -import { reconcile } from "../reconciler"; +import { + type FiberRoot, + createRoot as createFiberRoot, + flushSync, + updateContainer, +} from "../fiber"; import { type AnyMiniReactElement, type ElementType, type JSXElementType, - PORTAL, TEXT_ELEMENT, - type VDOMInstance, } from "./types"; -// Store root instances for each container -const rootInstances = new Map(); -// Store original root elements for re-rendering -const rootElements = new Map(); +// Store fiber roots for each container +const fiberRoots = new Map(); /** * Creates a MiniReact element (virtual DOM node) @@ -59,7 +58,7 @@ export function createElement( } /** - * Renders a MiniReact element into a container using the reconciler + * Renders a MiniReact element into a container using the fiber reconciler * @param element The element to render (can be null to clear) * @param containerNode The container DOM node */ @@ -67,120 +66,41 @@ export function render( element: AnyMiniReactElement | null | undefined, containerNode: HTMLElement, ): void { - // Initialize event system with the container - eventSystem.initialize(containerNode); - const newElement = element || null; - // Get the old instance BEFORE potentially deleting it - const oldInstance = rootInstances.get(containerNode) || null; - - if (newElement === null) { - // When unmounting, remove original element from map to prevent memory leaks - rootElements.delete(containerNode); - } else { - // Store the original element for re-renders - rootElements.set(containerNode, newElement); + // Get or create fiber root for this container + let root = fiberRoots.get(containerNode); + if (!root) { + root = createFiberRoot(containerNode); + fiberRoots.set(containerNode, root); } - // Track render performance - trackRenderStart(); - const newInstance = reconcile(containerNode, newElement, oldInstance); - trackRenderEnd(); + // Render tracking is handled inside performSyncWorkOnRoot + updateContainer(newElement, root); + flushSync(); if (newElement === null) { - // Ensure container is completely cleared when rendering null - containerNode.innerHTML = ""; - // Clean up rootInstances after reconciliation - rootInstances.delete(containerNode); - } else { - rootInstances.set(containerNode, newInstance); + // Clean up fiber root when unmounting + fiberRoots.delete(containerNode); } } /** - * Finds the root container for a given VDOM instance - * @param instance The VDOM instance - * @returns The root container element or null + * Gets the fiber root for a container + * @param container The container element + * @returns The fiber root or undefined */ -export function findRootContainer(instance: VDOMInstance): HTMLElement | null { - // Strategy 1: Walk up the parent chain and validate rootContainer references - let current: VDOMInstance | undefined = instance; - let depth = 0; - while (current) { - if (current.rootContainer) { - // Verify this rootContainer is actually a real root by checking our rootInstances map - for (const [container, rootInstance] of rootInstances) { - if (container === current.rootContainer && rootInstance) { - return container; - } - } - } - current = current.parent; - depth++; - if (depth > 10) { - console.warn( - "Parent chain depth exceeded 10, breaking to avoid infinite loop", - ); - break; - } - } - - // Strategy 2: Search through all root instances to find the one containing this instance - for (const [container, rootInstance] of rootInstances) { - if (rootInstance && isInstanceInTree(instance, rootInstance)) { - return container; - } - } - - // Strategy 3: If not found in main trees, check if this instance is part of a portal tree - current = instance; - while (current) { - if ( - current.element && - typeof current.element === "object" && - "type" in current.element && - current.element.type === PORTAL - ) { - // Found a portal parent - now find which root tree contains this portal - for (const [container, rootInstance] of rootInstances) { - if (rootInstance && isInstanceInTree(current, rootInstance)) { - return container; - } - } - } - current = current.parent; - } - - return null; +export function getFiberRoot(container: HTMLElement): FiberRoot | undefined { + return fiberRoots.get(container); } -/** - * Checks if a given instance is part of a VDOM tree - * @param targetInstance The instance to find - * @param rootInstance The root of the tree to search - * @returns True if the instance is in the tree - */ -function isInstanceInTree( - targetInstance: VDOMInstance, - rootInstance: VDOMInstance, -): boolean { - if (targetInstance === rootInstance) { - return true; - } +// Re-export fiber-based hydration for SSR +export { hydrateRoot, type HydrateRootOptions } from "../fiber/hydration"; - return rootInstance.childInstances.some((child) => - isInstanceInTree(targetInstance, child), - ); -} - -/** - * Gets the root element for re-rendering - * @param container The container element - * @returns The root element or null - */ -export function getRootElement( - container: HTMLElement, -): AnyMiniReactElement | null { - return rootElements.get(container) || null; -} +// Re-export fiber-based resumability for state persistence +export { + serializeFiberTree, + dehydrateRoot, + parseSerializedRoot, + type SerializedFiberRoot, +} from "../fiber/resumability"; diff --git a/src/dom-renderer/index.ts b/src/dom-renderer/index.ts deleted file mode 100644 index a7aeea4..0000000 --- a/src/dom-renderer/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { type JSXElementType, TEXT_ELEMENT } from "../core/types"; - -/* ******************** */ -/* DOM Renderer Utility */ -/* ******************** */ - -/** - * Checks if a prop name is an event handler - * @param propName The prop name to check - * @returns True if it's an event handler, false otherwise - */ -function isEventHandler(propName: string): boolean { - return propName.startsWith("on") && propName.length > 2; -} - -/** - * Sets a single attribute on a DOM element, handling special cases - * @param element The DOM element - * @param key The attribute name - * @param value The attribute value - */ -function setAttribute(element: Element, key: string, value: unknown): void { - if (key === "className") { - element.setAttribute("class", String(value)); - } else if (key === "style" && typeof value === "string") { - element.setAttribute("style", value); - } else if (typeof value === "boolean") { - // For boolean attributes, only set if true, remove if false - if (value) { - element.setAttribute(key, ""); // Set to empty string for boolean attributes - } else { - element.removeAttribute(key); - } - } else if (value !== undefined && value !== null) { - element.setAttribute(key, String(value)); - } -} - -/** - * Creates a DOM node from a VDOM element - * @param element The VDOM element to convert - * @returns The created DOM node - */ -export function createDomNode(element: JSXElementType): Element | Text { - // Handle text elements (TEXT_ELEMENT) - if (element.type === TEXT_ELEMENT) { - return document.createTextNode(String(element.props.nodeValue)); - } - - // Create host element - const domElement = document.createElement(element.type as string); - - // Set attributes (props), but skip event handlers, children, and key - if (element.props) { - for (const [key, value] of Object.entries(element.props)) { - if (key !== "children" && key !== "key" && !isEventHandler(key)) { - setAttribute(domElement, key, value); - } - } - } - - return domElement; -} - -/** - * Removes a DOM node from its parent - * - * @param domNode The DOM node to remove - */ -export function removeDomNode(domNode: Node): void { - if (domNode.parentNode) { - domNode.parentNode.removeChild(domNode); - } -} - -/** - * Replaces an old DOM node with a new one - * - * @param oldDom The old DOM node - * @param newDom The new DOM node - */ -export function replaceDomNode(oldDom: Node, newDom: Node): void { - if (oldDom.parentNode) { - oldDom.parentNode.replaceChild(newDom, oldDom); - } -} diff --git a/src/fragments/index.ts b/src/fragments/index.ts deleted file mode 100644 index f090e9f..0000000 --- a/src/fragments/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* ************* */ -/* Fragments */ -/* ************* */ - -import { FRAGMENT } from "../core/types"; - -/** - * Fragment component - renders children without creating a wrapper DOM node - */ -export const Fragment: typeof FRAGMENT = FRAGMENT; diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 7d96064..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,465 +0,0 @@ -/* *********** */ -/* Hooks API */ -/* *********** */ - -import { findRootContainer, getRootElement, render } from "../core"; -import type { VDOMInstance } from "../core/types"; -import type { - CallbackHook, - DependencyList, - EffectCallback, - EffectHook, - MemoHook, - MutableRefObject, - Reducer, - ReducerHook, - RefHook, - StateHook, - StateOrEffectHook, - UseStateHook, -} from "./types"; - -// Hook state management -let currentRenderInstance: VDOMInstance | null = null; - -// Effect queue management -const effectQueue: (() => void)[] = []; -let isFlushingEffects = false; - -/** - * Helper function to trigger re-render for a hook instance - * @param hookInstance The VDOM instance that contains the hook - */ -function triggerRerender(hookInstance: VDOMInstance): void { - // Find the root container for this instance and trigger re-render - const container = findRootContainer(hookInstance); - if (container) { - // Use the original root element for re-render instead of stale element from instance - const rootElement = getRootElement(container); - if (rootElement) { - render(rootElement, container); - } else { - console.warn("No root element found for container, skipping re-render"); - } - } else { - console.warn( - "No root container found for hook instance, skipping re-render", - ); - } -} - -/** - * Sets the current render instance for hooks - * @param instance The current VDOM instance - */ -export function setHookContext(instance: VDOMInstance | null): void { - currentRenderInstance = instance; - // Reset hookCursor to 0 at the beginning of each component's render - if (instance) { - instance.hookCursor = 0; - } -} - -/** - * Schedules an effect to be run after the current render - */ -export function scheduleEffect(effectFn: () => void): void { - effectQueue.push(effectFn); - - if (!isFlushingEffects) { - queueMicrotask(flushEffects); - } -} - -/** - * Flushes all queued effects - */ -function flushEffects(): void { - if (isFlushingEffects) return; - - isFlushingEffects = true; - - try { - while (effectQueue.length > 0) { - const effect = effectQueue.shift(); - if (effect) { - effect(); - } - } - } finally { - isFlushingEffects = false; - } -} - -/** - * Gets the effect queue for external access - */ -export function getEffectQueue(): (() => void)[] { - return effectQueue; -} - -/** - * useState hook implementation - * @param initialState The initial state value or function that returns initial state - * @returns A tuple with current state and setState function - */ -export function useState(initialState: T | (() => T)): UseStateHook { - if (!currentRenderInstance) { - throw new Error("useState must be called inside a functional component"); - } - - // Capture the current instance at hook creation time - const hookInstance = currentRenderInstance; - - // Ensure hooks array exists - if (!hookInstance.hooks) { - hookInstance.hooks = []; - } - - const hooks = hookInstance.hooks; - const currentHookIndex = hookInstance.hookCursor ?? 0; - hookInstance.hookCursor = currentHookIndex + 1; - - // Initialize hook if it doesn't exist - if (hooks.length <= currentHookIndex) { - const initialStateValue = - typeof initialState === "function" - ? (initialState as () => T)() - : initialState; - - const stateHook: StateHook = { - type: "state", - state: initialStateValue, - setState: () => {}, // Will be set below - }; - - (hooks as StateOrEffectHook[]).push(stateHook); - } - - const hook = hooks[currentHookIndex] as StateHook; - - // Create setState function with closure over hook and container - const setState = (newState: T | ((prevState: T) => T)) => { - const nextState = - typeof newState === "function" - ? (newState as (prevState: T) => T)(hook.state as T) - : newState; - - // Only update if state actually changed - if (nextState !== hook.state) { - hook.state = nextState; - triggerRerender(hookInstance); - } - }; - - // Update the setState function reference - hook.setState = setState; - - return [hook.state as T, setState]; -} - -/** - * useEffect hook implementation - * @param callback Effect callback function that may return a cleanup function - * @param dependencies Optional dependency array - */ -export function useEffect( - callback: EffectCallback, - dependencies?: DependencyList, -): void { - if (!currentRenderInstance) { - throw new Error("useEffect must be called inside a functional component"); - } - - // Capture the current instance at hook creation time - const hookInstance = currentRenderInstance; - - // Ensure hooks array exists - if (!hookInstance.hooks) { - hookInstance.hooks = []; - } - - const hooks = hookInstance.hooks; - const currentHookIndex = hookInstance.hookCursor ?? 0; - hookInstance.hookCursor = currentHookIndex + 1; - - // Initialize hook if it doesn't exist - if (hooks.length <= currentHookIndex) { - const effectHook: EffectHook = { - type: "effect", - callback, - dependencies, - hasRun: false, - }; - hooks.push(effectHook); - } - - const hook = hooks[currentHookIndex] as EffectHook; - const prevDependencies = hook.dependencies; - - // Check if dependencies have changed - const dependenciesChanged = - dependencies === undefined || - prevDependencies === undefined || - dependencies.length !== prevDependencies.length || - dependencies.some((dep, index) => !Object.is(dep, prevDependencies[index])); - - // Update hook data - hook.callback = callback; - hook.dependencies = dependencies; - - // Schedule effect if dependencies changed or it's the first run - if (!hook.hasRun || dependenciesChanged) { - scheduleEffect(() => { - // Run cleanup from previous effect if it exists - if (hook.cleanup) { - try { - hook.cleanup(); - } catch (error) { - console.error("Error in useEffect cleanup:", error); - } - hook.cleanup = undefined; - } - - // Run the effect - try { - const cleanupFunction = hook.callback(); - if (typeof cleanupFunction === "function") { - hook.cleanup = cleanupFunction; - } - } catch (error) { - console.error("Error in useEffect callback:", error); - } - - hook.hasRun = true; - }); - } -} - -/** - * useReducer hook implementation - * @param reducer The reducer function that takes state and action and returns new state - * @param initialArg The initial state or argument for lazy initialization - * @param init Optional lazy initialization function - * @returns A tuple with current state and dispatch function - */ -export function useReducer( - reducer: Reducer, - initialArg: State, -): [State, (action: Action) => void]; -export function useReducer( - reducer: Reducer, - initialArg: Init, - init: (arg: Init) => State, -): [State, (action: Action) => void]; -export function useReducer( - reducer: Reducer, - initialArg: State | Init, - init?: (arg: Init) => State, -): [State, (action: Action) => void] { - if (!currentRenderInstance) { - throw new Error("useReducer must be called inside a functional component"); - } - - // Capture the current instance at hook creation time - const hookInstance = currentRenderInstance; - - // Ensure hooks array exists - if (!hookInstance.hooks) { - hookInstance.hooks = []; - } - - const hooks = hookInstance.hooks; - const currentHookIndex = hookInstance.hookCursor ?? 0; - hookInstance.hookCursor = currentHookIndex + 1; - - // Initialize hook if it doesn't exist - if (hooks.length <= currentHookIndex) { - // Calculate initial state based on whether init function is provided - const initialState = init - ? init(initialArg as Init) - : (initialArg as State); - - const reducerHook: ReducerHook = { - type: "reducer", - state: initialState, - reducer, - dispatch: () => {}, // Will be set below - }; - - hooks.push(reducerHook as StateOrEffectHook); - } - - const hook = hooks[currentHookIndex] as ReducerHook; - - // Update reducer in case it changed between renders - hook.reducer = reducer; - - // Create dispatch function with closure over hook and container - const dispatch = (action: Action) => { - const nextState = hook.reducer(hook.state, action); - - // Only update if state actually changed - if (nextState !== hook.state) { - hook.state = nextState; - triggerRerender(hookInstance); - } - }; - - // Update the dispatch function reference - hook.dispatch = dispatch; - - return [hook.state, dispatch]; -} - -/** - * useRef hook implementation - * @param initialValue The initial value to set on the ref object - * @returns A mutable ref object with a current property - */ -export function useRef(initialValue: T): MutableRefObject { - if (!currentRenderInstance) { - throw new Error("useRef must be called inside a functional component"); - } - - // Capture the current instance at hook creation time - const hookInstance = currentRenderInstance; - - // Ensure hooks array exists - if (!hookInstance.hooks) { - hookInstance.hooks = []; - } - - const hooks = hookInstance.hooks; - const currentHookIndex = hookInstance.hookCursor ?? 0; - hookInstance.hookCursor = currentHookIndex + 1; - - // Initialize hook if it doesn't exist - if (hooks.length <= currentHookIndex) { - const refHook: RefHook = { - type: "ref", - current: initialValue, - }; - - hooks.push(refHook as StateOrEffectHook); - } - - const hook = hooks[currentHookIndex] as RefHook; - - // Return a reference to the hook itself as the mutable ref object - // This ensures mutations to .current directly modify the hook's state - return hook as MutableRefObject; -} - -/** - * useMemo hook implementation - * @param factory Function that returns the memoized value - * @param dependencies Optional dependency array - * @returns The memoized value - */ -export function useMemo(factory: () => T, dependencies?: DependencyList): T { - if (!currentRenderInstance) { - throw new Error("useMemo must be called inside a functional component"); - } - - // Capture the current instance at hook creation time - const hookInstance = currentRenderInstance; - - // Ensure hooks array exists - if (!hookInstance.hooks) { - hookInstance.hooks = []; - } - - const hooks = hookInstance.hooks; - const currentHookIndex = hookInstance.hookCursor ?? 0; - hookInstance.hookCursor = currentHookIndex + 1; - - // Initialize hook if it doesn't exist - if (hooks.length <= currentHookIndex) { - const memoHook: MemoHook = { - type: "memo", - value: factory(), - dependencies, - hasComputed: true, - }; - - hooks.push(memoHook as StateOrEffectHook); - return memoHook.value; - } - - const hook = hooks[currentHookIndex] as MemoHook; - const prevDependencies = hook.dependencies; - - // Check if dependencies have changed - const dependenciesChanged = - dependencies === undefined || - prevDependencies === undefined || - dependencies.length !== prevDependencies.length || - dependencies.some((dep, index) => !Object.is(dep, prevDependencies[index])); - - // Recompute value if dependencies changed or it's the first computation - if (!hook.hasComputed || dependenciesChanged) { - hook.value = factory(); - hook.dependencies = dependencies; - hook.hasComputed = true; - } - - return hook.value; -} - -/** - * useCallback hook implementation - * @param callback The callback function to memoize - * @param dependencies Optional dependency array - * @returns The memoized callback function - */ -export function useCallback unknown>( - callback: T, - dependencies?: DependencyList, -): T { - if (!currentRenderInstance) { - throw new Error("useCallback must be called inside a functional component"); - } - - // Capture the current instance at hook creation time - const hookInstance = currentRenderInstance; - - // Ensure hooks array exists - if (!hookInstance.hooks) { - hookInstance.hooks = []; - } - - const hooks = hookInstance.hooks; - const currentHookIndex = hookInstance.hookCursor ?? 0; - hookInstance.hookCursor = currentHookIndex + 1; - - // Initialize hook if it doesn't exist - if (hooks.length <= currentHookIndex) { - const callbackHook: CallbackHook = { - type: "callback", - callback, - dependencies, - }; - - hooks.push(callbackHook as StateOrEffectHook); - return callbackHook.callback; - } - - const hook = hooks[currentHookIndex] as CallbackHook; - const prevDependencies = hook.dependencies; - - // Check if dependencies have changed - const dependenciesChanged = - dependencies === undefined || - prevDependencies === undefined || - dependencies.length !== prevDependencies.length || - dependencies.some((dep, index) => !Object.is(dep, prevDependencies[index])); - - // Update callback if dependencies changed - if (dependenciesChanged) { - hook.callback = callback; - hook.dependencies = dependencies; - } - - return hook.callback; -} diff --git a/src/hooks/types.ts b/src/hooks/types.ts deleted file mode 100644 index fb46005..0000000 --- a/src/hooks/types.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* ********** */ -/* Hook Types */ -/* ********** */ - -import type { MiniReactContext } from "../context/types"; - -export interface StateHook { - type: "state"; - state: T; - setState: (newState: T | ((prevState: T) => T)) => void; -} - -export interface EffectHook { - type: "effect"; - callback: EffectCallback; - cleanup?: () => void; - dependencies?: DependencyList; - hasRun: boolean; -} - -export interface ContextHook { - type: "context"; - context: MiniReactContext; - value: T; -} - -export interface ReducerHook { - type: "reducer"; - state: State; - reducer: (state: State, action: Action) => State; - dispatch: (action: Action) => void; -} - -export interface RefHook { - type: "ref"; - current: T; -} - -export interface MemoHook { - type: "memo"; - value: T; - dependencies?: DependencyList; - hasComputed: boolean; -} - -export interface CallbackHook< - T extends (...args: unknown[]) => unknown = (...args: unknown[]) => unknown, -> { - type: "callback"; - callback: T; - dependencies?: DependencyList; -} - -/** - * Union type for hooks stored in component instances. - * Note: The generic parameter T only applies to StateHook and ContextHook, EffectHook ignores it. - */ -export type StateOrEffectHook = - | StateHook - | EffectHook - | ContextHook - | ReducerHook - | RefHook - | MemoHook - | CallbackHook<(...args: unknown[]) => unknown>; - -export type UseStateHook = [ - T, - (newState: T | ((prevState: T) => T)) => void, -]; - -// Effect types -export type EffectCallback = (() => void) | (() => () => void); -export type DependencyList = readonly unknown[]; - -export type UseEffectHook = ( - callback: EffectCallback, - dependencies?: DependencyList, -) => void; - -// Reducer types -export type Reducer = (state: State, action: Action) => State; -export type ReducerStateWithoutAction = R extends Reducer - ? S - : never; -export type ReducerActionWithoutState = R extends Reducer - ? A - : never; - -export type UseReducerHook = { - , I>( - reducer: R, - initializerArg: I, - initializer: (arg: I) => ReducerStateWithoutAction, - ): [ - ReducerStateWithoutAction, - (action: ReducerActionWithoutState) => void, - ]; - >( - reducer: R, - initialState: ReducerStateWithoutAction, - ): [ - ReducerStateWithoutAction, - (action: ReducerActionWithoutState) => void, - ]; -}; - -// Ref types -export type MutableRefObject = { - current: T; -}; - -export type UseRefHook = (initialValue: T) => MutableRefObject; diff --git a/src/jsx-dev-runtime.ts b/src/jsx-dev-runtime.ts index 0d9a5af..4ce6577 100644 --- a/src/jsx-dev-runtime.ts +++ b/src/jsx-dev-runtime.ts @@ -4,4 +4,4 @@ // Export development JSX runtime functions export { jsxDEV as jsx, jsxDEV as jsxs } from "./jsx-runtime/index"; -export { Fragment } from "./fragments"; +export { FRAGMENT as Fragment } from "./core/types"; diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 5a3975a..287a1cf 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -4,4 +4,4 @@ // Re-export JSX runtime functions for build tool integration export { jsx, jsxs, jsxDEV } from "./jsx-runtime/index"; -export { Fragment } from "./fragments"; +export { FRAGMENT as Fragment } from "./core/types"; diff --git a/src/jsx-runtime/index.ts b/src/jsx-runtime/index.ts index 6616916..adab32b 100644 --- a/src/jsx-runtime/index.ts +++ b/src/jsx-runtime/index.ts @@ -19,7 +19,7 @@ export function jsx( const finalProps = { ...restProps }; if (key !== undefined) { - finalProps.key = key; + finalProps["key"] = key; } // Handle children @@ -69,7 +69,7 @@ export function jsxDEV( // Store development metadata if needed (for future dev tools support) if (source && typeof element === "object" && element !== null) { - (element as unknown as Record).__source = source; + (element as unknown as Record)["__source"] = source; } return element; diff --git a/src/reconciler/index.ts b/src/reconciler/index.ts deleted file mode 100644 index 61b4f2c..0000000 --- a/src/reconciler/index.ts +++ /dev/null @@ -1,1090 +0,0 @@ -import type { - AnyMiniReactElement, - FunctionalComponent, - VDOMInstance, -} from "../core/types"; -import { FRAGMENT, PORTAL, TEXT_ELEMENT } from "../core/types"; -import { createDomNode, removeDomNode, replaceDomNode } from "../dom-renderer"; -import { eventSystem } from "../events"; -import type { PortalElement } from "../portals/types"; - -// Import scheduleEffect to properly schedule cleanup -let scheduleEffectFunction: ((effectFn: () => void) => void) | null = null; -// Context management functions -let pushContextFunction: - | ((contextValues: Map) => void) - | null = null; -let popContextFunction: (() => void) | null = null; - -export function setScheduleEffect( - scheduleEffect: (effectFn: () => void) => void, -): void { - scheduleEffectFunction = scheduleEffect; -} - -export function setContextHooks( - pushContext: (contextValues: Map) => void, - popContext: () => void, -): void { - pushContextFunction = pushContext; - popContextFunction = popContext; -} - -/* ********** */ -/* Reconciler */ -/* ********** */ - -/** - * Main reconciliation function that handles creation, updates, and removal of VDOM instances - * - * @param parentDom The parent DOM node (can be null during cleanup) - * @param newElement The new element to reconcile - * @param oldInstance The existing VDOM instance to reconcile against - * @returns The resulting VDOM instance (null if removed) - */ -export function reconcile( - parentDom: Node | null, - newElement: AnyMiniReactElement | null, - oldInstance: VDOMInstance | null, -): VDOMInstance | null { - // Case 1: Element removal - newElement is null but oldInstance exists - if (newElement === null && oldInstance !== null) { - // Handle removal based on element type - if (oldInstance.element.type === PORTAL) { - // For portals, clean up children from their target container - const portalElement = oldInstance.element as PortalElement; - const targetContainer = portalElement.props.targetContainer; - - // Clean up all portal children from target container - for (const childInstance of oldInstance.childInstances) { - if (childInstance?.dom && targetContainer.contains(childInstance.dom)) { - reconcile(null, null, childInstance); - } - } - } else if (oldInstance.element.type === FRAGMENT) { - // For fragments, recursively clean up all children - for (const childInstance of oldInstance.childInstances) { - if (childInstance) { - reconcile(null, null, childInstance); - } - } - } else { - // For regular elements, clean up hooks and remove from DOM - if (oldInstance.hooks && scheduleEffectFunction) { - for (const hook of oldInstance.hooks) { - if (hook.type === "effect" && hook.cleanup) { - const cleanup = hook.cleanup; - scheduleEffectFunction(() => { - try { - cleanup(); - } catch (error) { - console.error("Error in useEffect cleanup:", error); - } - }); - } - } - } - - if (oldInstance.dom) { - eventSystem.unregisterInstance(oldInstance); - removeDomNode(oldInstance.dom); - } - } - - // Clean up child instances - for (const childInstance of oldInstance.childInstances) { - if (childInstance) { - reconcile(null, null, childInstance); - } - } - - return null; - } - - // Case 2: Element creation - newElement exists but oldInstance is null - if (newElement !== null && oldInstance === null) { - if (!parentDom) { - throw new Error("Parent DOM node is required for element creation"); - } - return createVDOMInstance(parentDom, newElement); - } - - // Both should exist at this point - but handle edge cases gracefully - if (!newElement || !oldInstance) { - // Add detailed logging for debugging - console.warn("Reconcile called with suspicious null values:", { - newElement: newElement, - oldInstance: oldInstance, - parentDom: parentDom, - stack: new Error().stack, - }); - - // If we have newElement but no oldInstance, treat as creation - if (newElement && !oldInstance) { - if (!parentDom) { - throw new Error("Parent DOM node is required for element creation"); - } - return createVDOMInstance(parentDom, newElement); - } - // If we have oldInstance but no newElement, treat as removal - if (!newElement && oldInstance) { - return reconcile(parentDom, null, oldInstance); - } - // If both are null, check if this is a valid cleanup scenario - if (!newElement && !oldInstance) { - // This could be a valid case during cleanup - just return null - console.warn("Both newElement and oldInstance are null - returning null"); - return null; - } - // If we reach here, something unexpected happened - throw new Error( - `Unexpected reconciliation state: newElement=${newElement}, oldInstance=${oldInstance}`, - ); - } - - // Case 3: Type change - recreate everything - if (!isSameElementType(oldInstance.element, newElement)) { - if (!parentDom) { - throw new Error( - "Parent DOM node is required for type change reconciliation", - ); - } - const newInstance = createVDOMInstance(parentDom, newElement); - - // Clean up old instance hooks - if (oldInstance.hooks && scheduleEffectFunction) { - for (const hook of oldInstance.hooks) { - if (hook.type === "effect" && hook.cleanup) { - const cleanup = hook.cleanup; - scheduleEffectFunction(() => { - try { - cleanup(); - } catch (error) { - console.error( - "Error in useEffect cleanup during type change:", - error, - ); - } - }); - } - } - } - - // Handle DOM cleanup - fragments and portals need special handling - if (oldInstance.element.type === FRAGMENT) { - // For fragments, recursively clean up all children - for (const childInstance of oldInstance.childInstances) { - if (childInstance) { - reconcile(null, null, childInstance); - } - } - } else if (oldInstance.element.type === PORTAL) { - // For portals, clean up children from their target container - const oldPortalElement = oldInstance.element as PortalElement; - const oldTargetContainer = oldPortalElement.props.targetContainer; - - for (const childInstance of oldInstance.childInstances) { - if ( - childInstance?.dom && - oldTargetContainer.contains(childInstance.dom) - ) { - reconcile(null, null, childInstance); - } - } - } else if (oldInstance.dom && newInstance.dom) { - // For regular elements, replace the DOM node - eventSystem.unregisterInstance(oldInstance); - replaceDomNode(oldInstance.dom, newInstance.dom); - } else if (oldInstance.dom) { - // If old instance had DOM but new doesn't, just remove old - eventSystem.unregisterInstance(oldInstance); - removeDomNode(oldInstance.dom); - } - - return newInstance; - } - - // Case 4: Same type - update existing instance - return updateVDOMInstance(oldInstance, newElement); -} - -/** - * Creates a new VDOM instance for the given element and attaches it to the parent DOM - * - * @param parentDom The parent DOM node - * @param element The element to create an instance for - * @returns The created VDOM instance - */ -function createVDOMInstance( - parentDom: Node, - element: AnyMiniReactElement, -): VDOMInstance { - // Handle primitives by converting them to text elements - if ( - typeof element === "string" || - typeof element === "number" || - typeof element === "boolean" - ) { - const textElement = { - type: TEXT_ELEMENT, - props: { - nodeValue: element, - children: [], - }, - }; - return createVDOMInstance(parentDom, textElement); - } - - // Handle null/undefined elements - if (element === null || element === undefined) { - throw new Error( - "Cannot create VDOM instance for null or undefined element", - ); - } - - // Type guard to ensure we have an element object, not a primitive - if ( - !element || - typeof element !== "object" || - !("type" in element) || - !("props" in element) - ) { - throw new Error("createVDOMInstance expects an element object"); - } - - const { type, props } = element; - - // Handle portals - if (type === PORTAL) { - const portalElement = element as PortalElement; - const targetContainer = portalElement.props.targetContainer; - - // Add event delegation to portal target so events bubble through React tree - eventSystem.addEventDelegation(targetContainer); - - const instance: VDOMInstance = { - element, - dom: null, // Portals don't have their own DOM node in the parent tree - childInstances: [], - rootContainer: - parentDom.nodeType === Node.ELEMENT_NODE - ? (parentDom as HTMLElement) - : undefined, - }; - - // Render children directly to the target container - // Portal children should inherit context from their logical parent, not DOM parent - instance.childInstances = reconcileChildren( - targetContainer, - [], - portalElement.props.children, - instance, - ); - - // Ensure all portal children have the correct rootContainer for state management - function propagateRootContainer( - instances: VDOMInstance[], - rootContainer: HTMLElement | undefined, - ) { - for (const childInstance of instances) { - if (!childInstance.rootContainer && rootContainer) { - childInstance.rootContainer = rootContainer; - } - propagateRootContainer(childInstance.childInstances, rootContainer); - } - } - propagateRootContainer(instance.childInstances, instance.rootContainer); - - return instance; - } - - // Handle fragments - if (type === FRAGMENT) { - const instance: VDOMInstance = { - element, - dom: null, // Fragments don't have their own DOM node - childInstances: [], - rootContainer: - parentDom.nodeType === Node.ELEMENT_NODE - ? (parentDom as HTMLElement) - : undefined, - }; - - // Use reconcileChildren to handle fragment children properly - // This will ensure correct ordering through reorderDomNodes - instance.childInstances = reconcileChildren( - parentDom, - [], - props.children, - instance, - ); - - return instance; - } - - // Handle functional components - if (typeof type === "function") { - // Determine rootContainer - prefer the actual root container over immediate DOM parent - let rootContainer: HTMLElement | undefined; - if (parentDom.nodeType === Node.ELEMENT_NODE) { - const parentElement = parentDom as HTMLElement; - // If parentDom is the main app container, use it - if (parentElement === document.body) { - rootContainer = parentElement; - } else { - // Otherwise, try to find the root container by looking for a container with an ID or walking up - let current: HTMLElement | null = parentElement; - while (current && current !== document.body) { - if (current.id) { - rootContainer = current; - break; - } - current = current.parentElement; - } - // Fallback to the immediate parent if we can't find a better root - if (!rootContainer) { - rootContainer = parentElement; - } - } - } - - // Create the instance - const instance: VDOMInstance = { - element, - dom: null, - childInstances: [], - hooks: [], // Initialize hooks array - rootContainer, - }; - - // Set hook context before calling component - setCurrentRenderInstance(instance); - - let childElement: AnyMiniReactElement | null; - try { - childElement = (type as FunctionalComponent)(props); - } finally { - setCurrentRenderInstance(null); // always reset - } - // Check if this component is a context provider (has contextValues) - // and push context BEFORE reconciling children - let contextWasPushed = false; - if (instance.contextValues && pushContextFunction) { - pushContextFunction(instance.contextValues); - contextWasPushed = true; - } - - let childInstance: VDOMInstance | null = null; - try { - childInstance = childElement - ? createVDOMInstance(parentDom, childElement) - : null; - } finally { - // Pop context AFTER reconciling children - ensure this happens even on exceptions - if (contextWasPushed && popContextFunction) { - popContextFunction(); - } - } - - if (childInstance) { - childInstance.parent = instance; // Set parent reference for functional component child - // Ensure child inherits the same rootContainer - if (!childInstance.rootContainer && instance.rootContainer) { - childInstance.rootContainer = instance.rootContainer; - } - } - - instance.dom = childInstance?.dom || null; - instance.childInstances = childInstance ? [childInstance] : []; - - // Don't register functional components with event system - // They don't have their own DOM nodes and shouldn't be in the event path - - return instance; - } - - // Handle host elements (including text elements) - const domNode = createDomNode(element); - const childInstances: VDOMInstance[] = []; - - // Create the instance - const instance: VDOMInstance = { - element, - dom: domNode, - childInstances: [], - rootContainer: - parentDom.nodeType === Node.ELEMENT_NODE - ? (parentDom as HTMLElement) - : undefined, - }; - - // Register with event system for host elements - eventSystem.registerInstance(instance, domNode); - - // Check if this element has event handlers that need delegation - eventSystem.hasEventHandlers(props as Record); - - // Process children - for (const child of props.children) { - const childInstance = createVDOMInstance(domNode, child); - childInstance.parent = instance; // Set parent reference - childInstances.push(childInstance); - - // Handle DOM insertion based on child type - if (childInstance.element.type === FRAGMENT) { - // For fragments, append all their DOM children - for (const fragChild of childInstance.childInstances) { - if (fragChild.dom) { - domNode.appendChild(fragChild.dom); - } - } - } else if (childInstance.dom) { - // For regular elements, append the DOM node - domNode.appendChild(childInstance.dom); - } - } - - // Update instance with children - instance.childInstances = childInstances; - - // Append to parent - parentDom.appendChild(domNode); - - return instance; -} - -// Hook context function - will be set by MiniReact module -let setCurrentRenderInstance: (instance: VDOMInstance | null) => void = - () => {}; - -/** - * Sets the hook context function from MiniReact module - * @param fn The setCurrentRenderInstance function - */ -export function setHookContext( - fn: (instance: VDOMInstance | null) => void, -): void { - setCurrentRenderInstance = fn; -} - -/** - * Checks if two elements have the same type for reconciliation purposes - * - * @param oldElement The old element - * @param newElement The new element - * @returns True if the elements have the same type, false otherwise - */ -function isSameElementType( - oldElement: AnyMiniReactElement, - newElement: AnyMiniReactElement, -): boolean { - // Type guards to ensure we have element objects - if ( - !oldElement || - typeof oldElement !== "object" || - !("type" in oldElement) || - !newElement || - typeof newElement !== "object" || - !("type" in newElement) - ) { - return false; - } - return oldElement.type === newElement.type; -} - -/** - * Efficiently diffs props and applies only the necessary DOM updates - * - * @param domElement The DOM element to update - * @param oldProps The previous props - * @param newProps The new props - * @param instance The VDOM instance (for event system registration) - */ -function diffProps( - domElement: Element, - oldProps: Record, - newProps: Record, -): void { - // Helper function to check if a prop is an event handler - const isEventHandler = (key: string): boolean => - key.startsWith("on") && key.length > 2; - - // Create sets of old and new prop keys (excluding children, key, and event handlers) - const oldKeys = new Set( - Object.keys(oldProps).filter( - (key) => key !== "children" && key !== "key" && !isEventHandler(key), - ), - ); - const newKeys = new Set( - Object.keys(newProps).filter( - (key) => key !== "children" && key !== "key" && !isEventHandler(key), - ), - ); - - // Remove props that are no longer present - for (const key of oldKeys) { - if (!newKeys.has(key)) { - domElement.removeAttribute(key === "className" ? "class" : key); - } - } - - // Update props that have changed or are new - for (const key of newKeys) { - const oldValue = oldProps[key]; - const newValue = newProps[key]; - - if (oldValue !== newValue) { - setAttribute(domElement, key, newValue); - } - } -} - -/** - * Sets an attribute on a DOM element with proper handling for special cases - * - * @param domElement The DOM element - * @param key The attribute key - * @param value The attribute value - */ -function setAttribute(domElement: Element, key: string, value: unknown): void { - if (key === "key") { - // Keys are used internally for reconciliation and should not be set as DOM attributes - return; - } - if (key === "className") { - domElement.setAttribute("class", String(value)); - } else if (key.startsWith("on") && typeof value === "function") { - // Event handling is now managed by the event system - // No need to attach individual listeners here - } else if (typeof value === "boolean") { - // Handle boolean attributes specially - if (value) { - domElement.setAttribute(key, ""); // Set to empty string for boolean attributes - } else { - // Remove the attribute when boolean value is false - domElement.removeAttribute(key); - } - } else if (value !== undefined && value !== null) { - domElement.setAttribute(key, String(value)); - } else if (value === null || value === undefined) { - // Remove the attribute when value is null/undefined - domElement.removeAttribute(key); - } -} - -/** - * Efficiently reconciles children using key-based diffing and DOM node reuse - * - * @param parentDom The parent DOM node - * @param oldChildInstances The existing child VDOM instances - * @param newChildElements The new child elements to render - * @param parentInstance The parent VDOM instance (optional) - * @returns The updated child instances - */ -function reconcileChildren( - parentDom: Node, - oldChildInstances: VDOMInstance[], - newChildElements: AnyMiniReactElement[], - parentInstance?: VDOMInstance, -): VDOMInstance[] { - // Filter out null/undefined elements early and handle them separately - const filteredNewChildElements = newChildElements.filter( - (child) => child !== null && child !== undefined, - ); - - // Separate keyed and unkeyed children - const oldKeyed = new Map(); - const oldUnkeyed: VDOMInstance[] = []; - const newKeyed = new Map(); - const newUnkeyed: AnyMiniReactElement[] = []; - - // Categorize old children by key - for (const oldChild of oldChildInstances) { - if (oldChild) { - // Add null check for safety - const key = getElementKey(oldChild.element); - if (key !== null) { - oldKeyed.set(key, oldChild); - } else { - oldUnkeyed.push(oldChild); - } - } - } - - // Categorize new children by key - only process filtered elements - for (const newChild of filteredNewChildElements) { - const key = getElementKey(newChild); - if (key !== null) { - newKeyed.set(key, newChild); - } else { - newUnkeyed.push(newChild); - } - } - - const newChildInstances: VDOMInstance[] = []; - let unkeyedIndex = 0; - - // Process new children in order - only process filtered elements - for (let i = 0; i < filteredNewChildElements.length; i++) { - const newChild = filteredNewChildElements[i]; - - // Skip null/undefined elements (already filtered but add extra safety) - if (newChild === null || newChild === undefined) { - continue; - } - - const key = getElementKey(newChild); - let newChildInstance: VDOMInstance | null = null; - - if (key !== null) { - // Handle keyed child - const oldChildInstance = oldKeyed.get(key) || null; - newChildInstance = reconcile(parentDom, newChild, oldChildInstance); - - // Remove from oldKeyed so we know it's been processed - if (oldChildInstance) { - oldKeyed.delete(key); - } - } else { - // Handle unkeyed child - match with next available unkeyed old child - const oldChildInstance = oldUnkeyed[unkeyedIndex] || null; - newChildInstance = reconcile(parentDom, newChild, oldChildInstance); - unkeyedIndex++; - } - - if (newChildInstance) { - // Set parent reference for the new child instance - newChildInstance.parent = parentInstance; - // Inherit rootContainer if child doesn't have one - if (!newChildInstance.rootContainer && parentInstance?.rootContainer) { - newChildInstance.rootContainer = parentInstance.rootContainer; - } - // Special case: if parent is a portal, ensure children inherit the portal's root container - if ( - parentInstance?.element.type === PORTAL && - parentInstance.rootContainer - ) { - newChildInstance.rootContainer = parentInstance.rootContainer; - } - newChildInstances.push(newChildInstance); - } - } - - // Remove any remaining old keyed children that weren't reused - for (const [, oldChild] of oldKeyed) { - // Clean up based on element type - if (oldChild.element.type === PORTAL) { - // For portals, clean up from their target container - const portalElement = oldChild.element as PortalElement; - const targetContainer = portalElement.props.targetContainer; - - for (const childInstance of oldChild.childInstances) { - if (childInstance?.dom && targetContainer.contains(childInstance.dom)) { - reconcile(null, null, childInstance); - } - } - } else if (oldChild) { - // For regular elements and fragments - add null check - reconcile(null, null, oldChild); - } - } - - // Remove any remaining old unkeyed children that weren't reused - for (let i = unkeyedIndex; i < oldUnkeyed.length; i++) { - const oldChild = oldUnkeyed[i]; - // Clean up based on element type - if (oldChild?.element.type === PORTAL) { - // For portals, clean up from their target container - const portalElement = oldChild.element as PortalElement; - const targetContainer = portalElement.props.targetContainer; - - for (const childInstance of oldChild.childInstances) { - if (childInstance?.dom && targetContainer.contains(childInstance.dom)) { - reconcile(null, null, childInstance); - } - } - } else if (oldChild) { - // For regular elements and fragments - add null check - reconcile(null, null, oldChild); - } - } - - // Ensure DOM nodes are in correct order (only for non-portal parent containers) - if (parentInstance?.element.type !== PORTAL) { - reorderDomNodes(parentDom, newChildInstances); - } - - return newChildInstances; -} - -/** - * Extracts the key from an element, returning null if no key is present - * - * @param element The element to get the key from - * @returns The key string or null - */ -function getElementKey(element: AnyMiniReactElement): string | null { - // Handle null/undefined elements - if (element === null || element === undefined) { - return null; - } - - // Handle primitive values (string, number, boolean) - if ( - typeof element === "string" || - typeof element === "number" || - typeof element === "boolean" - ) { - return null; // Primitives don't have keys - } - - // Type guard to ensure we have an element object - if (!element || typeof element !== "object" || !("props" in element)) { - return null; - } - - const key = (element.props as Record).key; - return key !== undefined && key !== null ? String(key) : null; -} - -/** - * Reorders DOM nodes to match the order of VDOM instances - * - * @param parentDom The parent DOM node - * @param childInstances The child instances in the desired order - */ -function reorderDomNodes( - parentDom: Node, - childInstances: VDOMInstance[], -): void { - // Collect all DOM nodes that should be in this parent, in order - const targetDomNodes: Node[] = []; - - function collectDomNodes(instances: VDOMInstance[]): void { - for (const instance of instances) { - if (instance.element.type === PORTAL) { - // Skip portal instances - their children render to different containers - } else if (instance.dom && instance.dom.parentNode === parentDom) { - targetDomNodes.push(instance.dom); - } else if (instance.element.type === FRAGMENT) { - // For fragments, collect their children's DOM nodes - collectDomNodes(instance.childInstances); - } - } - } - - collectDomNodes(childInstances); - - // Now reorder the DOM nodes to match the target order - let currentDomChild = parentDom.firstChild; - for (const targetNode of targetDomNodes) { - if (currentDomChild !== targetNode) { - parentDom.insertBefore(targetNode, currentDomChild); - } - currentDomChild = targetNode.nextSibling; - } -} - -/** - * Updates an existing VDOM instance with a new element (same type) - * - * @param instance The existing VDOM instance - * @param newElement The new element to update the VDOM instance with - * @returns The updated VDOM instance - */ -function updateVDOMInstance( - instance: VDOMInstance, - newElement: AnyMiniReactElement, -): VDOMInstance { - // Type guard to ensure we have an element object - if ( - !newElement || - typeof newElement !== "object" || - !("type" in newElement) || - !("props" in newElement) - ) { - throw new Error("updateVDOMInstance expects an element object"); - } - - const { type, props } = newElement; - - // Handle portals - if (type === PORTAL) { - const portalElement = newElement as PortalElement; - const targetContainer = portalElement.props.targetContainer; - - // For portal updates, we need to check if the target container changed - const oldPortalElement = instance.element as PortalElement; - const oldTargetContainer = oldPortalElement.props.targetContainer; - - // If target container changed, clean up old container first - if (oldTargetContainer !== targetContainer) { - // Clean up old portal children from old container - for (const childInstance of instance.childInstances) { - if (childInstance) { - reconcile(null, null, childInstance); - } - } - instance.childInstances = []; - } - - // Update portal children in the (possibly new) target container - instance.element = newElement; - instance.childInstances = reconcileChildren( - targetContainer, - instance.childInstances, - portalElement.props.children, - instance, - ); - - return instance; - } - - // Handle fragments - if (type === FRAGMENT) { - // Find parent DOM node for fragments - let parentNode: Node | null = null; - - // Strategy 1: Check if any current child has a DOM parent - for (const childInstance of instance.childInstances) { - if (childInstance.dom?.parentNode) { - parentNode = childInstance.dom.parentNode; - break; - } - } - - // Strategy 2: Walk up parent tree to find DOM node - if (!parentNode) { - let currentParent = instance.parent; - while (currentParent && !parentNode) { - if (currentParent.dom) { - parentNode = currentParent.dom; - break; - } - // For fragments, check if any child has a DOM node we can use - if (currentParent.element.type === FRAGMENT) { - for (const childInstance of currentParent.childInstances) { - if (childInstance.dom?.parentNode) { - parentNode = childInstance.dom.parentNode; - break; - } - } - if (parentNode) break; - } - currentParent = currentParent.parent; - } - } - - if (!parentNode) { - throw new Error("Unable to find parent node for fragment reconciliation"); - } - - // Reconcile fragment children directly with parent DOM - instance.childInstances = reconcileChildren( - parentNode, - instance.childInstances, - props.children, - instance, - ); - - return instance; - } - - // Handle functional components - re-execute and reconcile output - if (typeof type === "function") { - // Check for memo optimization - const memoInfo = (type as unknown as Record).__memo as - | { - Component: FunctionalComponent; - areEqual: ( - prevProps: Record, - nextProps: Record, - ) => boolean; - } - | undefined; - - // If this is a memoized component, check if props have changed - if (memoInfo) { - const oldProps = instance.element.props as Record; - const newProps = props as Record; - - // If props haven't changed according to the comparison function, skip re-render - if (memoInfo.areEqual(oldProps, newProps)) { - // Update the element but keep the same child instances - instance.element = newElement; - return instance; - } - } - - // Set hook context before calling component - setCurrentRenderInstance(instance); - - const childElement = (type as FunctionalComponent)(props); - - setCurrentRenderInstance(null); // Clear context after call - - const oldChildInstance = instance.childInstances[0] || null; - - // Find the correct parent node to reconcile in - let parentNode: Node | null = null; - - // Strategy 1: Check if old child has a parent DOM node - if (oldChildInstance?.dom?.parentNode) { - parentNode = oldChildInstance.dom.parentNode; - } - // Strategy 2: Check if this instance has a DOM parent - else if (instance.dom?.parentNode) { - parentNode = instance.dom.parentNode; - } - // Strategy 3: Walk up the parent tree to find a DOM node - else { - let currentParent = instance.parent; - while (currentParent && !parentNode) { - if (currentParent.dom) { - parentNode = currentParent.dom; - break; - } - // For fragments, check if any child has a DOM node we can use - if (currentParent.element.type === FRAGMENT) { - for (const childInstance of currentParent.childInstances) { - if (childInstance.dom?.parentNode) { - parentNode = childInstance.dom.parentNode; - break; - } - } - if (parentNode) break; - } - currentParent = currentParent.parent; - } - } - - // Strategy 4: Check siblings for DOM parents - if (!parentNode && instance.parent) { - for (const sibling of instance.parent.childInstances) { - if (sibling !== instance && sibling.dom?.parentNode) { - parentNode = sibling.dom.parentNode; - break; - } - } - } - - // Strategy 5: If still no parent, try to find root container - if (!parentNode) { - // Walk up to find the root container by checking if this instance is a root - let currentInstance: VDOMInstance | undefined = instance; - while (currentInstance?.parent) { - currentInstance = currentInstance.parent; - } - // Check if any child of root has a DOM node - if (currentInstance?.childInstances) { - for (const child of currentInstance.childInstances) { - if (child.dom?.parentNode) { - parentNode = child.dom.parentNode; - break; - } - } - } - // If we found a root instance, check if it has a rootContainer - if (!parentNode && currentInstance?.rootContainer) { - parentNode = currentInstance.rootContainer; - } - } - - // Strategy 6: Use instance's own rootContainer as last resort - if (!parentNode && instance.rootContainer) { - parentNode = instance.rootContainer; - } - - if (!parentNode) { - throw new Error( - "Unable to find parent node for functional component reconciliation", - ); - } - - // Special case: if component was rendering something but now returns null, - // we need to clean up the functional component's effects - if (oldChildInstance && childElement === null) { - // Schedule cleanup for the functional component's hooks when it stops rendering - if (instance.hooks && scheduleEffectFunction) { - for (const hook of instance.hooks) { - if (hook.type === "effect" && hook.cleanup) { - const cleanup = hook.cleanup; - scheduleEffectFunction(() => { - try { - cleanup(); - } catch (error) { - console.error( - "Error in useEffect cleanup during null return:", - error, - ); - } - }); - hook.cleanup = undefined; - hook.hasRun = false; // Reset for potential future re-renders - } - } - } - } - - // Check if this component is a context provider (has contextValues) - // and push context BEFORE reconciling children - let contextWasPushed = false; - if (instance.contextValues && pushContextFunction) { - pushContextFunction(instance.contextValues); - contextWasPushed = true; - } - - let childInstance: VDOMInstance | null = null; - try { - childInstance = reconcile(parentNode, childElement, oldChildInstance); - if (childInstance) { - childInstance.parent = instance; - } - } finally { - // Pop context AFTER reconciling children - ensure this happens even on exceptions - if (contextWasPushed && popContextFunction) { - popContextFunction(); - } - } - - // Update the existing instance in-place instead of creating a new one - // This preserves the event system mappings and other references - instance.element = newElement; - instance.dom = childInstance?.dom || null; - instance.childInstances = childInstance ? [childInstance] : []; - - // hooks are already preserved on the instance - - return instance; - } - - // Handle host elements - use efficient prop diffing - const oldProps = instance.element.props as Record; - instance.element = newElement; - - // For host elements, update only changed DOM attributes using diffProps - if (instance.dom && instance.dom.nodeType === Node.ELEMENT_NODE) { - const domElement = instance.dom as Element; - const newProps = props as Record; - - // Use efficient prop diffing instead of naive clear-and-set approach - diffProps(domElement, oldProps, newProps); - } - - // For now, we'll just update the DOM node if it's a text element - if (instance.dom && instance.dom.nodeType === Node.TEXT_NODE) { - instance.dom.nodeValue = String(props.nodeValue); - } - - // Efficient children reconciliation with key-based diffing - if (instance.dom) { - instance.childInstances = reconcileChildren( - instance.dom, - instance.childInstances, - props.children, - instance, - ); - } - - return instance; -} diff --git a/tsconfig.json b/tsconfig.json index 2ed5981..a92a5c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,10 +20,15 @@ "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, + // Strict flags + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "noImplicitOverride": true, // Path aliases "paths": {