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 = ;\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 should have a 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 should have a parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type === 'td' && parentVNode.type !== 'tr') { console.error( 'Improper nesting of table. Your parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type === 'th' && parentVNode.type !== 'tr') { console.error( 'Improper nesting of table. Your .' + 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. // `{{ foo: 123, bar: "abc" }}`). // 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 ? '>..' : ' />' }`; }
should have a
should have a