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, isNaN } from './util'; const isWeakMapSupported = typeof WeakMap == 'function'; /** * @param {import('./internal').VNode} vnode * @returns {Array} */ function getDomChildren(vnode) { let domChildren = []; if (!vnode._children) return domChildren; vnode._children.forEach(child => { if (child && typeof child.type === 'function') { domChildren.push.apply(domChildren, getDomChildren(child)); } else if (child && typeof child.type === 'string') { domChildren.push(child.type); } }); return domChildren; } /** * @param {import('./internal').VNode} parent * @returns {string} */ function getClosestDomNodeParentName(parent) { if (!parent) return ''; if (typeof parent.type == 'function') { if (parent._parent === null) { if (parent._dom !== null && parent._dom.parentNode !== null) { return parent._dom.parentNode.localName; } return ''; } return getClosestDomNodeParentName(parent._parent); } return /** @type {string} */ (parent.type); } export function initDebug() { setupComponentStack(); let hooksAllowed = false; /* eslint-disable no-console */ let oldBeforeDiff = options._diff; let oldDiffed = options.diffed; let oldVnode = options.vnode; let oldRender = options._render; 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, errorInfo) => { 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 { errorInfo = errorInfo || {}; errorInfo.componentStack = getOwnerStack(vnode); oldCatchError(error, vnode, oldVNode, errorInfo); // when an error was handled by an ErrorBoundary we will nonetheless 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 } = vnode; 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 ( 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._render = vnode => { if (oldRender) { oldRender(vnode); } hooksAllowed = true; }; 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 => { const { type, _parent: parent } = 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 (typeof child === 'object' && child && child.type === undefined) { 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)}` ); } }); } if (typeof type === 'string' && (isTableElement(type) || type === 'p')) { // Avoid false positives when Preact only partially rendered the // HTML tree. Whilst we attempt to include the outer DOM in our // validation, this wouldn't work on the server for // `preact-render-to-string`. There we'd otherwise flood the terminal // with false positives, which we'd like to avoid. let domParentName = getClosestDomNodeParentName(parent); if (domParentName !== '') { if ( type === 'table' && // Tables can be nested inside each other if it's inside a cell. // See https://developer.mozilla.org/en-US/docs/Learn/HTML/Tables/Advanced#nesting_tables domParentName !== 'td' && isTableElement(domParentName) ) { console.log(domParentName, parent._dom); console.error( 'Improper nesting of table. Your should not have a table-node parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if ( (type === 'thead' || type === 'tfoot' || type === 'tbody') && domParentName !== 'table' ) { console.error( 'Improper nesting of table. Your should have a
parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if ( type === 'tr' && domParentName !== 'thead' && domParentName !== 'tfoot' && domParentName !== 'tbody' && domParentName !== 'table' ) { console.error( 'Improper nesting of table. Your should have a parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type === 'td' && domParentName !== 'tr') { console.error( 'Improper nesting of table. Your parent.' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } else if (type === 'th' && domParentName !== 'tr') { console.error( 'Improper nesting of table. Your .' + serializeVNode(vnode) + `\n\n${getOwnerStack(vnode)}` ); } } else if (type === 'p') { let illegalDomChildrenTypes = getDomChildren(vnode).filter(childType => ILLEGAL_PARAGRAPH_CHILD_ELEMENTS.test(childType) ); if (illegalDomChildrenTypes.length) { console.error( 'Improper nesting of paragraph. Your

should not have ' + illegalDomChildrenTypes.join(', ') + 'as child-elements.' + serializeVNode(vnode) + `\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); } } if (vnode._component != null && vnode._component.__hooks != null) { // Validate that none of the hooks in this component contain arguments that are NaN. // This is a common mistake that can be hard to debug, so we want to catch it early. const hooks = vnode._component.__hooks._list; if (hooks) { for (let i = 0; i < hooks.length; i += 1) { const hook = hooks[i]; if (hook._args) { for (let j = 0; j < hook._args.length; j++) { const arg = hook._args[j]; if (isNaN(arg)) { const componentName = getDisplayName(vnode); throw new Error( `Invalid argument passed to hook. Hooks should not be called with NaN in the dependency array. Hook index ${i} in component ${componentName} was called with NaN.` ); } } } } } } }; } 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())}` ); } } return setState.call(this, update, callback); }; function isTableElement(type) { return ( type === 'table' || type === 'tfoot' || type === 'tbody' || type === 'thead' || type === 'td' || type === 'tr' || type === 'th' ); } const ILLEGAL_PARAGRAPH_CHILD_ELEMENTS = /^(address|article|aside|blockquote|details|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|main|menu|nav|ol|p|pre|search|section|table|ul)$/; 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