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 < string > }
* /
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 ;
let oldCommit = options . _ commit ;
const warnedComponents = ! isWeakMapSupported
? null
: {
useEffect : new WeakMap ( ) ,
useLayoutEffect : new WeakMap ( ) ,
lazyPropTypes : new WeakMap ( )
} ;
const deprecations = [ ] ;
/** @type {import("./internal.d.ts").VNode[]} */
let checkVNodeDom = [ ] ;
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 ;
if ( ( typeof type === 'string' && isTableElement ( type ) ) || type === 'p' ) {
checkVNodeDom . push ( 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 = <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 (
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 => {
// 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 ( 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 ) } `
) ;
}
} ) ;
}
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. `
) ;
}
}
}
}
}
}
} ;
options . _ commit = ( root , queue ) => {
for ( let i = 0 ; i < checkVNodeDom . length ; i ++ ) {
const vnode = checkVNodeDom [ i ] ;
// Check if HTML nesting is valid. We need to do it in `options.diffed`
// so that we can optionally traverse outside the vdom root in case
// it's an island embedded in an existing (and valid) HTML tree.
const { type , _ parent : parent } = vnode ;
let domParentName = getClosestDomNodeParentName ( parent ) ;
if ( type === 'table' && isTableElement ( domParentName ) ) {
console . error (
'Improper nesting of table. Your <table> 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 <thead/tbody/tfoot> should have a <table> 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 <tr> should have a <thead/tbody/tfoot/table> parent.' +
serializeVNode ( vnode ) +
` \n \n ${ getOwnerStack ( vnode ) } `
) ;
} else if ( type === 'td' && domParentName !== 'tr' ) {
console . error (
'Improper nesting of table. Your <td> should have a <tr> parent.' +
serializeVNode ( vnode ) +
` \n \n ${ getOwnerStack ( vnode ) } `
) ;
} else if ( type === 'th' && domParentName !== 'tr' ) {
console . error (
'Improper nesting of table. Your <th> should have a <tr>.' +
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 <p> should not have ' +
illegalDomChildrenTypes . join ( ', ' ) +
'as child-elements.' +
serializeVNode ( vnode ) +
` \n \n ${ getOwnerStack ( vnode ) } `
) ;
}
}
}
checkVNodeDom = [ ] ;
if ( oldCommit ) oldCommit ( root , queue ) ;
} ;
}
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 ? '>..</' + name + '>' : ' />'
} ` ;
}