// June's JSX runtime — the transform-free island primitive. // // Set `jsxImportSource: "@junejs/core"` and the standard JSX compile routes every // element through this `jsx()` (NOT an AST transform — it's the function the // compiler already calls). When a COMPONENT is used with a `client:*` directive // (``), we emit the `` hydration marker // around its SSR — so a PLAIN "use client" component becomes an island with no // `island()` wrapper. Everything else (host elements, components without a // directive) passes straight through to React. // // The marker is byte-identical to the island() one (same attrs), so the client // runtime, morph, and persist all keep working unchanged. import { jsx as rjsx, jsxs as rjsxs, Fragment } from "react/jsx-runtime"; import { ISLAND_TAG, ISLAND_NAME_ATTR, ISLAND_PROPS_ATTR, ISLAND_STRATEGY_ATTR, ISLAND_PERSIST_ATTR, ISLAND_SLOT_ATTR, JuneSlot, serializeIslandProps, type Strategy, } from "./islands"; export { Fragment }; // The JSX namespace (the type side jsxImportSource resolves) IS React's — re-exported. export type { JSX } from "react/jsx-runtime"; // …augmented with the `client:*` hydration directives, so `` // type-checks on any component and a typo (`client:bogus`) is a compile error. This // merges into React's JSX.IntrinsicAttributes (loaded with this module). declare module "react" { namespace JSX { interface IntrinsicAttributes { "client:load"?: boolean; "client:idle"?: boolean; "client:visible"?: boolean; "client:only"?: boolean; // Carry the island's live node across a soft navigation (only on an island, // i.e. alongside a client:* directive). persist?: boolean; } } } const DIRECTIVE = "client:"; // If `type` is a component used with a truthy `client:` directive, build // its island marker; otherwise return null (the caller passes through to React). export function islandMarker(type: unknown, props: Record | null): unknown { if (typeof type !== "function" || props == null) return null; // Fast path: a component is an island ONLY if some prop is a `client:*` directive. // The common case is a normal component render — bail here, before allocating // `rest`, so the whole tree doesn't pay an O(props) copy + alloc per element. let isIsland = false; for (const k in props) { if (k.charCodeAt(0) === 99 /* 'c' */ && k.startsWith(DIRECTIVE)) { isIsland = true; break; } } if (!isIsland) return null; let strategy: Strategy | undefined; let persist = false; const rest: Record = {}; for (const k in props) { if (k.startsWith(DIRECTIVE)) { if (props[k]) strategy = k.slice(DIRECTIVE.length) as Strategy; } else if (k === "persist") { persist = Boolean(props[k]); } else { rest[k] = props[k]; } } if (!strategy) return null; // only `client:false` directives → not an island const name = (type as { displayName?: string; name?: string }).displayName || (type as { name?: string }).name; // Slot island: a component used WITH children is an interactive shell wrapping // server-rendered content. Wrap the children in (server: renders them // as zero-JS HTML inside ; client: re-renders the captured HTML // opaquely so it hydrates 1:1 and never reconciles). The component just renders // `{children}`. A leaf island taking no children is the common case (no slot). const { children, ...serializable } = rest; // A slot island is one given real children. Whitespace-only string children (a // stray space/newline) don't count — a leaf island isn't a slot by accident. const hasSlot = children != null && children !== false && !(typeof children === "string" && children.trim() === ""); if (hasSlot && strategy === "only") { throw new Error( `[june] island <${name} client:only/> cannot take children — client:only is never server-rendered, so there is no server content to slot.`, ); } const componentProps = hasSlot ? { ...serializable, children: rjsx(JuneSlot as never, { children } as never) } : serializable; return rjsx(ISLAND_TAG as never, { [ISLAND_NAME_ATTR]: name, [ISLAND_STRATEGY_ATTR]: strategy, [ISLAND_PROPS_ATTR]: serializeIslandProps(serializable), // children never serialize; they SSR via the slot ...(persist ? { [ISLAND_PERSIST_ATTR]: "" } : {}), ...(hasSlot ? { [ISLAND_SLOT_ATTR]: "" } : {}), // "only" → never server-render (client mounts fresh); else SSR the component. children: strategy === "only" ? undefined : rjsx(type as never, componentProps as never), } as never); } export function jsx(type: unknown, props: Record | null, key?: unknown): unknown { return islandMarker(type, props) ?? rjsx(type as never, props as never, key as never); } export function jsxs(type: unknown, props: Record | null, key?: unknown): unknown { return islandMarker(type, props) ?? rjsxs(type as never, props as never, key as never); }