diff options
Diffstat (limited to 'preact/debug/src/debug.js')
-rw-r--r-- | preact/debug/src/debug.js | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/preact/debug/src/debug.js b/preact/debug/src/debug.js new file mode 100644 index 0000000..27927cb --- /dev/null +++ b/preact/debug/src/debug.js @@ -0,0 +1,442 @@ +import { checkPropTypes } from './check-props'; +import { options, Component } from 'preact'; +import { + ELEMENT_NODE, + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE +} from './constants'; +import { + getOwnerStack, + setupComponentStack, + getCurrentVNode, + getDisplayName +} from './component-stack'; +import { assign } from './util'; + +const isWeakMapSupported = typeof WeakMap == 'function'; + +function getClosestDomNodeParent(parent) { + if (!parent) return {}; + if (typeof parent.type == 'function') { + return getClosestDomNodeParent(parent._parent); + } + return parent; +} + +export function initDebug() { + setupComponentStack(); + + let hooksAllowed = false; + + /* eslint-disable no-console */ + let oldBeforeDiff = options._diff; + let oldDiffed = options.diffed; + let oldVnode = options.vnode; + let oldCatchError = options._catchError; + let oldRoot = options._root; + let oldHook = options._hook; + const warnedComponents = !isWeakMapSupported + ? null + : { + useEffect: new WeakMap(), + useLayoutEffect: new WeakMap(), + lazyPropTypes: new WeakMap() + }; + const deprecations = []; + + options._catchError = (error, vnode, oldVNode) => { + let component = vnode && vnode._component; + if (component && typeof error.then == 'function') { + const promise = error; + error = new Error( + `Missing Suspense. The throwing component was: ${getDisplayName(vnode)}` + ); + + let parent = vnode; + for (; parent; parent = parent._parent) { + if (parent._component && parent._component._childDidSuspend) { + error = promise; + break; + } + } + + // We haven't recovered and we know at this point that there is no + // Suspense component higher up in the tree + if (error instanceof Error) { + throw error; + } + } + + try { + oldCatchError(error, vnode, oldVNode); + + // when an error was handled by an ErrorBoundary we will nontheless emit an error + // event on the window object. This is to make up for react compatibility in dev mode + // and thus make the Next.js dev overlay work. + if (typeof error.then != 'function') { + setTimeout(() => { + throw error; + }); + } + } catch (e) { + throw e; + } + }; + + options._root = (vnode, parentNode) => { + if (!parentNode) { + throw new Error( + 'Undefined parent passed to render(), this is the second argument.\n' + + 'Check if the element is available in the DOM/has the correct id.' + ); + } + + let isValid; + switch (parentNode.nodeType) { + case ELEMENT_NODE: + case DOCUMENT_FRAGMENT_NODE: + case DOCUMENT_NODE: + isValid = true; + break; + default: + isValid = false; + } + + if (!isValid) { + let componentName = getDisplayName(vnode); + throw new Error( + `Expected a valid HTML node as a second argument to render. Received ${parentNode} instead: render(<${componentName} />, ${parentNode});` + ); + } + + if (oldRoot) oldRoot(vnode, parentNode); + }; + + options._diff = vnode => { + let { type, _parent: parent } = vnode; + let parentVNode = getClosestDomNodeParent(parent); + + hooksAllowed = true; + + if (type === undefined) { + throw new Error( + 'Undefined component passed to createElement()\n\n' + + 'You likely forgot to export your component or might have mixed up default and named imports' + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } else if (type != null && typeof type == 'object') { + if (type._children !== undefined && type._dom !== undefined) { + throw new Error( + `Invalid type passed to createElement(): ${type}\n\n` + + 'Did you accidentally pass a JSX literal as JSX twice?\n\n' + + ` let My${getDisplayName(vnode)} = ${serializeVNode(type)};\n` + + ` let vnode = <My${getDisplayName(vnode)} />;\n\n` + + 'This usually happens when you export a JSX literal and not the component.' + + `\n\n${getOwnerStack(vnode)}` + ); + } + + throw new Error( + 'Invalid type passed to createElement(): ' + + (Array.isArray(type) ? 'array' : type) + ); + } + + if ( + (type === 'thead' || type === 'tfoot' || type === 'tbody') && + parentVNode.type !== 'table' + ) { + console.error( + 'Improper nesting of table. Your <thead/tbody/tfoot> should have a <table> parent.' + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } else if ( + type === 'tr' && + parentVNode.type !== 'thead' && + parentVNode.type !== 'tfoot' && + parentVNode.type !== 'tbody' && + parentVNode.type !== 'table' + ) { + console.error( + 'Improper nesting of table. Your <tr> should have a <thead/tbody/tfoot/table> parent.' + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } else if (type === 'td' && parentVNode.type !== 'tr') { + console.error( + 'Improper nesting of table. Your <td> should have a <tr> parent.' + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } else if (type === 'th' && parentVNode.type !== 'tr') { + console.error( + 'Improper nesting of table. Your <th> should have a <tr>.' + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } + + if ( + vnode.ref !== undefined && + typeof vnode.ref != 'function' && + typeof vnode.ref != 'object' && + !('$$typeof' in vnode) // allow string refs when preact-compat is installed + ) { + throw new Error( + `Component's "ref" property should be a function, or an object created ` + + `by createRef(), but got [${typeof vnode.ref}] instead\n` + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } + + if (typeof vnode.type == 'string') { + for (const key in vnode.props) { + if ( + key[0] === 'o' && + key[1] === 'n' && + typeof vnode.props[key] != 'function' && + vnode.props[key] != null + ) { + throw new Error( + `Component's "${key}" property should be a function, ` + + `but got [${typeof vnode.props[key]}] instead\n` + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + } + } + } + + // Check prop-types if available + if (typeof vnode.type == 'function' && vnode.type.propTypes) { + if ( + vnode.type.displayName === 'Lazy' && + warnedComponents && + !warnedComponents.lazyPropTypes.has(vnode.type) + ) { + const m = + 'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. '; + try { + const lazyVNode = vnode.type(); + warnedComponents.lazyPropTypes.set(vnode.type, true); + console.warn( + m + `Component wrapped in lazy() is ${getDisplayName(lazyVNode)}` + ); + } catch (promise) { + console.warn( + m + "We will log the wrapped component's name once it is loaded." + ); + } + } + + let values = vnode.props; + if (vnode.type._forwarded) { + values = assign({}, values); + delete values.ref; + } + + checkPropTypes( + vnode.type.propTypes, + values, + 'prop', + getDisplayName(vnode), + () => getOwnerStack(vnode) + ); + } + + if (oldBeforeDiff) oldBeforeDiff(vnode); + }; + + options._hook = (comp, index, type) => { + if (!comp || !hooksAllowed) { + throw new Error('Hook can only be invoked from render methods.'); + } + + if (oldHook) oldHook(comp, index, type); + }; + + // Ideally we'd want to print a warning once per component, but we + // don't have access to the vnode that triggered it here. As a + // compromise and to avoid flooding the console with warnings we + // print each deprecation warning only once. + const warn = (property, message) => ({ + get() { + const key = 'get' + property + message; + if (deprecations && deprecations.indexOf(key) < 0) { + deprecations.push(key); + console.warn(`getting vnode.${property} is deprecated, ${message}`); + } + }, + set() { + const key = 'set' + property + message; + if (deprecations && deprecations.indexOf(key) < 0) { + deprecations.push(key); + console.warn(`setting vnode.${property} is not allowed, ${message}`); + } + } + }); + + const deprecatedAttributes = { + nodeName: warn('nodeName', 'use vnode.type'), + attributes: warn('attributes', 'use vnode.props'), + children: warn('children', 'use vnode.props.children') + }; + + const deprecatedProto = Object.create({}, deprecatedAttributes); + + options.vnode = vnode => { + const props = vnode.props; + if ( + vnode.type !== null && + props != null && + ('__source' in props || '__self' in props) + ) { + const newProps = (vnode.props = {}); + for (let i in props) { + const v = props[i]; + if (i === '__source') vnode.__source = v; + else if (i === '__self') vnode.__self = v; + else newProps[i] = v; + } + } + + // eslint-disable-next-line + vnode.__proto__ = deprecatedProto; + if (oldVnode) oldVnode(vnode); + }; + + options.diffed = vnode => { + // Check if the user passed plain objects as children. Note that we cannot + // move this check into `options.vnode` because components can receive + // children in any shape they want (e.g. + // `<MyJSONFormatter>{{ foo: 123, bar: "abc" }}</MyJSONFormatter>`). + // Putting this check in `options.diffed` ensures that + // `vnode._children` is set and that we only validate the children + // that were actually rendered. + if (vnode._children) { + vnode._children.forEach(child => { + if (child && child.type === undefined) { + // Remove internal vnode keys that will always be patched + delete child._parent; + delete child._depth; + const keys = Object.keys(child).join(','); + throw new Error( + `Objects are not valid as a child. Encountered an object with the keys {${keys}}.` + + `\n\n${getOwnerStack(vnode)}` + ); + } + }); + } + + hooksAllowed = false; + + if (oldDiffed) oldDiffed(vnode); + + if (vnode._children != null) { + const keys = []; + for (let i = 0; i < vnode._children.length; i++) { + const child = vnode._children[i]; + if (!child || child.key == null) continue; + + const key = child.key; + if (keys.indexOf(key) !== -1) { + console.error( + 'Following component has two or more children with the ' + + `same key attribute: "${key}". This may cause glitches and misbehavior ` + + 'in rendering process. Component: \n\n' + + serializeVNode(vnode) + + `\n\n${getOwnerStack(vnode)}` + ); + + // Break early to not spam the console + break; + } + + keys.push(key); + } + } + }; +} + +const setState = Component.prototype.setState; +Component.prototype.setState = function(update, callback) { + if (this._vnode == null) { + // `this._vnode` will be `null` during componentWillMount. But it + // is perfectly valid to call `setState` during cWM. So we + // need an additional check to verify that we are dealing with a + // call inside constructor. + if (this.state == null) { + console.warn( + `Calling "this.setState" inside the constructor of a component is a ` + + `no-op and might be a bug in your application. Instead, set ` + + `"this.state = {}" directly.\n\n${getOwnerStack(getCurrentVNode())}` + ); + } + } else if (this._parentDom == null) { + console.warn( + `Can't call "this.setState" on an unmounted component. This is a no-op, ` + + `but it indicates a memory leak in your application. To fix, cancel all ` + + `subscriptions and asynchronous tasks in the componentWillUnmount method.` + + `\n\n${getOwnerStack(this._vnode)}` + ); + } + + return setState.call(this, update, callback); +}; + +const forceUpdate = Component.prototype.forceUpdate; +Component.prototype.forceUpdate = function(callback) { + if (this._vnode == null) { + console.warn( + `Calling "this.forceUpdate" inside the constructor of a component is a ` + + `no-op and might be a bug in your application.\n\n${getOwnerStack( + getCurrentVNode() + )}` + ); + } else if (this._parentDom == null) { + console.warn( + `Can't call "this.forceUpdate" on an unmounted component. This is a no-op, ` + + `but it indicates a memory leak in your application. To fix, cancel all ` + + `subscriptions and asynchronous tasks in the componentWillUnmount method.` + + `\n\n${getOwnerStack(this._vnode)}` + ); + } + return forceUpdate.call(this, callback); +}; + +/** + * Serialize a vnode tree to a string + * @param {import('./internal').VNode} vnode + * @returns {string} + */ +export function serializeVNode(vnode) { + let { props } = vnode; + let name = getDisplayName(vnode); + + let attrs = ''; + for (let prop in props) { + if (props.hasOwnProperty(prop) && prop !== 'children') { + let value = props[prop]; + + // If it is an object but doesn't have toString(), use Object.toString + if (typeof value == 'function') { + value = `function ${value.displayName || value.name}() {}`; + } + + value = + Object(value) === value && !value.toString + ? Object.prototype.toString.call(value) + : value + ''; + + attrs += ` ${prop}=${JSON.stringify(value)}`; + } + } + + let children = props.children; + return `<${name}${attrs}${ + children && children.length ? '>..</' + name + '>' : ' />' + }`; +} |