diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:46:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:48:30 -0300 |
commit | 38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch) | |
tree | 453dbf70000cc5e338b06201af1eaca8343f8f73 /preact/compat/src | |
parent | f26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff) | |
download | node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.gz node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.tar.bz2 node-vendor-38acabfa6089ab8ac469c12b5f55022fb96935e5.zip |
Diffstat (limited to 'preact/compat/src')
-rw-r--r-- | preact/compat/src/Children.js | 21 | ||||
-rw-r--r-- | preact/compat/src/PureComponent.js | 15 | ||||
-rw-r--r-- | preact/compat/src/forwardRef.js | 51 | ||||
-rw-r--r-- | preact/compat/src/index.d.ts | 140 | ||||
-rw-r--r-- | preact/compat/src/index.js | 187 | ||||
-rw-r--r-- | preact/compat/src/internal.d.ts | 47 | ||||
-rw-r--r-- | preact/compat/src/memo.js | 34 | ||||
-rw-r--r-- | preact/compat/src/portals.js | 80 | ||||
-rw-r--r-- | preact/compat/src/render.js | 219 | ||||
-rw-r--r-- | preact/compat/src/suspense-list.d.ts | 14 | ||||
-rw-r--r-- | preact/compat/src/suspense-list.js | 126 | ||||
-rw-r--r-- | preact/compat/src/suspense.d.ts | 15 | ||||
-rw-r--r-- | preact/compat/src/suspense.js | 270 | ||||
-rw-r--r-- | preact/compat/src/util.js | 28 |
14 files changed, 1247 insertions, 0 deletions
diff --git a/preact/compat/src/Children.js b/preact/compat/src/Children.js new file mode 100644 index 0000000..0295d93 --- /dev/null +++ b/preact/compat/src/Children.js @@ -0,0 +1,21 @@ +import { toChildArray } from 'preact'; + +const mapFn = (children, fn) => { + if (children == null) return null; + return toChildArray(toChildArray(children).map(fn)); +}; + +// This API is completely unnecessary for Preact, so it's basically passthrough. +export const Children = { + map: mapFn, + forEach: mapFn, + count(children) { + return children ? toChildArray(children).length : 0; + }, + only(children) { + const normalized = toChildArray(children); + if (normalized.length !== 1) throw 'Children.only'; + return normalized[0]; + }, + toArray: toChildArray +}; diff --git a/preact/compat/src/PureComponent.js b/preact/compat/src/PureComponent.js new file mode 100644 index 0000000..6396ce4 --- /dev/null +++ b/preact/compat/src/PureComponent.js @@ -0,0 +1,15 @@ +import { Component } from 'preact'; +import { shallowDiffers } from './util'; + +/** + * Component class with a predefined `shouldComponentUpdate` implementation + */ +export function PureComponent(p) { + this.props = p; +} +PureComponent.prototype = new Component(); +// Some third-party libraries check if this property is present +PureComponent.prototype.isPureReactComponent = true; +PureComponent.prototype.shouldComponentUpdate = function(props, state) { + return shallowDiffers(this.props, props) || shallowDiffers(this.state, state); +}; diff --git a/preact/compat/src/forwardRef.js b/preact/compat/src/forwardRef.js new file mode 100644 index 0000000..39585cb --- /dev/null +++ b/preact/compat/src/forwardRef.js @@ -0,0 +1,51 @@ +import { options } from 'preact'; +import { assign } from './util'; + +let oldDiffHook = options._diff; +options._diff = vnode => { + if (vnode.type && vnode.type._forwarded && vnode.ref) { + vnode.props.ref = vnode.ref; + vnode.ref = null; + } + if (oldDiffHook) oldDiffHook(vnode); +}; + +export const REACT_FORWARD_SYMBOL = + (typeof Symbol != 'undefined' && + Symbol.for && + Symbol.for('react.forward_ref')) || + 0xf47; + +/** + * Pass ref down to a child. This is mainly used in libraries with HOCs that + * wrap components. Using `forwardRef` there is an easy way to get a reference + * of the wrapped component instead of one of the wrapper itself. + * @param {import('./index').ForwardFn} fn + * @returns {import('./internal').FunctionComponent} + */ +export function forwardRef(fn) { + // We always have ref in props.ref, except for + // mobx-react. It will call this function directly + // and always pass ref as the second argument. + function Forwarded(props, ref) { + let clone = assign({}, props); + delete clone.ref; + ref = props.ref || ref; + return fn( + clone, + !ref || (typeof ref === 'object' && !('current' in ref)) ? null : ref + ); + } + + // mobx-react checks for this being present + Forwarded.$$typeof = REACT_FORWARD_SYMBOL; + // mobx-react heavily relies on implementation details. + // It expects an object here with a `render` property, + // and prototype.render will fail. Without this + // mobx-react throws. + Forwarded.render = Forwarded; + + Forwarded.prototype.isReactComponent = Forwarded._forwarded = true; + Forwarded.displayName = 'ForwardRef(' + (fn.displayName || fn.name) + ')'; + return Forwarded; +} diff --git a/preact/compat/src/index.d.ts b/preact/compat/src/index.d.ts new file mode 100644 index 0000000..82c1a4f --- /dev/null +++ b/preact/compat/src/index.d.ts @@ -0,0 +1,140 @@ +import * as _hooks from '../../hooks'; +import * as preact from '../../src'; +import { JSXInternal } from '../../src/jsx'; +import * as _Suspense from './suspense'; +import * as _SuspenseList from './suspense-list'; + +// export default React; +export = React; +export as namespace React; +declare namespace React { + // Export JSX + export import JSX = JSXInternal; + + // Hooks + export import CreateHandle = _hooks.CreateHandle; + export import EffectCallback = _hooks.EffectCallback; + export import Inputs = _hooks.Inputs; + export import PropRef = _hooks.PropRef; + export import Reducer = _hooks.Reducer; + export import Ref = _hooks.Ref; + export import StateUpdater = _hooks.StateUpdater; + export import useCallback = _hooks.useCallback; + export import useContext = _hooks.useContext; + export import useDebugValue = _hooks.useDebugValue; + export import useEffect = _hooks.useEffect; + export import useImperativeHandle = _hooks.useImperativeHandle; + export import useLayoutEffect = _hooks.useLayoutEffect; + export import useMemo = _hooks.useMemo; + export import useReducer = _hooks.useReducer; + export import useRef = _hooks.useRef; + export import useState = _hooks.useState; + + // Preact Defaults + export import Component = preact.Component; + export import FunctionComponent = preact.FunctionComponent; + export import FC = preact.FunctionComponent; + export import createContext = preact.createContext; + export import createRef = preact.createRef; + export import Fragment = preact.Fragment; + export import createElement = preact.createElement; + export import cloneElement = preact.cloneElement; + + // Suspense + export import Suspense = _Suspense.Suspense; + export import lazy = _Suspense.lazy; + export import SuspenseList = _SuspenseList.SuspenseList; + + // Compat + export import StrictMode = preact.Fragment; + export const version: string; + + export function createPortal( + vnode: preact.VNode, + container: Element + ): preact.VNode<any>; + + export function render( + vnode: preact.VNode<any>, + parent: Element, + callback?: () => void + ): Component | null; + + export function hydrate( + vnode: preact.VNode<any>, + parent: Element, + callback?: () => void + ): Component | null; + + export function unmountComponentAtNode( + container: Element | Document | ShadowRoot | DocumentFragment + ): boolean; + + export function createFactory( + type: preact.VNode<any>['type'] + ): ( + props?: any, + ...children: preact.ComponentChildren[] + ) => preact.VNode<any>; + export function isValidElement(element: any): boolean; + export function findDOMNode(component: preact.Component): Element | null; + + export abstract class PureComponent<P = {}, S = {}> extends preact.Component< + P, + S + > { + isPureReactComponent: boolean; + } + + export function memo<P = {}>( + component: preact.FunctionalComponent<P>, + comparer?: (prev: P, next: P) => boolean + ): preact.FunctionComponent<P>; + export function memo<C extends preact.FunctionalComponent<any>>( + component: C, + comparer?: ( + prev: preact.ComponentProps<C>, + next: preact.ComponentProps<C> + ) => boolean + ): C; + + export interface ForwardFn<P = {}, T = any> { + (props: P, ref: Ref<T>): preact.ComponentChild; + displayName?: string; + } + + export function forwardRef<R, P = {}>( + fn: ForwardFn<P, R> + ): preact.FunctionalComponent<Omit<P, 'ref'> & { ref?: preact.RefObject<R> }>; + + export function unstable_batchedUpdates( + callback: (arg?: any) => void, + arg?: any + ): void; + + export const Children: { + map<T extends preact.ComponentChild, R>( + children: T | T[], + fn: (child: T, i: number) => R + ): R[]; + forEach<T extends preact.ComponentChild>( + children: T | T[], + fn: (child: T, i: number) => void + ): void; + count: (children: preact.ComponentChildren) => number; + only: (children: preact.ComponentChildren) => preact.ComponentChild; + toArray: (children: preact.ComponentChildren) => preact.VNode<{}>[]; + }; + + // scheduler + export const unstable_ImmediatePriority: number; + export const unstable_UserBlockingPriority: number; + export const unstable_NormalPriority: number; + export const unstable_LowPriority: number; + export const unstable_IdlePriority: number; + export function unstable_runWithPriority( + priority: number, + callback: () => void + ): void; + export const unstable_now: () => number; +} diff --git a/preact/compat/src/index.js b/preact/compat/src/index.js new file mode 100644 index 0000000..c8f9a6c --- /dev/null +++ b/preact/compat/src/index.js @@ -0,0 +1,187 @@ +import { + createElement, + render as preactRender, + cloneElement as preactCloneElement, + createRef, + Component, + createContext, + Fragment +} from 'preact'; +import { + useState, + useReducer, + useEffect, + useLayoutEffect, + useRef, + useImperativeHandle, + useMemo, + useCallback, + useContext, + useDebugValue +} from 'preact/hooks'; +import { PureComponent } from './PureComponent'; +import { memo } from './memo'; +import { forwardRef } from './forwardRef'; +import { Children } from './Children'; +import { Suspense, lazy } from './suspense'; +import { SuspenseList } from './suspense-list'; +import { createPortal } from './portals'; +import { + hydrate, + render, + REACT_ELEMENT_TYPE, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED +} from './render'; + +const version = '17.0.2'; // trick libraries to think we are react + +/** + * Legacy version of createElement. + * @param {import('./internal').VNode["type"]} type The node name or Component constructor + */ +function createFactory(type) { + return createElement.bind(null, type); +} + +/** + * Check if the passed element is a valid (p)react node. + * @param {*} element The element to check + * @returns {boolean} + */ +function isValidElement(element) { + return !!element && element.$$typeof === REACT_ELEMENT_TYPE; +} + +/** + * Wrap `cloneElement` to abort if the passed element is not a valid element and apply + * all vnode normalizations. + * @param {import('./internal').VNode} element The vnode to clone + * @param {object} props Props to add when cloning + * @param {Array<import('./internal').ComponentChildren>} rest Optional component children + */ +function cloneElement(element) { + if (!isValidElement(element)) return element; + return preactCloneElement.apply(null, arguments); +} + +/** + * Remove a component tree from the DOM, including state and event handlers. + * @param {import('./internal').PreactElement} container + * @returns {boolean} + */ +function unmountComponentAtNode(container) { + if (container._children) { + preactRender(null, container); + return true; + } + return false; +} + +/** + * Get the matching DOM node for a component + * @param {import('./internal').Component} component + * @returns {import('./internal').PreactElement | null} + */ +function findDOMNode(component) { + return ( + (component && + (component.base || (component.nodeType === 1 && component))) || + null + ); +} + +/** + * Deprecated way to control batched rendering inside the reconciler, but we + * already schedule in batches inside our rendering code + * @template Arg + * @param {(arg: Arg) => void} callback function that triggers the updated + * @param {Arg} [arg] Optional argument that can be passed to the callback + */ +// eslint-disable-next-line camelcase +const unstable_batchedUpdates = (callback, arg) => callback(arg); + +/** + * In React, `flushSync` flushes the entire tree and forces a rerender. It's + * implmented here as a no-op. + * @template Arg + * @template Result + * @param {(arg: Arg) => Result} callback function that runs before the flush + * @param {Arg} [arg] Optional arugment that can be passed to the callback + * @returns + */ +const flushSync = (callback, arg) => callback(arg); + +/** + * Strict Mode is not implemented in Preact, so we provide a stand-in for it + * that just renders its children without imposing any restrictions. + */ +const StrictMode = Fragment; + +export * from 'preact/hooks'; +export { + version, + Children, + render, + hydrate, + unmountComponentAtNode, + createPortal, + createElement, + createContext, + createFactory, + cloneElement, + createRef, + Fragment, + isValidElement, + findDOMNode, + Component, + PureComponent, + memo, + forwardRef, + flushSync, + // eslint-disable-next-line camelcase + unstable_batchedUpdates, + StrictMode, + Suspense, + SuspenseList, + lazy, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED +}; + +// React copies the named exports to the default one. +export default { + useState, + useReducer, + useEffect, + useLayoutEffect, + useRef, + useImperativeHandle, + useMemo, + useCallback, + useContext, + useDebugValue, + version, + Children, + render, + hydrate, + unmountComponentAtNode, + createPortal, + createElement, + createContext, + createFactory, + cloneElement, + createRef, + Fragment, + isValidElement, + findDOMNode, + Component, + PureComponent, + memo, + forwardRef, + flushSync, + unstable_batchedUpdates, + StrictMode, + Suspense, + SuspenseList, + lazy, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED +}; diff --git a/preact/compat/src/internal.d.ts b/preact/compat/src/internal.d.ts new file mode 100644 index 0000000..cb68ffa --- /dev/null +++ b/preact/compat/src/internal.d.ts @@ -0,0 +1,47 @@ +import { + Component as PreactComponent, + VNode as PreactVNode, + FunctionComponent as PreactFunctionComponent +} from '../../src/internal'; +import { SuspenseProps } from './suspense'; + +export { ComponentChildren } from '../..'; + +export { PreactElement } from '../../src/internal'; + +export interface Component<P = {}, S = {}> extends PreactComponent<P, S> { + isReactComponent?: object; + isPureReactComponent?: true; + _patchedLifecycles?: true; + + // Suspense internal properties + _childDidSuspend?(error: Promise<void>, suspendingVNode: VNode): void; + _suspended: (vnode: VNode) => (unsuspend: () => void) => void; + _onResolve?(): void; + + // Portal internal properties + _temp: any; + _container: PreactElement; +} + +export interface FunctionComponent<P = {}> extends PreactFunctionComponent<P> { + shouldComponentUpdate?(nextProps: Readonly<P>): boolean; + _forwarded?: boolean; + _patchedLifecycles?: true; +} + +export interface VNode<T = any> extends PreactVNode<T> { + $$typeof?: symbol | string; + preactCompatNormalized?: boolean; +} + +export interface SuspenseState { + _suspended?: null | VNode<any>; +} + +export interface SuspenseComponent + extends PreactComponent<SuspenseProps, SuspenseState> { + _pendingSuspensionCount: number; + _suspenders: Component[]; + _detachOnNextRender: null | VNode<any>; +} diff --git a/preact/compat/src/memo.js b/preact/compat/src/memo.js new file mode 100644 index 0000000..e743199 --- /dev/null +++ b/preact/compat/src/memo.js @@ -0,0 +1,34 @@ +import { createElement } from 'preact'; +import { shallowDiffers } from './util'; + +/** + * Memoize a component, so that it only updates when the props actually have + * changed. This was previously known as `React.pure`. + * @param {import('./internal').FunctionComponent} c functional component + * @param {(prev: object, next: object) => boolean} [comparer] Custom equality function + * @returns {import('./internal').FunctionComponent} + */ +export function memo(c, comparer) { + function shouldUpdate(nextProps) { + let ref = this.props.ref; + let updateRef = ref == nextProps.ref; + if (!updateRef && ref) { + ref.call ? ref(null) : (ref.current = null); + } + + if (!comparer) { + return shallowDiffers(this.props, nextProps); + } + + return !comparer(this.props, nextProps) || !updateRef; + } + + function Memoed(props) { + this.shouldComponentUpdate = shouldUpdate; + return createElement(c, props); + } + Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')'; + Memoed.prototype.isReactComponent = true; + Memoed._forwarded = true; + return Memoed; +} diff --git a/preact/compat/src/portals.js b/preact/compat/src/portals.js new file mode 100644 index 0000000..cf9f8f7 --- /dev/null +++ b/preact/compat/src/portals.js @@ -0,0 +1,80 @@ +import { createElement, render } from 'preact'; + +/** + * @param {import('../../src/index').RenderableProps<{ context: any }>} props + */ +function ContextProvider(props) { + this.getChildContext = () => props.context; + return props.children; +} + +/** + * Portal component + * @this {import('./internal').Component} + * @param {object | null | undefined} props + * + * TODO: use createRoot() instead of fake root + */ +function Portal(props) { + const _this = this; + let container = props._container; + + _this.componentWillUnmount = function() { + render(null, _this._temp); + _this._temp = null; + _this._container = null; + }; + + // When we change container we should clear our old container and + // indicate a new mount. + if (_this._container && _this._container !== container) { + _this.componentWillUnmount(); + } + + // When props.vnode is undefined/false/null we are dealing with some kind of + // conditional vnode. This should not trigger a render. + if (props._vnode) { + if (!_this._temp) { + _this._container = container; + + // Create a fake DOM parent node that manages a subset of `container`'s children: + _this._temp = { + nodeType: 1, + parentNode: container, + childNodes: [], + appendChild(child) { + this.childNodes.push(child); + _this._container.appendChild(child); + }, + insertBefore(child, before) { + this.childNodes.push(child); + _this._container.appendChild(child); + }, + removeChild(child) { + this.childNodes.splice(this.childNodes.indexOf(child) >>> 1, 1); + _this._container.removeChild(child); + } + }; + } + + // Render our wrapping element into temp. + render( + createElement(ContextProvider, { context: _this.context }, props._vnode), + _this._temp + ); + } + // When we come from a conditional render, on a mounted + // portal we should clear the DOM. + else if (_this._temp) { + _this.componentWillUnmount(); + } +} + +/** + * Create a `Portal` to continue rendering the vnode tree at a different DOM node + * @param {import('./internal').VNode} vnode The vnode to render + * @param {import('./internal').PreactElement} container The DOM node to continue rendering in to. + */ +export function createPortal(vnode, container) { + return createElement(Portal, { _vnode: vnode, _container: container }); +} diff --git a/preact/compat/src/render.js b/preact/compat/src/render.js new file mode 100644 index 0000000..2448d2d --- /dev/null +++ b/preact/compat/src/render.js @@ -0,0 +1,219 @@ +import { + render as preactRender, + hydrate as preactHydrate, + options, + toChildArray, + Component +} from 'preact'; + +export const REACT_ELEMENT_TYPE = + (typeof Symbol != 'undefined' && Symbol.for && Symbol.for('react.element')) || + 0xeac7; + +const CAMEL_PROPS = /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/; + +// Input types for which onchange should not be converted to oninput. +// type="file|checkbox|radio", plus "range" in IE11. +// (IE11 doesn't support Symbol, which we use here to turn `rad` into `ra` which matches "range") +const onChangeInputType = type => + (typeof Symbol != 'undefined' && typeof Symbol() == 'symbol' + ? /fil|che|rad/i + : /fil|che|ra/i + ).test(type); + +// Some libraries like `react-virtualized` explicitly check for this. +Component.prototype.isReactComponent = {}; + +// `UNSAFE_*` lifecycle hooks +// Preact only ever invokes the unprefixed methods. +// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method. +// - If a component defines its own `componentDidMount()` (including via defineProperty), use that. +// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter. +// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property. +// See https://github.com/preactjs/preact/issues/1941 +[ + 'componentWillMount', + 'componentWillReceiveProps', + 'componentWillUpdate' +].forEach(key => { + Object.defineProperty(Component.prototype, key, { + configurable: true, + get() { + return this['UNSAFE_' + key]; + }, + set(v) { + Object.defineProperty(this, key, { + configurable: true, + writable: true, + value: v + }); + } + }); +}); + +/** + * Proxy render() since React returns a Component reference. + * @param {import('./internal').VNode} vnode VNode tree to render + * @param {import('./internal').PreactElement} parent DOM node to render vnode tree into + * @param {() => void} [callback] Optional callback that will be called after rendering + * @returns {import('./internal').Component | null} The root component reference or null + */ +export function render(vnode, parent, callback) { + // React destroys any existing DOM nodes, see #1727 + // ...but only on the first render, see #1828 + if (parent._children == null) { + parent.textContent = ''; + } + + preactRender(vnode, parent); + if (typeof callback == 'function') callback(); + + return vnode ? vnode._component : null; +} + +export function hydrate(vnode, parent, callback) { + preactHydrate(vnode, parent); + if (typeof callback == 'function') callback(); + + return vnode ? vnode._component : null; +} + +let oldEventHook = options.event; +options.event = e => { + if (oldEventHook) e = oldEventHook(e); + e.persist = empty; + e.isPropagationStopped = isPropagationStopped; + e.isDefaultPrevented = isDefaultPrevented; + return (e.nativeEvent = e); +}; + +function empty() {} + +function isPropagationStopped() { + return this.cancelBubble; +} + +function isDefaultPrevented() { + return this.defaultPrevented; +} + +let classNameDescriptor = { + configurable: true, + get() { + return this.class; + } +}; + +let oldVNodeHook = options.vnode; +options.vnode = vnode => { + let type = vnode.type; + let props = vnode.props; + let normalizedProps = props; + + // only normalize props on Element nodes + if (typeof type === 'string') { + normalizedProps = {}; + + for (let i in props) { + let value = props[i]; + + if (i === 'value' && 'defaultValue' in props && value == null) { + // Skip applying value if it is null/undefined and we already set + // a default value + continue; + } else if ( + i === 'defaultValue' && + 'value' in props && + props.value == null + ) { + // `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined. + // `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property. + i = 'value'; + } else if (i === 'download' && value === true) { + // Calling `setAttribute` with a truthy value will lead to it being + // passed as a stringified value, e.g. `download="true"`. React + // converts it to an empty string instead, otherwise the attribute + // value will be used as the file name and the file will be called + // "true" upon downloading it. + value = ''; + } else if (/ondoubleclick/i.test(i)) { + i = 'ondblclick'; + } else if ( + /^onchange(textarea|input)/i.test(i + type) && + !onChangeInputType(props.type) + ) { + i = 'oninput'; + } else if (/^on(Ani|Tra|Tou|BeforeInp)/.test(i)) { + i = i.toLowerCase(); + } else if (CAMEL_PROPS.test(i)) { + i = i.replace(/[A-Z0-9]/, '-$&').toLowerCase(); + } else if (value === null) { + value = undefined; + } + + normalizedProps[i] = value; + } + + // Add support for array select values: <select multiple value={[]} /> + if ( + type == 'select' && + normalizedProps.multiple && + Array.isArray(normalizedProps.value) + ) { + // forEach() always returns undefined, which we abuse here to unset the value prop. + normalizedProps.value = toChildArray(props.children).forEach(child => { + child.props.selected = + normalizedProps.value.indexOf(child.props.value) != -1; + }); + } + + // Adding support for defaultValue in select tag + if (type == 'select' && normalizedProps.defaultValue != null) { + normalizedProps.value = toChildArray(props.children).forEach(child => { + if (normalizedProps.multiple) { + child.props.selected = + normalizedProps.defaultValue.indexOf(child.props.value) != -1; + } else { + child.props.selected = + normalizedProps.defaultValue == child.props.value; + } + }); + } + + vnode.props = normalizedProps; + } + + if (type && props.class != props.className) { + classNameDescriptor.enumerable = 'className' in props; + if (props.className != null) normalizedProps.class = props.className; + Object.defineProperty(normalizedProps, 'className', classNameDescriptor); + } + + vnode.$$typeof = REACT_ELEMENT_TYPE; + + if (oldVNodeHook) oldVNodeHook(vnode); +}; + +// Only needed for react-relay +let currentComponent; +const oldBeforeRender = options._render; +options._render = function(vnode) { + if (oldBeforeRender) { + oldBeforeRender(vnode); + } + currentComponent = vnode._component; +}; + +// This is a very very private internal function for React it +// is used to sort-of do runtime dependency injection. So far +// only `react-relay` makes use of it. It uses it to read the +// context value. +export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = { + ReactCurrentDispatcher: { + current: { + readContext(context) { + return currentComponent._globalContext[context._id].props.value; + } + } + } +}; diff --git a/preact/compat/src/suspense-list.d.ts b/preact/compat/src/suspense-list.d.ts new file mode 100644 index 0000000..caa1eb6 --- /dev/null +++ b/preact/compat/src/suspense-list.d.ts @@ -0,0 +1,14 @@ +import { Component, ComponentChild, ComponentChildren } from '../../src'; + +// +// SuspenseList +// ----------------------------------- + +export interface SuspenseListProps { + children?: ComponentChildren; + revealOrder?: 'forwards' | 'backwards' | 'together'; +} + +export class SuspenseList extends Component<SuspenseListProps> { + render(): ComponentChild; +} diff --git a/preact/compat/src/suspense-list.js b/preact/compat/src/suspense-list.js new file mode 100644 index 0000000..cf162cb --- /dev/null +++ b/preact/compat/src/suspense-list.js @@ -0,0 +1,126 @@ +import { Component, toChildArray } from 'preact'; +import { suspended } from './suspense.js'; + +// Indexes to linked list nodes (nodes are stored as arrays to save bytes). +const SUSPENDED_COUNT = 0; +const RESOLVED_COUNT = 1; +const NEXT_NODE = 2; + +// Having custom inheritance instead of a class here saves a lot of bytes. +export function SuspenseList() { + this._next = null; + this._map = null; +} + +// Mark one of child's earlier suspensions as resolved. +// Some pending callbacks may become callable due to this +// (e.g. the last suspended descendant gets resolved when +// revealOrder === 'together'). Process those callbacks as well. +const resolve = (list, child, node) => { + if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) { + // The number a child (or any of its descendants) has been suspended + // matches the number of times it's been resolved. Therefore we + // mark the child as completely resolved by deleting it from ._map. + // This is used to figure out when *all* children have been completely + // resolved when revealOrder is 'together'. + list._map.delete(child); + } + + // If revealOrder is falsy then we can do an early exit, as the + // callbacks won't get queued in the node anyway. + // If revealOrder is 'together' then also do an early exit + // if all suspended descendants have not yet been resolved. + if ( + !list.props.revealOrder || + (list.props.revealOrder[0] === 't' && list._map.size) + ) { + return; + } + + // Walk the currently suspended children in order, calling their + // stored callbacks on the way. Stop if we encounter a child that + // has not been completely resolved yet. + node = list._next; + while (node) { + while (node.length > 3) { + node.pop()(); + } + if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) { + break; + } + list._next = node = node[NEXT_NODE]; + } +}; + +// Things we do here to save some bytes but are not proper JS inheritance: +// - call `new Component()` as the prototype +// - do not set `Suspense.prototype.constructor` to `Suspense` +SuspenseList.prototype = new Component(); + +SuspenseList.prototype._suspended = function(child) { + const list = this; + const delegated = suspended(list._vnode); + + let node = list._map.get(child); + node[SUSPENDED_COUNT]++; + + return unsuspend => { + const wrappedUnsuspend = () => { + if (!list.props.revealOrder) { + // Special case the undefined (falsy) revealOrder, as there + // is no need to coordinate a specific order or unsuspends. + unsuspend(); + } else { + node.push(unsuspend); + resolve(list, child, node); + } + }; + if (delegated) { + delegated(wrappedUnsuspend); + } else { + wrappedUnsuspend(); + } + }; +}; + +SuspenseList.prototype.render = function(props) { + this._next = null; + this._map = new Map(); + + const children = toChildArray(props.children); + if (props.revealOrder && props.revealOrder[0] === 'b') { + // If order === 'backwards' (or, well, anything starting with a 'b') + // then flip the child list around so that the last child will be + // the first in the linked list. + children.reverse(); + } + // Build the linked list. Iterate through the children in reverse order + // so that `_next` points to the first linked list node to be resolved. + for (let i = children.length; i--; ) { + // Create a new linked list node as an array of form: + // [suspended_count, resolved_count, next_node] + // where suspended_count and resolved_count are numeric counters for + // keeping track how many times a node has been suspended and resolved. + // + // Note that suspended_count starts from 1 instead of 0, so we can block + // processing callbacks until componentDidMount has been called. In a sense + // node is suspended at least until componentDidMount gets called! + // + // Pending callbacks are added to the end of the node: + // [suspended_count, resolved_count, next_node, callback_0, callback_1, ...] + this._map.set(children[i], (this._next = [1, 0, this._next])); + } + return props.children; +}; + +SuspenseList.prototype.componentDidUpdate = SuspenseList.prototype.componentDidMount = function() { + // Iterate through all children after mounting for two reasons: + // 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases + // each node[RELEASED_COUNT] by 1, therefore balancing the counters. + // The nodes can now be completely consumed from the linked list. + // 2. Handle nodes that might have gotten resolved between render and + // componentDidMount. + this._map.forEach((node, child) => { + resolve(this, child, node); + }); +}; diff --git a/preact/compat/src/suspense.d.ts b/preact/compat/src/suspense.d.ts new file mode 100644 index 0000000..9bd0e74 --- /dev/null +++ b/preact/compat/src/suspense.d.ts @@ -0,0 +1,15 @@ +import { Component, ComponentChild, ComponentChildren } from '../../src'; + +// +// Suspense/lazy +// ----------------------------------- +export function lazy<T>(loader: () => Promise<{ default: T } | T>): T; + +export interface SuspenseProps { + children?: ComponentChildren; + fallback: ComponentChildren; +} + +export class Suspense extends Component<SuspenseProps> { + render(): ComponentChild; +} diff --git a/preact/compat/src/suspense.js b/preact/compat/src/suspense.js new file mode 100644 index 0000000..6244eaf --- /dev/null +++ b/preact/compat/src/suspense.js @@ -0,0 +1,270 @@ +import { Component, createElement, options, Fragment } from 'preact'; +import { assign } from './util'; + +const oldCatchError = options._catchError; +options._catchError = function(error, newVNode, oldVNode) { + if (error.then) { + /** @type {import('./internal').Component} */ + let component; + let vnode = newVNode; + + for (; (vnode = vnode._parent); ) { + if ((component = vnode._component) && component._childDidSuspend) { + if (newVNode._dom == null) { + newVNode._dom = oldVNode._dom; + newVNode._children = oldVNode._children; + } + // Don't call oldCatchError if we found a Suspense + return component._childDidSuspend(error, newVNode); + } + } + } + oldCatchError(error, newVNode, oldVNode); +}; + +const oldUnmount = options.unmount; +options.unmount = function(vnode) { + /** @type {import('./internal').Component} */ + const component = vnode._component; + if (component && component._onResolve) { + component._onResolve(); + } + + // if the component is still hydrating + // most likely it is because the component is suspended + // we set the vnode.type as `null` so that it is not a typeof function + // so the unmount will remove the vnode._dom + if (component && vnode._hydrating === true) { + vnode.type = null; + } + + if (oldUnmount) oldUnmount(vnode); +}; + +function detachedClone(vnode, detachedParent, parentDom) { + if (vnode) { + if (vnode._component && vnode._component.__hooks) { + vnode._component.__hooks._list.forEach(effect => { + if (typeof effect._cleanup == 'function') effect._cleanup(); + }); + + vnode._component.__hooks = null; + } + + vnode = assign({}, vnode); + if (vnode._component != null) { + if (vnode._component._parentDom === parentDom) { + vnode._component._parentDom = detachedParent; + } + vnode._component = null; + } + + vnode._children = + vnode._children && + vnode._children.map(child => + detachedClone(child, detachedParent, parentDom) + ); + } + + return vnode; +} + +function removeOriginal(vnode, detachedParent, originalParent) { + if (vnode) { + vnode._original = null; + vnode._children = + vnode._children && + vnode._children.map(child => + removeOriginal(child, detachedParent, originalParent) + ); + + if (vnode._component) { + if (vnode._component._parentDom === detachedParent) { + if (vnode._dom) { + originalParent.insertBefore(vnode._dom, vnode._nextDom); + } + vnode._component._force = true; + vnode._component._parentDom = originalParent; + } + } + } + + return vnode; +} + +// having custom inheritance instead of a class here saves a lot of bytes +export function Suspense() { + // we do not call super here to golf some bytes... + this._pendingSuspensionCount = 0; + this._suspenders = null; + this._detachOnNextRender = null; +} + +// Things we do here to save some bytes but are not proper JS inheritance: +// - call `new Component()` as the prototype +// - do not set `Suspense.prototype.constructor` to `Suspense` +Suspense.prototype = new Component(); + +/** + * @this {import('./internal').SuspenseComponent} + * @param {Promise} promise The thrown promise + * @param {import('./internal').VNode<any, any>} suspendingVNode The suspending component + */ +Suspense.prototype._childDidSuspend = function(promise, suspendingVNode) { + const suspendingComponent = suspendingVNode._component; + + /** @type {import('./internal').SuspenseComponent} */ + const c = this; + + if (c._suspenders == null) { + c._suspenders = []; + } + c._suspenders.push(suspendingComponent); + + const resolve = suspended(c._vnode); + + let resolved = false; + const onResolved = () => { + if (resolved) return; + + resolved = true; + suspendingComponent._onResolve = null; + + if (resolve) { + resolve(onSuspensionComplete); + } else { + onSuspensionComplete(); + } + }; + + suspendingComponent._onResolve = onResolved; + + const onSuspensionComplete = () => { + if (!--c._pendingSuspensionCount) { + // If the suspension was during hydration we don't need to restore the + // suspended children into the _children array + if (c.state._suspended) { + const suspendedVNode = c.state._suspended; + c._vnode._children[0] = removeOriginal( + suspendedVNode, + suspendedVNode._component._parentDom, + suspendedVNode._component._originalParentDom + ); + } + + c.setState({ _suspended: (c._detachOnNextRender = null) }); + + let suspended; + while ((suspended = c._suspenders.pop())) { + suspended.forceUpdate(); + } + } + }; + + /** + * We do not set `suspended: true` during hydration because we want the actual markup + * to remain on screen and hydrate it when the suspense actually gets resolved. + * While in non-hydration cases the usual fallback -> component flow would occour. + */ + const wasHydrating = suspendingVNode._hydrating === true; + if (!c._pendingSuspensionCount++ && !wasHydrating) { + c.setState({ _suspended: (c._detachOnNextRender = c._vnode._children[0]) }); + } + promise.then(onResolved, onResolved); +}; + +Suspense.prototype.componentWillUnmount = function() { + this._suspenders = []; +}; + +/** + * @this {import('./internal').SuspenseComponent} + * @param {import('./internal').SuspenseComponent["props"]} props + * @param {import('./internal').SuspenseState} state + */ +Suspense.prototype.render = function(props, state) { + if (this._detachOnNextRender) { + // When the Suspense's _vnode was created by a call to createVNode + // (i.e. due to a setState further up in the tree) + // it's _children prop is null, in this case we "forget" about the parked vnodes to detach + if (this._vnode._children) { + const detachedParent = document.createElement('div'); + const detachedComponent = this._vnode._children[0]._component; + this._vnode._children[0] = detachedClone( + this._detachOnNextRender, + detachedParent, + (detachedComponent._originalParentDom = detachedComponent._parentDom) + ); + } + + this._detachOnNextRender = null; + } + + // Wrap fallback tree in a VNode that prevents itself from being marked as aborting mid-hydration: + /** @type {import('./internal').VNode} */ + const fallback = + state._suspended && createElement(Fragment, null, props.fallback); + if (fallback) fallback._hydrating = null; + + return [ + createElement(Fragment, null, state._suspended ? null : props.children), + fallback + ]; +}; + +/** + * Checks and calls the parent component's _suspended method, passing in the + * suspended vnode. This is a way for a parent (e.g. SuspenseList) to get notified + * that one of its children/descendants suspended. + * + * The parent MAY return a callback. The callback will get called when the + * suspension resolves, notifying the parent of the fact. + * Moreover, the callback gets function `unsuspend` as a parameter. The resolved + * child descendant will not actually get unsuspended until `unsuspend` gets called. + * This is a way for the parent to delay unsuspending. + * + * If the parent does not return a callback then the resolved vnode + * gets unsuspended immediately when it resolves. + * + * @param {import('./internal').VNode} vnode + * @returns {((unsuspend: () => void) => void)?} + */ +export function suspended(vnode) { + /** @type {import('./internal').Component} */ + let component = vnode._parent._component; + return component && component._suspended && component._suspended(vnode); +} + +export function lazy(loader) { + let prom; + let component; + let error; + + function Lazy(props) { + if (!prom) { + prom = loader(); + prom.then( + exports => { + component = exports.default || exports; + }, + e => { + error = e; + } + ); + } + + if (error) { + throw error; + } + + if (!component) { + throw prom; + } + + return createElement(component, props); + } + + Lazy.displayName = 'Lazy'; + Lazy._forwarded = true; + return Lazy; +} diff --git a/preact/compat/src/util.js b/preact/compat/src/util.js new file mode 100644 index 0000000..fa12b09 --- /dev/null +++ b/preact/compat/src/util.js @@ -0,0 +1,28 @@ +/** + * Assign properties from `props` to `obj` + * @template O, P The obj and props types + * @param {O} obj The object to copy properties to + * @param {P} props The object to copy properties from + * @returns {O & P} + */ +export function assign(obj, props) { + for (let i in props) obj[i] = props[i]; + return /** @type {O & P} */ (obj); +} + +/** + * Check if two objects have a different shape + * @param {object} a + * @param {object} b + * @returns {boolean} + */ +export function shallowDiffers(a, b) { + for (let i in a) if (i !== '__source' && !(i in b)) return true; + for (let i in b) if (i !== '__source' && a[i] !== b[i]) return true; + return false; +} + +export function removeNode(node) { + let parentNode = node.parentNode; + if (parentNode) parentNode.removeChild(node); +} |