ripple
Version:
Ripple is an elegant TypeScript UI framework
292 lines (251 loc) • 8.75 kB
JavaScript
/** @import { AppendIntoAnchor, RootBoundaryOptions } from '#client' */
import { destroy_block, root } from './internal/client/blocks.js';
import { handle_root_events, release_root_events } from './internal/client/events.js';
import { init_operations } from './internal/client/operations.js';
import { render_component } from './internal/client/component.js';
import { try_block } from './internal/client/try.js';
import { remove_styles } from './internal/client/css.js';
import { install_hydration } from './internal/client/hydrate.js';
import { hydration_disabled, root_boundary_disabled } from './internal/client/errors.js';
import { ROOT_BOUNDARY } from 'ripple/internal/client/root-boundary-enabled';
import { normalize_children } from './element.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydration,
track_hash_reference,
} from './internal/client/hydration.js';
import { COMMENT_NODE, HYDRATION_START } from '../constants.js';
// Re-export JSX runtime functions for jsxImportSource: "ripple"
export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
export { set_transport as setTransport } from './internal/client/transport.js';
export {
UNINITIALIZED,
TRACKED_UPDATED,
SUSPENSE_PENDING,
SUSPENSE_REJECTED,
} from './internal/client/constants.js';
/**
* @param {Node | AppendIntoAnchor} anchor
* @param {(anchor: Node) => void} render_content
* @param {RootBoundaryOptions | undefined} boundary
* @returns {void}
*/
function render_root_boundary(anchor, render_content, boundary) {
const Pending = boundary?.pending;
const Catch = boundary?.catch;
try_block(
anchor,
(component_anchor) => {
render_content(component_anchor);
},
Catch
? (catch_anchor, error, reset) => {
render_component(Catch, catch_anchor, { error, reset: reset ?? (() => {}) });
}
: null,
(pending_anchor) => {
if (Pending) {
render_component(Pending, pending_anchor, {});
}
},
true,
);
}
/**
* Apply the same children normalization that compiled component call sites
* get, so `mount`/`hydrate` accept a component function as the `children`
* prop.
*
* @param {Record<string, any>} props
* @returns {Record<string, any>}
*/
function normalize_props(props) {
return { ...props, children: normalize_children(props.children) };
}
/**
* `rootBoundary` configures the default `try`/`pending`/`catch` boundary the
* app is rendered under; `false` renders without one. Without a root
* boundary, `trackAsync()` must sit inside a user `@try` block, and errors
* that escape one propagate out of the flush.
* @param {Function} component
* @param {{ props?: Record<string, any>, target: HTMLElement, rootBoundary?: RootBoundaryOptions | false }} options
* @returns {() => void}
*/
export function mount(component, options) {
init_operations();
// Only a page with server-rendered inline styles has anything to remove.
// Requesting a frame otherwise makes the browser attach layout to the
// mounted tree before the app's next update, which slows a teardown that
// would have preceded the first paint.
if (HYDRATION) {
if (document.querySelector('style[data-ripple-ssr]') !== null) {
requestAnimationFrame(remove_styles);
}
}
let props = options.props ?? {};
if (props.children != null) {
props = normalize_props(props);
}
const target = options.target;
// Clear target content in case of SSR
if (target.firstChild) {
target.textContent = '';
}
// The app appends into the emptied target; a root that keeps inserting
// relative to its anchor materializes one (see `resolve_anchor`).
/** @type {AppendIntoAnchor} */
const anchor = { parent: target, into: true, tail: false };
/** @type {import('./internal/client/events.js').RootTargetRef | null} */
let events_ref = handle_root_events(target);
const root_boundary = options.rootBoundary;
if (!ROOT_BOUNDARY && root_boundary != null && root_boundary !== false) {
root_boundary_disabled();
}
const _root = root(() => {
if (!ROOT_BOUNDARY || root_boundary === false) {
render_component(component, anchor, props);
return;
}
render_root_boundary(
anchor,
(component_anchor) => {
render_component(component, component_anchor, props);
},
root_boundary,
);
});
return () => {
// The disposer may be called again (a remount, HMR, a stale reference);
// only the first call owns the target's delegated-listener ref.
if (events_ref === null) return;
release_root_events(events_ref);
events_ref = null;
destroy_block(_root);
};
}
/**
* `rootBoundary` works as in {@link mount}: the app hydrates under a default
* `try` boundary, or with `false` directly under the root block. Server output
* always carries the root boundary markers; without a boundary, `hydrate()`
* steps over a plain `<!--[-->` itself. A streamed shell whose root suspended
* starts with a `<!--[?N-->` / `<!--[!N-->` slot instead, and only a boundary
* can adopt its fallback and activate the chunk, so that shell hydrates under
* the default boundary regardless.
* @param {Function} component
* @param {{ props?: Record<string, any>, target: HTMLElement, rootBoundary?: RootBoundaryOptions | false }} options
* @returns {() => void}
*/
export function hydrate(component, options) {
if (!HYDRATION) {
hydration_disabled();
}
init_operations();
install_hydration();
requestAnimationFrame(remove_styles);
let props = options.props ?? {};
if (props.children != null) {
props = normalize_props(props);
}
const target = options.target;
const was_hydrating = HYDRATION && hydrating;
const previous_hydrate_node = hydrate_node;
let anchor = target.firstChild;
/** @type {import('./internal/client/events.js').RootTargetRef | null} */
let events_ref = handle_root_events(target);
let _root;
try {
while (
anchor &&
(anchor.nodeType !== COMMENT_NODE ||
// any `[`-prefixed marker anchors the root boundary — a streamed
// shell whose root suspended starts with a `<!--[?N-->` slot
// marker instead of a plain `<!--[-->`
!(/** @type {Comment} */ (anchor).data.startsWith(HYDRATION_START)))
) {
anchor = anchor.nextSibling;
}
set_hydration(true, /** @type {Comment} */ (anchor));
const root_boundary = options.rootBoundary;
const marker = /** @type {Comment} */ (anchor);
if (!ROOT_BOUNDARY && root_boundary != null && root_boundary !== false) {
root_boundary_disabled();
}
_root = root(() => {
if ((!ROOT_BOUNDARY || root_boundary === false) && marker.data === HYDRATION_START) {
// The root boundary's own hydration walk: consume the `<!--[-->`
// marker and render against it, as `try_block` does for the root.
hydrate_next();
render_component(component, marker, props);
return;
}
render_root_boundary(
marker,
(component_anchor) => {
render_component(component, component_anchor, props);
},
root_boundary === false ? undefined : root_boundary,
);
});
} catch (e) {
throw e;
} finally {
set_hydration(was_hydrating, previous_hydrate_node);
if (!was_hydrating) {
track_hash_reference.clear();
}
}
return () => {
if (events_ref === null) return;
release_root_events(events_ref);
events_ref = null;
destroy_block(_root);
};
}
export { Context } from './internal/client/context.js';
export {
flush_sync as flushSync,
track,
track_read_only as trackReadOnly,
untrack,
tick,
is_tracked_pending as trackPending,
peek_tracked as peek,
} from './internal/client/runtime.js';
export { track_async as trackAsync } from './internal/client/track-async.js';
export { snapshot } from './proxy.js';
export { RippleArray } from './array.js';
export { RippleObject } from './object.js';
export { RippleSet } from './set.js';
export { RippleMap } from './map.js';
export { RippleDate } from './date.js';
export { RippleURL } from './url.js';
export { RippleURLSearchParams } from './url-search-params.js';
export { createSubscriber } from './create-subscriber.js';
export { MediaQuery } from './media-query.js';
export { user_effect as effect } from './internal/client/blocks.js';
export { Portal } from './internal/client/portal.js';
export { ref_prop as createRefKey } from './internal/client/runtime.js';
export { isRefProp } from '@tsrx/core/runtime/ref';
export { on } from './internal/client/events.js';
export {
bindValue,
bindChecked,
bindGroup,
bindClientWidth,
bindClientHeight,
bindContentRect,
bindContentBoxSize,
bindBorderBoxSize,
bindDevicePixelContentBoxSize,
bindFiles,
bindIndeterminate,
bindInnerHTML,
bindInnerText,
bindTextContent,
bindNode,
bindOffsetWidth,
bindOffsetHeight,
} from './internal/client/bindings.js';
import { HYDRATION } from 'ripple/internal/client/hydration-enabled';