diff --git a/src/runtime/native/react/rules_new.ts b/src/runtime/native/react/rules_new.ts new file mode 100644 index 0000000..9dd008e --- /dev/null +++ b/src/runtime/native/react/rules_new.ts @@ -0,0 +1,30 @@ +import { StyleCollection } from "../injection"; +import type { + ContainerContextValue, + VariableContextValue, +} from "../reactivity"; +import type { GetFunction } from "../tracking"; +import type { Config } from "./useNativeCss"; + +export function updateRulesNew( + get: GetFunction, + originalProps: Record, + configs: Config[], + inheritedVariables: VariableContextValue, + inheritedContainers: ContainerContextValue, +) { + let inlineVariables: Record | undefined; + let animated = false; + + for (const config of configs) { + const styleRuleSet = []; + + const source = originalProps[config.source]; + if (typeof source === "string") { + const classNames = source.split(/\s+/); + for (const className of classNames) { + styleRuleSet.push(...get(StyleCollection.styles(className))); + } + } + } +} diff --git a/src/runtime/native/react/useNativeCSS_new.ts b/src/runtime/native/react/useNativeCSS_new.ts new file mode 100644 index 0000000..870f14e --- /dev/null +++ b/src/runtime/native/react/useNativeCSS_new.ts @@ -0,0 +1,18 @@ +import { type ComponentType } from "react"; + +import type { Config } from "prettier"; + +import { ContainerContext, VariableContext } from "../reactivity"; +import { useDeepWatcher } from "../tracking"; + +export function useNativeCss( + type: ComponentType, + originalProps: Record | undefined | null, + configs: Config[] = [{ source: "className", target: "style" }], +) { + const state = useDeepWatcher(() => { + return {}; + }, [configs, originalProps, VariableContext, ContainerContext]); + + return null; +} diff --git a/src/runtime/native/signals.ts b/src/runtime/native/signals.ts new file mode 100644 index 0000000..2bf6d97 --- /dev/null +++ b/src/runtime/native/signals.ts @@ -0,0 +1,81 @@ +// signals.ts + +type Subscriber = () => void; + +export interface Signal { + _get(): T; + _subs: Set; + set(value: T): void; + subscribe(fn: Subscriber): () => void; +} + +export type GetFunction = (sig: Signal) => T; + +export function signal(initialValue: T): Signal { + let value = initialValue; + const subscribers = new Set(); + + function _get(): T { + return value; + } + + function set(newValue: T): void { + if (newValue !== value) { + value = newValue; + for (const fn of subscribers) { + if (isBatching) { + batchQueue.add(fn); + } else { + fn(); + } + } + } + } + + function subscribe(fn: Subscriber): () => void { + subscribers.add(fn); + return () => subscribers.delete(fn); + } + + return { _get, _subs: subscribers, set, subscribe }; +} + +// computed + +export function computed(fn: (get: GetFunction) => T) { + const s = signal(undefined as unknown as T); + + const cleanupFns = new Set<() => void>(); + + const run = (): void => { + s.set(fn(get)); + }; + + const get: GetFunction = (signal) => { + signal._subs.add(run); + cleanupFns.add(() => signal._subs.delete(run)); + return signal._get(); + }; + + run(); + + return s; +} + +// Batching system + +let isBatching = false; +const batchQueue = new Set(); + +function flushBatch(): void { + const queue = Array.from(batchQueue); + batchQueue.clear(); + isBatching = false; + for (const fn of queue) fn(); +} + +export function batch(fn: (get: GetFunction) => void): void { + isBatching = true; + fn((sig) => sig._get()); + flushBatch(); +} diff --git a/src/runtime/native/tracking.ts b/src/runtime/native/tracking.ts new file mode 100644 index 0000000..8dee11e --- /dev/null +++ b/src/runtime/native/tracking.ts @@ -0,0 +1,186 @@ +import { use, useState, type Context } from "react"; + +const LOCK = Symbol("react-native-css-lock"); + +const IS_EQUAL = Symbol("react-native-css-is-equal"); +interface IsEqualObject { + [IS_EQUAL]: (other: unknown) => boolean; +} + +export type GetFunction = (sig: Signal) => T; +type Subscriber = () => void; + +export interface Signal { + _get(): T; + _subs: Set; + set(value: T): void; + subscribe(fn: Subscriber): () => void; +} + +function cleanupSubscriptions(subscriptions: Set<() => void>): void { + for (const dispose of subscriptions) { + dispose(); + } + subscriptions.clear(); +} + +/** + * A custom built watcher for the library + */ +export function useDeepWatcher< + T extends object, + Configs extends object, + Props extends object, + Variables extends object, + Containers extends object, +>( + fn: (get: GetFunction, ...args: [Configs, Props, Variables, Containers]) => T, + deps: [ + Configs, + Props | undefined | null, + Context, + Context, + ], +) { + const [state, setState] = useState(() => { + const subscriptions = new Set(); + + const configs = deps[0]; + const props = makeAccessTreeProxy(deps[1] ?? ({} as Props)); + + const lazyVariables = makeLazyContext(deps[2]); + const variables = makeAccessTreeProxy(lazyVariables); + + const lazyContainers = makeLazyContext(deps[3]); + const containers = makeAccessTreeProxy(lazyContainers); + + const get: GetFunction = (signal) => { + const dispose = signal.subscribe(() => { + cleanupSubscriptions(subscriptions); + + lazyVariables[LOCK] = true; + lazyContainers[LOCK] = true; + + setState((s) => ({ + ...s, + value: fn(get, configs, props, variables, containers), + })); + }); + + subscriptions.add(dispose); + return signal._get(); + }; + + const value = fn(get, configs, props, variables, containers); + + return { + value, + subscriptions, + deps: [configs, props, variables, containers], + }; + }); + + if ( + state.deps.some((dep, index) => { + return !(dep as IsEqualObject)[IS_EQUAL](deps[index]); + }) + ) { + setState((s) => { + const subscriptions = s.subscriptions; + cleanupSubscriptions(subscriptions); + + const configs = deps[0]; + const props = makeAccessTreeProxy(deps[1] ?? ({} as Props)); + + const lazyVariables = makeLazyContext(deps[2]); + const variables = makeAccessTreeProxy(lazyVariables); + + const lazyContainers = makeLazyContext(deps[3]); + const containers = makeAccessTreeProxy(lazyContainers); + + const get: GetFunction = (signal) => { + const dispose = signal.subscribe(() => { + cleanupSubscriptions(subscriptions); + + lazyVariables[LOCK] = true; + lazyContainers[LOCK] = true; + + setState((s) => ({ + ...s, + value: fn(get, configs, props, variables, containers), + })); + }); + + subscriptions.add(dispose); + return signal._get(); + }; + + return { + value: fn(get, configs, props, variables, containers), + subscriptions: s.subscriptions, + deps: [configs, props, variables, containers], + }; + }); + } + + return state.value; +} + +function makeLazyContext(context: React.Context) { + let locked = false; + let ctx: T | undefined; + + return new Proxy( + {}, + { + get(_, prop, receiver) { + if (prop === LOCK) { + locked = true; + return undefined; + } + + if (locked) { + if (ctx === undefined) { + return; + } + return Reflect.get(ctx, prop, receiver); + } + + ctx ??= makeAccessTreeProxy(use(context)); + + return Reflect.get(ctx, prop, receiver); + }, + }, + ) as T & { [LOCK]: true }; +} + +function makeAccessTreeProxy(value: T): T { + const branches = new Map(); + + return new Proxy(value, { + get(target, prop, receiver) { + if (prop === IS_EQUAL) { + return (other: T) => { + return ( + Object.is(target, other) || + Array.from(branches).every(([key, child]) => { + return typeof child === "object" && IS_EQUAL in child + ? (child as IsEqualObject)[IS_EQUAL](other[key]) + : Object.is(child, other[key]); + }) + ); + }; + } + + const value = Reflect.get(target, prop, receiver); + + if (typeof value === "object" && value !== null) { + const proxy = makeAccessTreeProxy(value); + branches.set(prop as keyof T, proxy); + return proxy; + } else { + return value; + } + }, + }); +} diff --git a/src/runtime/native/useTrackedMemo.ts b/src/runtime/native/useTrackedMemo.ts new file mode 100644 index 0000000..0454985 --- /dev/null +++ b/src/runtime/native/useTrackedMemo.ts @@ -0,0 +1,108 @@ +import { use, useState, type Context } from "react"; + +export const CHANGED = Symbol("react-native-css-changed"); +export const TRACKED = Symbol("react-native-css-tracked"); + +const cache = new WeakMap(); + +type TrackedProxy = T & { + [CHANGED](prev?: Record): boolean; + [TRACKED]: Record; +}; + +function makeProxy( + obj: T, + path?: string, + tracked: Record = {}, +): TrackedProxy { + return new Proxy(obj, { + get(target, prop, receiver) { + if (prop === CHANGED) { + return (prev: Record | undefined) => { + if (!prev || Object.is(tracked, prev)) { + return false; + } + + for (const key in tracked) { + if (!Object.is(tracked[key], prev[key])) { + return true; + } + } + + return false; + }; + } + + if (prop === TRACKED) { + return tracked; + } + + if (typeof prop !== "string") { + return Reflect.get(target, prop, receiver); + } + + const fullPath = path ? `${path}.${prop}` : prop; + const result = Reflect.get(target, prop, receiver); + + tracked[fullPath] = result; + + if (typeof result === "object" && result !== null) { + return makeProxy(result, fullPath, tracked); + } + + return result; + }, + }) as TrackedProxy; +} + +const DEFAULT_TRACKED = { [CHANGED]: () => false, [TRACKED]: {} }; + +export function createTrackedProxy( + obj: T | undefined | null, +): TrackedProxy { + obj ??= DEFAULT_TRACKED as T; + + let cached = cache.get(obj) as TrackedProxy | undefined; + if (!cached) { + cached = makeProxy(Object.assign({}, DEFAULT_TRACKED, obj)); + cache.set(obj, cached); + } + return cached; +} + +/** + * Turns a Context into a lazy proxy that tracks changes. + */ +export function createLazyContextProxy(context: Context) { + return new Proxy( + {}, + { + get(target, prop, receiver) { + target = use(context); + return Reflect.get(target, prop, receiver) as C[keyof C]; + }, + }, + ) as C; +} + +export function useAutoTrackedMemo( + fn: (...args: object[]) => R, + ...args: Parameters +): R | undefined { + const [lastValues, setLastValues] = useState< + Record> + >({}); + + const trackedArgs = args.map((arg) => createTrackedProxy(arg)); + const result = fn(...trackedArgs); + + const changed = trackedArgs.some((trackedArg, index) => { + return trackedArg[CHANGED](lastValues[`${index}`]); + }); + + if (changed) { + setLastValues({}); + } + + return result; +}