import { Owner, createContext, createMemo, useContext, runWithOwner, onError, Accessor, Setter, Signal, castError, onCleanup, cleanNode, BRANCH } from "./reactive.js"; import type { JSX } from "../jsx.js"; export type Component

= (props: P) => JSX.Element; export type VoidProps

= P & { children?: never }; export type VoidComponent

= Component>; export type ParentProps

= P & { children?: JSX.Element }; export type ParentComponent

= Component>; export type FlowProps

= P & { children: C }; export type FlowComponent

= Component>; export type Ref = T | ((val: T) => void); export type ValidComponent = keyof JSX.IntrinsicElements | Component | (string & {}); export type ComponentProps = T extends Component ? P : T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : Record; function resolveSSRNode(node: any): string { const t = typeof node; if (t === "string") return node; if (node == null || t === "boolean") return ""; if (Array.isArray(node)) { let mapped = ""; for (let i = 0, len = node.length; i < len; i++) mapped += resolveSSRNode(node[i]); return mapped; } if (t === "object") return node.t; if (t === "function") return resolveSSRNode(node()); return String(node); } type SharedConfig = { context?: HydrationContext; }; export const sharedConfig: SharedConfig = {}; function setHydrateContext(context?: HydrationContext): void { sharedConfig.context = context; } function nextHydrateContext(): HydrationContext | undefined { return sharedConfig.context ? { ...sharedConfig.context, id: `${sharedConfig.context.id}${sharedConfig.context.count++}-`, count: 0 } : undefined; } export function createUniqueId(): string { const ctx = sharedConfig.context; if (!ctx) throw new Error(`createUniqueId cannot be used under non-hydrating context`); return `${ctx.id}${ctx.count++}`; } export function createComponent(Comp: (props: T) => JSX.Element, props: T): JSX.Element { if (sharedConfig.context && !sharedConfig.context.noHydrate) { const c = sharedConfig.context; setHydrateContext(nextHydrateContext()); const r = Comp(props || ({} as T)); setHydrateContext(c); return r; } return Comp(props || ({} as T)); } export function mergeProps(source: T, source1: U): T & U; export function mergeProps(source: T, source1: U, source2: V): T & U & V; export function mergeProps( source: T, source1: U, source2: V, source3: W ): T & U & V & W; export function mergeProps(...sources: any): any { const target = {}; for (let i = 0; i < sources.length; i++) { let source = sources[i]; if (typeof source === "function") source = source(); if (source) Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } return target; } export function splitProps( props: T, ...keys: [K1[]] ): [Pick, Omit]; export function splitProps( props: T, ...keys: [K1[], K2[]] ): [Pick, Pick, Omit]; export function splitProps< T extends object, K1 extends keyof T, K2 extends keyof T, K3 extends keyof T >( props: T, ...keys: [K1[], K2[], K3[]] ): [Pick, Pick, Pick, Omit]; export function splitProps< T extends object, K1 extends keyof T, K2 extends keyof T, K3 extends keyof T, K4 extends keyof T >( props: T, ...keys: [K1[], K2[], K3[], K4[]] ): [Pick, Pick, Pick, Pick, Omit]; export function splitProps< T extends object, K1 extends keyof T, K2 extends keyof T, K3 extends keyof T, K4 extends keyof T, K5 extends keyof T >( props: T, ...keys: [K1[], K2[], K3[], K4[], K5[]] ): [ Pick, Pick, Pick, Pick, Pick, Omit ]; export function splitProps(props: T, ...keys: [(keyof T)[]]) { const descriptors = Object.getOwnPropertyDescriptors(props), split = (k: (keyof T)[]) => { const clone: Partial = {}; for (let i = 0; i < k.length; i++) { const key = k[i]; if (descriptors[key]) { Object.defineProperty(clone, key, descriptors[key]); delete descriptors[key]; } } return clone; }; return keys.map(split).concat(split(Object.keys(descriptors) as (keyof T)[])); } function simpleMap( props: { each: any[]; children: Function; fallback?: string }, wrap: (fn: Function, item: any, i: number) => string ) { const list = props.each || [], len = list.length, fn = props.children; if (len) { let mapped = Array(len); for (let i = 0; i < len; i++) mapped[i] = wrap(fn, list[i], i); return mapped; } return props.fallback; } export function For(props: { each: T[]; fallback?: string; children: (item: T, index: () => number) => string; }) { return simpleMap(props, (fn, item, i) => fn(item, () => i)); } // non-keyed export function Index(props: { each: T[]; fallback?: string; children: (item: () => T, index: number) => string; }) { return simpleMap(props, (fn, item, i) => fn(() => item, i)); } export function Show(props: { when: T | undefined | null | false; keyed?: boolean; fallback?: string; children: string | ((item: NonNullable) => string); }) { let c: string | ((item: NonNullable) => string); return props.when ? typeof (c = props.children) === "function" ? c(props.when!) : c : props.fallback || ""; } export function Switch(props: { fallback?: string; children: MatchProps | MatchProps[]; }) { let conditions = props.children; Array.isArray(conditions) || (conditions = [conditions]); for (let i = 0; i < conditions.length; i++) { const w = conditions[i].when; if (w) { const c = conditions[i].children; return typeof c === "function" ? c(w) : c; } } return props.fallback || ""; } type MatchProps = { when: T | false; keyed?: boolean; children: string | ((item: T) => string); }; export function Match(props: MatchProps) { return props; } export function resetErrorBoundaries() {} export function ErrorBoundary(props: { fallback: string | ((err: any, reset: () => void) => string); children: string; }) { let error: any, res: any, clean: any, sync = true; const ctx = sharedConfig.context!; const id = ctx.id + ctx.count; function displayFallback() { cleanNode(clean); ctx.writeResource(id, error, true); setHydrateContext({ ...ctx, count: 0 }); const f = props.fallback; return typeof f === "function" && f.length ? f(error, () => {}) : f; } onError(err => { error = err; !sync && ctx.replace("e" + id, displayFallback); sync = true; }); onCleanup(() => cleanNode(clean)); createMemo(() => { Owner!.context = { [BRANCH]: (clean = {}) }; return (res = props.children); }); if (error) return displayFallback(); sync = false; return { t: `${resolveSSRNode(res)}` }; } // Suspense Context export interface Resource { (): T | undefined; state: "unresolved" | "pending" | "ready" | "refreshing" | "errored"; loading: boolean; error: any; latest: T | undefined; } type SuspenseContextType = { resources: Map; completed: () => void; }; export type ResourceActions = { mutate: Setter; refetch: (info?: unknown) => void }; export type ResourceReturn = [Resource, ResourceActions]; export type ResourceSource = S | false | null | undefined | (() => S | false | null | undefined); export type ResourceFetcher = (k: S, info: ResourceFetcherInfo) => T | Promise; export type ResourceFetcherInfo = { value: T | undefined; refetching?: unknown }; export type ResourceOptions = undefined extends T ? { initialValue?: T; name?: string; deferStream?: boolean; ssrLoadFrom?: "initial" | "server"; storage?: () => Signal; onHydrated?: (k: S, info: ResourceFetcherInfo) => void; } : { initialValue: T; name?: string; deferStream?: boolean; ssrLoadFrom?: "initial" | "server"; storage?: (v?: T) => Signal; onHydrated?: (k: S, info: ResourceFetcherInfo) => void; }; const SuspenseContext = createContext(); let resourceContext: any[] | null = null; export function createResource( fetcher: ResourceFetcher, options?: ResourceOptions ): ResourceReturn; export function createResource( fetcher: ResourceFetcher, options: ResourceOptions ): ResourceReturn; export function createResource( source: ResourceSource, fetcher: ResourceFetcher, options?: ResourceOptions ): ResourceReturn; export function createResource( source: ResourceSource, fetcher: ResourceFetcher, options: ResourceOptions ): ResourceReturn; export function createResource( source: ResourceSource | ResourceFetcher, fetcher?: ResourceFetcher | ResourceOptions | ResourceOptions, options: ResourceOptions | ResourceOptions = {} ): ResourceReturn | ResourceReturn { if (arguments.length === 2) { if (typeof fetcher === "object") { options = fetcher as ResourceOptions | ResourceOptions; fetcher = source as ResourceFetcher; source = true as ResourceSource; } } else if (arguments.length === 1) { fetcher = source as ResourceFetcher; source = true as ResourceSource; } const contexts = new Set(); const id = sharedConfig.context!.id + sharedConfig.context!.count++; let resource: { ref?: any; data?: T } = {}; let value = options.storage ? options.storage(options.initialValue)[0]() : options.initialValue; let p: Promise | T | null; let error: any; if (sharedConfig.context!.async && options.ssrLoadFrom !== "initial") { resource = sharedConfig.context!.resources[id] || (sharedConfig.context!.resources[id] = {}); if (resource.ref) { if (!resource.data && !resource.ref[0].loading && !resource.ref[0].error) resource.ref[1].refetch(); return resource.ref; } } const read = () => { if (error) throw error; if (resourceContext && p) resourceContext.push(p!); const resolved = options.ssrLoadFrom !== "initial" && sharedConfig.context!.async && "data" in sharedConfig.context!.resources[id]; if (!resolved && read.loading) { const ctx = useContext(SuspenseContext); if (ctx) { ctx.resources.set(id, read); contexts.add(ctx); } } return resolved ? sharedConfig.context!.resources[id].data : value; }; read.loading = false; read.error = undefined as any; read.state = "initialValue" in options ? "resolved" : "unresolved"; Object.defineProperty(read, "latest", { get() { return read(); } }); function load() { const ctx = sharedConfig.context!; if (!ctx.async) return (read.loading = !!(typeof source === "function" ? (source as () => S)() : source)); if (ctx.resources && id in ctx.resources && "data" in ctx.resources[id]) { value = ctx.resources[id].data; return; } resourceContext = []; const lookup = typeof source === "function" ? (source as () => S)() : source; if (resourceContext.length) { p = Promise.all(resourceContext).then(() => (fetcher as ResourceFetcher)((source as Accessor)(), { value }) ); } resourceContext = null; if (!p) { if (lookup == null || lookup === false) return; p = (fetcher as ResourceFetcher)(lookup, { value }); } if (p != undefined && typeof p === "object" && "then" in p) { read.loading = true; read.state = "pending"; if (ctx.writeResource) ctx.writeResource(id, p, undefined, options.deferStream); return p .then(res => { read.loading = false; read.state = "resolved"; ctx.resources[id].data = res; p = null; notifySuspense(contexts); return res; }) .catch(err => { read.loading = false; read.state = "errored"; read.error = error = castError(err); p = null; notifySuspense(contexts); }); } ctx.resources[id].data = p; if (ctx.writeResource) ctx.writeResource(id, p); p = null; return ctx.resources[id].data; } if (options.ssrLoadFrom !== "initial") load(); return (resource.ref = [ read, { refetch: load, mutate: (v: T) => (value = v) } ] as ResourceReturn); } export function lazy>( fn: () => Promise<{ default: T }> ): T & { preload: () => Promise<{ default: T }> } { let resolved: T; let p: Promise<{ default: T }>; let load = () => { if (!p) { p = fn(); p.then(mod => (resolved = mod.default)); } return p; }; const contexts = new Set(); const wrap: Component> & { preload?: () => Promise<{ default: T }>; } = props => { load(); const id = sharedConfig.context!.id.slice(0, -1); if (resolved) return resolved(props); const ctx = useContext(SuspenseContext); const track = { loading: true, error: undefined }; if (ctx) { ctx.resources.set(id, track); contexts.add(ctx); } if (sharedConfig.context!.async) { sharedConfig.context!.block( p.then(() => { track.loading = false; notifySuspense(contexts); }) ); } return ""; }; wrap.preload = load; return wrap as T & { preload: () => Promise<{ default: T }> }; } function suspenseComplete(c: SuspenseContextType) { for (const r of c.resources.values()) { if (r.loading) return false; } return true; } function notifySuspense(contexts: Set) { for (const c of contexts) { if (suspenseComplete(c)) c.completed(); } contexts.clear(); } export function enableScheduling() {} export function enableHydration() {} export function startTransition(fn: () => any): void { fn(); } export function useTransition(): [() => boolean, (fn: () => any) => void] { return [ () => false, fn => { fn(); } ]; } type HydrationContext = { id: string; count: number; writeResource: ( id: string, v: Promise | any, error?: boolean, deferStream?: boolean ) => void; replace: (id: string, replacement: () => any) => void; block: (p: Promise) => void; resources: Record; suspense: Record; registerFragment: (v: string) => (v?: string, err?: any) => boolean; async?: boolean; noHydrate: boolean; }; export function SuspenseList(props: { children: string; revealOrder: "forwards" | "backwards" | "together"; tail?: "collapsed" | "hidden"; }) { // TODO: support tail options return props.children; } export function Suspense(props: { fallback?: string; children: string }) { let done: undefined | ((html?: string, error?: any) => boolean); let clean: any; const ctx = sharedConfig.context!; const id = ctx.id + ctx.count; const o = Owner; if (o) { if (o.context) o.context[BRANCH] = clean = {}; else o.context = { [BRANCH]: (clean = {}) }; } const value: SuspenseContextType = ctx.suspense[id] || (ctx.suspense[id] = { resources: new Map(), completed: () => { const res = runSuspense(); if (suspenseComplete(value)) { done!(resolveSSRNode(res)); } } }); function runSuspense() { setHydrateContext({ ...ctx, count: 0 }); return runWithOwner(o!, () => { return createComponent(SuspenseContext.Provider, { value, get children() { clean && cleanNode(clean); return props.children; } }); }); } const res = runSuspense(); // never suspended if (suspenseComplete(value)) return res; onError(err => { if (!done || !done(undefined, err)) { if (o) runWithOwner(o.owner!, () => { throw err; }); else throw err; } }); done = ctx.async ? ctx.registerFragment(id) : undefined; if (ctx.async) { setHydrateContext({ ...ctx, count: 0, id: ctx.id + "0.f", noHydrate: true }); const res = { t: `${resolveSSRNode(props.fallback)}` }; setHydrateContext(ctx); return res; } setHydrateContext({ ...ctx, count: 0, id: ctx.id + "0.f" }); ctx.writeResource(id, "$$f"); return props.fallback; }