enzyme-adapter-preact-pure
Version:
Enzyme adapter for Preact
302 lines (300 loc) • 10.2 kB
JavaScript
/**
* This file includes modified copies of the Preact source and custom
* implementations of Preact functions (e.g. setState) to perform a shallow
* render
*
* The Preact source is copyrighted to Jason Miller and licensed under the MIT
* License, found a the link below:
*
* https://github.com/preactjs/preact/blob/d4089df1263faab9b980a3493a4c7e986f254f8e/LICENSE
*/
import { Component, options as rawOptions } from 'preact';
const options = rawOptions;
/** Symbol to return if a component indicates it should not update */
export const skipUpdateSymbol = Symbol('PreactShallowRenderer skip update');
/**
* Shallowly render a component. Much of this function is copied directly from
* the Preact 10 source. Differences to the original source are commented.
*/
export function diffComponent(newVNode, oldVNode, globalContext, commitQueue) {
/* eslint-disable */
let tmp,
newType = newVNode.type,
parentDom = null;
if (options.__b) options.__b(newVNode);
// ===== START PREACT SOURCE COPY =====
let c, isNew, oldProps, clearProcessingException;
let newProps = newVNode.props;
// Necessary for createContext api. Setting this property will pass
// the context value as `this.context` just for this component.
tmp = newType.contextType;
let provider = tmp && globalContext[tmp.__c];
let componentContext = tmp ? provider ? provider.props.value : tmp.__ : globalContext;
// Get component and set it to `c`
if (oldVNode.__c) {
c = newVNode.__c = oldVNode.__c;
clearProcessingException = c.__ = c.__E;
} else {
// Instantiate the new component
if ('prototype' in newType && newType.prototype.render) {
// @ts-ignore The check above verifies that newType is suppose to be constructed
newVNode.__c = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
} else {
// @ts-ignore Trust me, Component implements the interface we want
newVNode.__c = c = new Component(newProps, componentContext);
c.constructor = newType;
c.render = doRender;
}
if (provider) provider.sub(c);
c.props = newProps;
if (!c.state) c.state = {};
c.context = componentContext;
c.__n = globalContext;
isNew = c.__d = true;
c.__h = [];
c._sb = [];
}
// Invoke getDerivedStateFromProps
if (c.__s == null) {
c.__s = c.state;
}
if (newType.getDerivedStateFromProps != null) {
if (c.__s == c.state) {
c.__s = assign({}, c.__s);
}
assign(c.__s, newType.getDerivedStateFromProps(newProps, c.__s));
}
oldProps = c.props;
// oldState = c.state;
// Invoke pre-render lifecycle methods
if (isNew) {
if (newType.getDerivedStateFromProps == null && c.componentWillMount != null) {
c.componentWillMount();
}
// == SHALLOW RENDER CHANGE: Don't invoke CDM
// if (c.componentDidMount != null) {
// c._renderCallbacks.push(c.componentDidMount);
// }
} else {
if (newType.getDerivedStateFromProps == null && newProps !== oldProps && c.componentWillReceiveProps != null) {
c.componentWillReceiveProps(newProps, componentContext);
}
if (!c.__e && c.shouldComponentUpdate != null && c.shouldComponentUpdate(newProps, c.__s, componentContext) === false || newVNode.__v === oldVNode.__v) {
c.props = newProps;
c.state = c.__s;
// More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
if (newVNode.__v !== oldVNode.__v) c.__d = false;
c.__v = newVNode;
newVNode.__e = oldVNode.__e;
newVNode.__k = oldVNode.__k;
newVNode.__k?.forEach(vnode => {
if (vnode) vnode.__ = newVNode;
});
for (let i = 0; i < c._sb.length; i++) {
c.__h.push(c._sb[i]);
}
c._sb = [];
if (c.__h.length) {
commitQueue.push(c);
}
// break outer;
return skipUpdateSymbol; // === SHALLOW RENDER CHANGE: return skip update symbol
}
if (c.componentWillUpdate != null) {
c.componentWillUpdate(newProps, c.__s, componentContext);
}
// == SHALLOW RENDER CHANGE: Don't invoke CDU
// if (c.componentDidUpdate != null) {
// c._renderCallbacks.push(() => {
// c.componentDidUpdate(oldProps, oldState, snapshot);
// });
// }
}
c.context = componentContext;
c.props = newProps;
c.__v = newVNode;
c.__P = parentDom;
let renderHook = options.__r,
count = 0;
if ('prototype' in newType && newType.prototype.render) {
c.state = c.__s;
c.__d = false;
if (renderHook) renderHook(newVNode);
tmp = c.render(c.props, c.state, c.context);
for (let i = 0; i < c._sb.length; i++) {
c.__h.push(c._sb[i]);
}
c._sb = [];
} else {
do {
c.__d = false;
if (renderHook) renderHook(newVNode);
tmp = c.render(c.props, c.state, c.context);
// Handle setState called in render, see #2553
c.state = c.__s;
} while (c.__d && ++count < 25);
}
// Handle setState called in render, see #2553
c.state = c.__s;
if (c.getChildContext != null) {
globalContext = assign(assign({}, globalContext), c.getChildContext());
}
// == SHALLOW RENDER CHANGE: Don't invoke gSBU
// if (!isNew && c.getSnapshotBeforeUpdate != null) {
// snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
// }
let renderResult = tmp;
// == SHALLOW RENDER CHANGE: Don't skip Fragments returned from components
// let isTopLevelFragment =
// tmp != null && tmp.type === Fragment && tmp.key == null;
// let renderResult = isTopLevelFragment ? tmp.props.children : tmp;
// == SHALLOW RENDER CHANGE: Don't render children
// diffChildren(
// parentDom,
// Array.isArray(renderResult) ? renderResult : [renderResult],
// newVNode,
// oldVNode,
// globalContext,
// isSvg,
// excessDomChildren,
// commitQueue,
// oldDom,
// isHydrating
// );
c.base = newVNode.__e;
// We successfully rendered this VNode, unset any stored hydration/bailout state:
newVNode.__h = null;
if (c.__h.length) {
commitQueue.push(c);
}
if (clearProcessingException) {
c.__E = c.__ = null;
}
c.__e = false;
// ===== END PREACT SOURCE COPY =====
if (options.diffed) options.diffed(newVNode);
return renderResult;
}
/**
* 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}
*/
function assign(obj, props) {
// ===== COPIED FROM PREACT SOURCE =====
// @ts-ignore We change the type of `obj` to be `O & P`
for (let i in props) obj[i] = props[i];
return obj;
}
/** The `.render()` method for a PFC backing instance. */
function doRender(props, state, context) {
return this.constructor(props, context);
}
/**
* @param {Array<import('../internal').Component>} commitQueue List of components
* which have callbacks to invoke in commitRoot
* @param {import('../internal').VNode} root
*/
export function commitRoot(commitQueue, root) {
// ===== COPIED FROM PREACT SOURCE =====
if (options.__c) options.__c(root, commitQueue);
commitQueue.some(c => {
try {
// @ts-ignore Reuse the commitQueue variable here so the type changes
commitQueue = c.__h;
c.__h = [];
commitQueue.some(cb => {
// @ts-ignore See above ts-ignore on commitQueue
cb.call(c);
});
} catch (e) {
options.__e(e, c.__v, undefined, {});
}
});
}
export function unmount(vnode) {
// ===== COPIED FROM PREACT SOURCE =====
let r;
if (options.unmount) options.unmount(vnode);
// == SHALLOW RENDER CHANGE: Don't invoke refs
// if ((r = vnode.ref)) {
// if (!r.current || r.current === vnode._dom) {
// applyRef(r, null, parentVNode);
// }
// }
if ((r = vnode.__c) != null) {
if (r.componentWillUnmount) {
try {
r.componentWillUnmount();
} catch (e) {
options.__e(e, vnode.__, undefined, undefined);
}
}
r.base = r.__P = undefined;
vnode.__c = null;
}
// == SHALLOW RENDER CHANGE: Don't unmount children
// if ((r = vnode._children)) {
// for (let i = 0; i < r.length; i++) {
// if (r[i]) {
// unmount(
// r[i],
// parentVNode,
// skipRemove || typeof vnode.type !== 'function'
// );
// }
// }
// }
// == SHALLOW RENDER CHANGE: Don't remove DOM nodes
// if (!skipRemove && vnode._dom != null) {
// removeNode(vnode._dom);
// }
// Must be set to `undefined` to properly clean up `_nextDom`
// for which `null` is a valid value. See comment in `create-element.js`
vnode.__ = vnode.__e = vnode.__d = undefined;
}
export function shallowSetState(update, callback) {
// ===== COPIED FROM PREACT SOURCE ===== Need to have our own version so our
// own `enqueueRender` function is called. The one built-in to Preact always
// calls the real client renderer
// only clone state when copying to nextState the first time.
let s;
if (this.__s != null && this.__s !== this.state) {
s = this.__s;
} else {
s = this.__s = assign({}, this.state);
}
if (typeof update == 'function') {
// Some libraries like `immer` mark the current state as readonly,
// preventing us from mutating it, so we need to clone it. See #2716
update = update(assign({}, s), this.props);
}
if (update) {
assign(s, update);
}
// Skip update if updater function returned null
if (update == null) return;
if (this.__v) {
if (callback) {
this._sb.push(callback);
}
enqueueRender(this);
}
}
export function shallowForceUpdate(callback) {
// ===== COPIED FROM PREACT SOURCE ===== Need to have our own version so our
// own `enqueueRender` function is called. The one built-in to Preact always
// calls the real client renderer
this.__e = true;
if (callback) this.__h.push(callback);
enqueueRender(this);
}
function enqueueRender(component) {
let newVNode = assign({}, component.__v);
newVNode.__v = NaN;
component.__d = true;
let renderer = component._preactShallowRenderer;
renderer?.render(newVNode, component.__n);
}