diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index b8e7612469bc1..ca2834be0fcac 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -50,8 +50,5 @@ export { DEPRECATED_createResponder, // enableScopeAPI unstable_createScope, - // enableJSXTransformAPI - jsx, - jsxs, - jsxDEV, } from './src/React'; +export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 67f222b9dc90c..f4fa5cffd9060 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -45,8 +45,5 @@ export { unstable_withSuspenseConfig, // enableBlocksAPI block, - // enableJSXTransformAPI - jsx, - jsxs, - jsxDEV, } from './src/React'; +export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/index.js b/packages/react/index.js index d139aa877d46d..b294a2914f772 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -76,7 +76,5 @@ export { DEPRECATED_createResponder, unstable_createFundamental, unstable_createScope, - jsx, - jsxs, - jsxDEV, } from './src/React'; +export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 0164f74b8e249..c42da6dd343af 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -49,8 +49,5 @@ export { DEPRECATED_createResponder, // enableScopeAPI unstable_createScope, - // enableJSXTransformAPI - jsx, - jsxs, - jsxDEV, } from './src/React'; +export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/jsx-dev-runtime.js b/packages/react/jsx-dev-runtime.js new file mode 100644 index 0000000000000..ed09edab4f8a0 --- /dev/null +++ b/packages/react/jsx-dev-runtime.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export {Fragment, jsxDEV} from './src/jsx/ReactJSX'; diff --git a/packages/react/jsx-runtime.js b/packages/react/jsx-runtime.js new file mode 100644 index 0000000000000..ebae0a4c54432 --- /dev/null +++ b/packages/react/jsx-runtime.js @@ -0,0 +1,9 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +export {Fragment, jsx, jsxs} from './src/jsx/ReactJSX'; diff --git a/packages/react/npm/jsx-dev-runtime.js b/packages/react/npm/jsx-dev-runtime.js new file mode 100644 index 0000000000000..324eb11e5992d --- /dev/null +++ b/packages/react/npm/jsx-dev-runtime.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-jsx-dev-runtime.production.min.js'); +} else { + module.exports = require('./cjs/react-jsx-dev-runtime.development.js'); +} diff --git a/packages/react/npm/jsx-runtime.js b/packages/react/npm/jsx-runtime.js new file mode 100644 index 0000000000000..0416df54291a8 --- /dev/null +++ b/packages/react/npm/jsx-runtime.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-jsx-runtime.production.min.js'); +} else { + module.exports = require('./cjs/react-jsx-runtime.development.js'); +} diff --git a/packages/react/package.json b/packages/react/package.json index 2ffa23333ad2a..466c97ec0efa5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -14,7 +14,9 @@ "build-info.json", "index.js", "cjs/", - "umd/" + "umd/", + "jsx-runtime.js", + "jsx-dev-runtime.js" ], "main": "index.js", "repository": { diff --git a/packages/react/src/React.js b/packages/react/src/React.js index eef2517973b5d..dd369e71c063a 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -24,7 +24,6 @@ import { createFactory as createFactoryProd, cloneElement as cloneElementProd, isValidElement, - jsx as jsxProd, } from './ReactElement'; import {createContext} from './ReactContext'; import {lazy} from './ReactLazy'; @@ -52,9 +51,6 @@ import { createElementWithValidation, createFactoryWithValidation, cloneElementWithValidation, - jsxWithValidation, - jsxWithValidationStatic, - jsxWithValidationDynamic, } from './ReactElementValidator'; import createMutableSource from './createMutableSource'; import ReactSharedInternals from './ReactSharedInternals'; @@ -67,12 +63,6 @@ const createElement = __DEV__ ? createElementWithValidation : createElementProd; const cloneElement = __DEV__ ? cloneElementWithValidation : cloneElementProd; const createFactory = __DEV__ ? createFactoryWithValidation : createFactoryProd; -const jsxDEV = __DEV__ ? jsxWithValidation : undefined; -const jsx = __DEV__ ? jsxWithValidationDynamic : jsxProd; -// we may want to special case jsxs internally to take advantage of static children. -// for now we can ship identical prod functions -const jsxs = __DEV__ ? jsxWithValidationStatic : jsxProd; - const Children = { map, forEach, @@ -127,9 +117,4 @@ export { createFundamental as unstable_createFundamental, // enableScopeAPI createScope as unstable_createScope, - // enableJSXTransformAPI - jsx, - jsxs, - // TODO: jsxDEV should not be exposed as a name. We might want to move it to a different entry point. - jsxDEV, }; diff --git a/packages/react/src/ReactDebugCurrentFrame.js b/packages/react/src/ReactDebugCurrentFrame.js index dd0a2231ef983..0c0d2dacfcc05 100644 --- a/packages/react/src/ReactDebugCurrentFrame.js +++ b/packages/react/src/ReactDebugCurrentFrame.js @@ -23,6 +23,13 @@ export function setCurrentlyValidatingElement(element: null | ReactElement) { } if (__DEV__) { + ReactDebugCurrentFrame.setCurrentlyValidatingElement = function( + element: null | ReactElement, + ) { + if (__DEV__) { + currentlyValidatingElement = element; + } + }; // Stack implementation injected by the current renderer. ReactDebugCurrentFrame.getCurrentStack = (null: null | (() => string)); diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index c81ace3c6b2f7..70f980c563fef 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -21,16 +21,10 @@ import { REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE, } from 'shared/ReactSymbols'; -import {warnAboutSpreadingKeyToJSX} from 'shared/ReactFeatureFlags'; import checkPropTypes from 'shared/checkPropTypes'; import ReactCurrentOwner from './ReactCurrentOwner'; -import { - isValidElement, - createElement, - cloneElement, - jsxDEV, -} from './ReactElement'; +import {isValidElement, createElement, cloneElement} from './ReactElement'; import {setCurrentlyValidatingElement} from './ReactDebugCurrentFrame'; let propTypesMisspellWarningShown; @@ -39,8 +33,6 @@ if (__DEV__) { propTypesMisspellWarningShown = false; } -const hasOwnProperty = Object.prototype.hasOwnProperty; - function getDeclarationErrorAddendum() { if (ReactCurrentOwner.current) { const name = getComponentName(ReactCurrentOwner.current.type); @@ -261,137 +253,6 @@ function validateFragmentProps(fragment) { } } -export function jsxWithValidation( - type, - props, - key, - isStaticChildren, - source, - self, -) { - const validType = isValidElementType(type); - - // We warn in this case but don't throw. We expect the element creation to - // succeed and there will likely be errors in render. - if (!validType) { - let info = ''; - if ( - type === undefined || - (typeof type === 'object' && - type !== null && - Object.keys(type).length === 0) - ) { - info += - ' You likely forgot to export your component from the file ' + - "it's defined in, or you might have mixed up default and named imports."; - } - - const sourceInfo = getSourceInfoErrorAddendum(source); - if (sourceInfo) { - info += sourceInfo; - } else { - info += getDeclarationErrorAddendum(); - } - - let typeString; - if (type === null) { - typeString = 'null'; - } else if (Array.isArray(type)) { - typeString = 'array'; - } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) { - typeString = `<${getComponentName(type.type) || 'Unknown'} />`; - info = - ' Did you accidentally export a JSX literal instead of a component?'; - } else { - typeString = typeof type; - } - - if (__DEV__) { - console.error( - 'React.jsx: type is invalid -- expected a string (for ' + - 'built-in components) or a class/function (for composite ' + - 'components) but got: %s.%s', - typeString, - info, - ); - } - } - - const element = jsxDEV(type, props, key, source, self); - - // The result can be nullish if a mock or a custom function is used. - // TODO: Drop this when these are no longer allowed as the type argument. - if (element == null) { - return element; - } - - // Skip key warning if the type isn't valid since our key validation logic - // doesn't expect a non-string/function type and can throw confusing errors. - // We don't want exception behavior to differ between dev and prod. - // (Rendering will throw with a helpful message and as soon as the type is - // fixed, the key warnings will appear.) - - if (validType) { - const children = props.children; - if (children !== undefined) { - if (isStaticChildren) { - if (Array.isArray(children)) { - for (let i = 0; i < children.length; i++) { - validateChildKeys(children[i], type); - } - - if (Object.freeze) { - Object.freeze(children); - } - } else { - if (__DEV__) { - console.error( - 'React.jsx: Static children should always be an array. ' + - 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + - 'Use the Babel transform instead.', - ); - } - } - } else { - validateChildKeys(children, type); - } - } - } - - if (__DEV__) { - if (warnAboutSpreadingKeyToJSX) { - if (hasOwnProperty.call(props, 'key')) { - console.error( - 'React.jsx: Spreading a key to JSX is a deprecated pattern. ' + - 'Explicitly pass a key after spreading props in your JSX call. ' + - 'E.g. <%s {...props} key={key} />', - getComponentName(type) || 'ComponentName', - ); - } - } - } - - if (type === REACT_FRAGMENT_TYPE) { - validateFragmentProps(element); - } else { - validatePropTypes(element); - } - - return element; -} - -// These two functions exist to still get child warnings in dev -// even with the prod transform. This means that jsxDEV is purely -// opt-in behavior for better messages but that we won't stop -// giving you warnings if you use production apis. -export function jsxWithValidationStatic(type, props, key) { - return jsxWithValidation(type, props, key, true); -} - -export function jsxWithValidationDynamic(type, props, key) { - return jsxWithValidation(type, props, key, false); -} - export function createElementWithValidation(type, props, children) { const validType = isValidElementType(type); diff --git a/packages/react/src/__tests__/ReactElementJSX-test.js b/packages/react/src/__tests__/ReactElementJSX-test.js index 881e72256784a..0ce12c86f6164 100644 --- a/packages/react/src/__tests__/ReactElementJSX-test.js +++ b/packages/react/src/__tests__/ReactElementJSX-test.js @@ -12,6 +12,8 @@ let React; let ReactDOM; let ReactTestUtils; +let JSXRuntime; +let JSXDEVRuntime; // NOTE: We're explicitly not using JSX here. This is intended to test // a new React.jsx api which does not have a JSX transformer yet. @@ -29,6 +31,8 @@ describe('ReactElement.jsx', () => { global.Symbol = undefined; React = require('react'); + JSXRuntime = require('react/jsx-runtime'); + JSXDEVRuntime = require('react/jsx-dev-runtime'); ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils'); }); @@ -37,32 +41,35 @@ describe('ReactElement.jsx', () => { global.Symbol = originalSymbol; }); - if (!__EXPERIMENTAL__) { - it("empty test so Jest doesn't complain", () => {}); - return; - } - it('allows static methods to be called using the type property', () => { class StaticMethodComponentClass extends React.Component { render() { - return React.jsx('div', {}); + return JSXRuntime.jsx('div', {}); } } StaticMethodComponentClass.someStaticMethod = () => 'someReturnValue'; - const element = React.jsx(StaticMethodComponentClass, {}); + const element = JSXRuntime.jsx(StaticMethodComponentClass, {}); expect(element.type.someStaticMethod()).toBe('someReturnValue'); }); it('identifies valid elements', () => { class Component extends React.Component { render() { - return React.jsx('div', {}); + return JSXRuntime.jsx('div', {}); } } - expect(React.isValidElement(React.jsx('div', {}))).toEqual(true); - expect(React.isValidElement(React.jsx(Component, {}))).toEqual(true); + expect(React.isValidElement(JSXRuntime.jsx('div', {}))).toEqual(true); + expect(React.isValidElement(JSXRuntime.jsx(Component, {}))).toEqual(true); + expect( + React.isValidElement(JSXRuntime.jsx(JSXRuntime.Fragment, {})), + ).toEqual(true); + if (__DEV__) { + expect(React.isValidElement(JSXDEVRuntime.jsxDEV('div', {}))).toEqual( + true, + ); + } expect(React.isValidElement(null)).toEqual(false); expect(React.isValidElement(true)).toEqual(false); @@ -83,12 +90,12 @@ describe('ReactElement.jsx', () => { expect(React.isValidElement(Component)).toEqual(false); expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); - const jsonElement = JSON.stringify(React.jsx('div', {})); + const jsonElement = JSON.stringify(JSXRuntime.jsx('div', {})); expect(React.isValidElement(JSON.parse(jsonElement))).toBe(true); }); it('is indistinguishable from a plain object', () => { - const element = React.jsx('div', {className: 'foo'}); + const element = JSXRuntime.jsx('div', {className: 'foo'}); const object = {}; expect(element.constructor).toBe(object.constructor); }); @@ -96,37 +103,37 @@ describe('ReactElement.jsx', () => { it('should use default prop value when removing a prop', () => { class Component extends React.Component { render() { - return React.jsx('span', {}); + return JSXRuntime.jsx('span', {}); } } Component.defaultProps = {fruit: 'persimmon'}; const container = document.createElement('div'); const instance = ReactDOM.render( - React.jsx(Component, {fruit: 'mango'}), + JSXRuntime.jsx(Component, {fruit: 'mango'}), container, ); expect(instance.props.fruit).toBe('mango'); - ReactDOM.render(React.jsx(Component, {}), container); + ReactDOM.render(JSXRuntime.jsx(Component, {}), container); expect(instance.props.fruit).toBe('persimmon'); }); it('should normalize props with default values', () => { class Component extends React.Component { render() { - return React.jsx('span', {children: this.props.prop}); + return JSXRuntime.jsx('span', {children: this.props.prop}); } } Component.defaultProps = {prop: 'testKey'}; const instance = ReactTestUtils.renderIntoDocument( - React.jsx(Component, {}), + JSXRuntime.jsx(Component, {}), ); expect(instance.props.prop).toBe('testKey'); const inst2 = ReactTestUtils.renderIntoDocument( - React.jsx(Component, {prop: null}), + JSXRuntime.jsx(Component, {prop: null}), ); expect(inst2.props.prop).toBe(null); }); @@ -134,7 +141,7 @@ describe('ReactElement.jsx', () => { it('throws when changing a prop (in dev) after element creation', () => { class Outer extends React.Component { render() { - const el = React.jsx('div', {className: 'moo'}); + const el = JSXRuntime.jsx('div', {className: 'moo'}); if (__DEV__) { expect(function() { @@ -150,7 +157,7 @@ describe('ReactElement.jsx', () => { } } const outer = ReactTestUtils.renderIntoDocument( - React.jsx(Outer, {color: 'orange'}), + JSXRuntime.jsx(Outer, {color: 'orange'}), ); if (__DEV__) { expect(ReactDOM.findDOMNode(outer).className).toBe('moo'); @@ -163,7 +170,7 @@ describe('ReactElement.jsx', () => { const container = document.createElement('div'); class Outer extends React.Component { render() { - const el = React.jsx('div', {children: this.props.sound}); + const el = JSXRuntime.jsx('div', {children: this.props.sound}); if (__DEV__) { expect(function() { @@ -179,7 +186,7 @@ describe('ReactElement.jsx', () => { } } Outer.defaultProps = {sound: 'meow'}; - const outer = ReactDOM.render(React.jsx(Outer, {}), container); + const outer = ReactDOM.render(JSXRuntime.jsx(Outer, {}), container); expect(ReactDOM.findDOMNode(outer).textContent).toBe('meow'); if (__DEV__) { expect(ReactDOM.findDOMNode(outer).className).toBe(''); @@ -191,11 +198,11 @@ describe('ReactElement.jsx', () => { it('does not warn for NaN props', () => { class Test extends React.Component { render() { - return React.jsx('div', {}); + return JSXRuntime.jsx('div', {}); } } const test = ReactTestUtils.renderIntoDocument( - React.jsx(Test, {value: +undefined}), + JSXRuntime.jsx(Test, {value: +undefined}), ); expect(test.props.value).toBeNaN(); }); @@ -204,21 +211,23 @@ describe('ReactElement.jsx', () => { const container = document.createElement('div'); class Child extends React.Component { render() { - return React.jsx('div', {children: this.props.key}); + return JSXRuntime.jsx('div', {children: this.props.key}); } } class Parent extends React.Component { render() { - return React.jsxs('div', { + return JSXRuntime.jsxs('div', { children: [ - React.jsx(Child, {}, '0'), - React.jsx(Child, {}, '1'), - React.jsx(Child, {}, '2'), + JSXRuntime.jsx(Child, {}, '0'), + JSXRuntime.jsx(Child, {}, '1'), + JSXRuntime.jsx(Child, {}, '2'), ], }); } } - expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toErrorDev( + expect(() => + ReactDOM.render(JSXRuntime.jsx(Parent, {}), container), + ).toErrorDev( 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + @@ -229,7 +238,10 @@ describe('ReactElement.jsx', () => { it('warns when a jsxs is passed something that is not an array', () => { const container = document.createElement('div'); expect(() => - ReactDOM.render(React.jsxs('div', {children: 'foo'}, null), container), + ReactDOM.render( + JSXRuntime.jsxs('div', {children: 'foo'}, null), + container, + ), ).toErrorDev( 'React.jsx: Static children should always be an array. ' + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + @@ -239,7 +251,7 @@ describe('ReactElement.jsx', () => { }); it('should warn when `key` is being accessed on a host element', () => { - const element = React.jsxs('div', {}, '3'); + const element = JSXRuntime.jsxs('div', {}, '3'); expect( () => void element.props.key, ).toErrorDev( @@ -255,17 +267,19 @@ describe('ReactElement.jsx', () => { const container = document.createElement('div'); class Child extends React.Component { render() { - return React.jsx('div', {children: this.props.ref}); + return JSXRuntime.jsx('div', {children: this.props.ref}); } } class Parent extends React.Component { render() { - return React.jsx('div', { - children: React.jsx(Child, {ref: 'childElement'}), + return JSXRuntime.jsx('div', { + children: JSXRuntime.jsx(Child, {ref: 'childElement'}), }); } } - expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toErrorDev( + expect(() => + ReactDOM.render(JSXRuntime.jsx(Parent, {}), container), + ).toErrorDev( 'Child: `ref` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + @@ -292,15 +306,16 @@ describe('ReactElement.jsx', () => { jest.resetModules(); React = require('react'); + JSXRuntime = require('react/jsx-runtime'); class Component extends React.Component { render() { - return React.jsx('div'); + return JSXRuntime.jsx('div'); } } - expect(React.isValidElement(React.jsx('div', {}))).toEqual(true); - expect(React.isValidElement(React.jsx(Component, {}))).toEqual(true); + expect(React.isValidElement(JSXRuntime.jsx('div', {}))).toEqual(true); + expect(React.isValidElement(JSXRuntime.jsx(Component, {}))).toEqual(true); expect(React.isValidElement(null)).toEqual(false); expect(React.isValidElement(true)).toEqual(false); @@ -321,29 +336,32 @@ describe('ReactElement.jsx', () => { expect(React.isValidElement(Component)).toEqual(false); expect(React.isValidElement({type: 'div', props: {}})).toEqual(false); - const jsonElement = JSON.stringify(React.jsx('div', {})); + const jsonElement = JSON.stringify(JSXRuntime.jsx('div', {})); expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false); }); it('should warn when unkeyed children are passed to jsx', () => { const container = document.createElement('div'); + class Child extends React.Component { render() { - return React.jsx('div', {}); + return JSXRuntime.jsx('div', {}); } } class Parent extends React.Component { render() { - return React.jsx('div', { + return JSXRuntime.jsx('div', { children: [ - React.jsx(Child, {}), - React.jsx(Child, {}), - React.jsx(Child, {}), + JSXRuntime.jsx(Child, {}), + JSXRuntime.jsx(Child, {}), + JSXRuntime.jsx(Child, {}), ], }); } } - expect(() => ReactDOM.render(React.jsx(Parent, {}), container)).toErrorDev( + expect(() => + ReactDOM.render(JSXRuntime.jsx(Parent, {}), container), + ).toErrorDev( 'Warning: Each child in a list should have a unique "key" prop.\n\n' + 'Check the render method of `Parent`. See https://fb.me/react-warning-keys for more information.\n' + ' in Child (created by Parent)\n' + @@ -356,18 +374,18 @@ describe('ReactElement.jsx', () => { const container = document.createElement('div'); class Child extends React.Component { render() { - return React.jsx('div', {}); + return JSXRuntime.jsx('div', {}); } } class Parent extends React.Component { render() { - return React.jsx('div', { - children: [React.jsx(Child, {key: '0'})], + return JSXRuntime.jsx('div', { + children: [JSXRuntime.jsx(Child, {key: '0'})], }); } } expect(() => - ReactDOM.render(React.jsx(Parent, {}), container), + ReactDOM.render(JSXRuntime.jsx(Parent, {}), container), ).toErrorDev( 'Warning: React.jsx: Spreading a key to JSX is a deprecated pattern. ' + 'Explicitly pass a key after spreading props in your JSX call. ' + @@ -380,21 +398,21 @@ describe('ReactElement.jsx', () => { const container = document.createElement('div'); class Child extends React.Component { render() { - return React.jsx('div', {}); + return JSXRuntime.jsx('div', {}); } } class Parent extends React.Component { render() { - return React.jsxs('div', { + return JSXRuntime.jsxs('div', { children: [ - React.jsx(Child, {}), - React.jsx(Child, {}), - React.jsx(Child, {}), + JSXRuntime.jsx(Child, {}), + JSXRuntime.jsx(Child, {}), + JSXRuntime.jsx(Child, {}), ], }); } } // TODO: an explicit expect for no warning? - ReactDOM.render(React.jsx(Parent, {}), container); + ReactDOM.render(JSXRuntime.jsx(Parent, {}), container); }); }); diff --git a/packages/react/src/jsx/ReactJSX.js b/packages/react/src/jsx/ReactJSX.js new file mode 100644 index 0000000000000..a4e8f63dedd74 --- /dev/null +++ b/packages/react/src/jsx/ReactJSX.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols'; +import { + jsxWithValidationStatic, + jsxWithValidationDynamic, + jsxWithValidation, +} from './ReactJSXElementValidator'; +import {jsx as jsxProd} from './ReactJSXElement'; +const jsx = __DEV__ ? jsxWithValidationDynamic : jsxProd; +// we may want to special case jsxs internally to take advantage of static children. +// for now we can ship identical prod functions +const jsxs = __DEV__ ? jsxWithValidationStatic : jsxProd; +const jsxDEV = __DEV__ ? jsxWithValidation : undefined; + +export {REACT_FRAGMENT_TYPE as Fragment, jsx, jsxs, jsxDEV}; diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js new file mode 100644 index 0000000000000..139af3fbd764b --- /dev/null +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -0,0 +1,358 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import getComponentName from 'shared/getComponentName'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +const RESERVED_PROPS = { + key: true, + ref: true, + __self: true, + __source: true, +}; + +let specialPropKeyWarningShown, + specialPropRefWarningShown, + didWarnAboutStringRefs; + +if (__DEV__) { + didWarnAboutStringRefs = {}; +} + +function hasValidRef(config) { + if (__DEV__) { + if (hasOwnProperty.call(config, 'ref')) { + const getter = Object.getOwnPropertyDescriptor(config, 'ref').get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.ref !== undefined; +} + +function hasValidKey(config) { + if (__DEV__) { + if (hasOwnProperty.call(config, 'key')) { + const getter = Object.getOwnPropertyDescriptor(config, 'key').get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.key !== undefined; +} + +function warnIfStringRefCannotBeAutoConverted(config) { + if (__DEV__) { + if ( + typeof config.ref === 'string' && + ReactCurrentOwner.current && + config.__self && + ReactCurrentOwner.current.stateNode !== config.__self + ) { + const componentName = getComponentName(ReactCurrentOwner.current.type); + + if (!didWarnAboutStringRefs[componentName]) { + console.error( + 'Component "%s" contains the string ref "%s". ' + + 'Support for string refs will be removed in a future major release. ' + + 'This case cannot be automatically converted to an arrow function. ' + + 'We ask you to manually fix this case by using useRef() or createRef() instead. ' + + 'Learn more about using refs safely here: ' + + 'https://fb.me/react-strict-mode-string-ref', + getComponentName(ReactCurrentOwner.current.type), + config.ref, + ); + didWarnAboutStringRefs[componentName] = true; + } + } + } +} + +function defineKeyPropWarningGetter(props, displayName) { + const warnAboutAccessingKey = function() { + if (__DEV__) { + if (!specialPropKeyWarningShown) { + specialPropKeyWarningShown = true; + console.error( + '%s: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + displayName, + ); + } + } + }; + warnAboutAccessingKey.isReactWarning = true; + Object.defineProperty(props, 'key', { + get: warnAboutAccessingKey, + configurable: true, + }); +} + +function defineRefPropWarningGetter(props, displayName) { + const warnAboutAccessingRef = function() { + if (__DEV__) { + if (!specialPropRefWarningShown) { + specialPropRefWarningShown = true; + console.error( + '%s: `ref` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://fb.me/react-special-props)', + displayName, + ); + } + } + }; + warnAboutAccessingRef.isReactWarning = true; + Object.defineProperty(props, 'ref', { + get: warnAboutAccessingRef, + configurable: true, + }); +} + +/** + * Factory method to create a new React element. This no longer adheres to + * the class pattern, so do not use new to call it. Also, instanceof check + * will not work. Instead test $$typeof field against Symbol.for('react.element') to check + * if something is a React Element. + * + * @param {*} type + * @param {*} props + * @param {*} key + * @param {string|object} ref + * @param {*} owner + * @param {*} self A *temporary* helper to detect places where `this` is + * different from the `owner` when React.createElement is called, so that we + * can warn. We want to get rid of owner and replace string `ref`s with arrow + * functions, and as long as `this` and owner are the same, there will be no + * change in behavior. + * @param {*} source An annotation object (added by a transpiler or otherwise) + * indicating filename, line number, and/or other information. + * @internal + */ +const ReactElement = function(type, key, ref, self, source, owner, props) { + const element = { + // This tag allows us to uniquely identify this as a React Element + $$typeof: REACT_ELEMENT_TYPE, + + // Built-in properties that belong on the element + type: type, + key: key, + ref: ref, + props: props, + + // Record the component responsible for creating this element. + _owner: owner, + }; + + if (__DEV__) { + // The validation flag is currently mutative. We put it on + // an external backing store so that we can freeze the whole object. + // This can be replaced with a WeakMap once they are implemented in + // commonly used development environments. + element._store = {}; + + // To make comparing ReactElements easier for testing purposes, we make + // the validation flag non-enumerable (where possible, which should + // include every environment we run tests in), so the test framework + // ignores it. + Object.defineProperty(element._store, 'validated', { + configurable: false, + enumerable: false, + writable: true, + value: false, + }); + // self and source are DEV only properties. + Object.defineProperty(element, '_self', { + configurable: false, + enumerable: false, + writable: false, + value: self, + }); + // Two elements created in two different places should be considered + // equal for testing purposes and therefore we hide it from enumeration. + Object.defineProperty(element, '_source', { + configurable: false, + enumerable: false, + writable: false, + value: source, + }); + if (Object.freeze) { + Object.freeze(element.props); + Object.freeze(element); + } + } + + return element; +}; + +/** + * https://github.com/reactjs/rfcs/pull/107 + * @param {*} type + * @param {object} props + * @param {string} key + */ +export function jsx(type, config, maybeKey) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + + // Currently, key can be spread in as a prop. This causes a potential + // issue if key is also explicitly declared (ie.
+ // or
). We want to deprecate key spread, + // but as an intermediary step, we will use jsxDEV for everything except + //
, because we aren't currently able to tell if + // key is explicitly declared to be undefined or not. + if (maybeKey !== undefined) { + key = '' + maybeKey; + } + + if (hasValidKey(config)) { + key = '' + config.key; + } + + if (hasValidRef(config)) { + ref = config.ref; + } + + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + + return ReactElement( + type, + key, + ref, + undefined, + undefined, + ReactCurrentOwner.current, + props, + ); +} + +/** + * https://github.com/reactjs/rfcs/pull/107 + * @param {*} type + * @param {object} props + * @param {string} key + */ +export function jsxDEV(type, config, maybeKey, source, self) { + let propName; + + // Reserved names are extracted + const props = {}; + + let key = null; + let ref = null; + + // Currently, key can be spread in as a prop. This causes a potential + // issue if key is also explicitly declared (ie.
+ // or
). We want to deprecate key spread, + // but as an intermediary step, we will use jsxDEV for everything except + //
, because we aren't currently able to tell if + // key is explicitly declared to be undefined or not. + if (maybeKey !== undefined) { + key = '' + maybeKey; + } + + if (hasValidKey(config)) { + key = '' + config.key; + } + + if (hasValidRef(config)) { + ref = config.ref; + warnIfStringRefCannotBeAutoConverted(config); + } + + // Remaining properties are added to a new props object + for (propName in config) { + if ( + hasOwnProperty.call(config, propName) && + !RESERVED_PROPS.hasOwnProperty(propName) + ) { + props[propName] = config[propName]; + } + } + + // Resolve default props + if (type && type.defaultProps) { + const defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + + if (key || ref) { + const displayName = + typeof type === 'function' + ? type.displayName || type.name || 'Unknown' + : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } + } + + return ReactElement( + type, + key, + ref, + self, + source, + ReactCurrentOwner.current, + props, + ); +} + +/** + * Verifies the object is a ReactElement. + * See https://reactjs.org/docs/react-api.html#isvalidelement + * @param {?object} object + * @return {boolean} True if `object` is a ReactElement. + * @final + */ +export function isValidElement(object) { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_ELEMENT_TYPE + ); +} diff --git a/packages/react/src/jsx/ReactJSXElementValidator.js b/packages/react/src/jsx/ReactJSXElementValidator.js new file mode 100644 index 0000000000000..b629524857e95 --- /dev/null +++ b/packages/react/src/jsx/ReactJSXElementValidator.js @@ -0,0 +1,382 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * ReactElementValidator provides a wrapper around a element factory + * which validates the props passed to the element. This is intended to be + * used only in DEV and could be replaced by a static type checker for languages + * that support it. + */ +import isValidElementType from 'shared/isValidElementType'; +import getComponentName from 'shared/getComponentName'; +import checkPropTypes from 'shared/checkPropTypes'; +import { + getIteratorFn, + REACT_FORWARD_REF_TYPE, + REACT_MEMO_TYPE, + REACT_FRAGMENT_TYPE, + REACT_ELEMENT_TYPE, +} from 'shared/ReactSymbols'; +import {warnAboutSpreadingKeyToJSX} from 'shared/ReactFeatureFlags'; + +import {isValidElement, jsxDEV} from './ReactJSXElement'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; +const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; + +let propTypesMisspellWarningShown; + +if (__DEV__) { + propTypesMisspellWarningShown = false; +} + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +function getDeclarationErrorAddendum() { + if (ReactCurrentOwner.current) { + const name = getComponentName(ReactCurrentOwner.current.type); + if (name) { + return '\n\nCheck the render method of `' + name + '`.'; + } + } + return ''; +} + +function getSourceInfoErrorAddendum(source) { + if (source !== undefined) { + const fileName = source.fileName.replace(/^.*[\\\/]/, ''); + const lineNumber = source.lineNumber; + return '\n\nCheck your code at ' + fileName + ':' + lineNumber + '.'; + } + return ''; +} + +/** + * Warn if there's no key explicitly set on dynamic arrays of children or + * object keys are not valid. This allows us to keep track of children between + * updates. + */ +const ownerHasKeyUseWarning = {}; + +function getCurrentComponentErrorInfo(parentType) { + let info = getDeclarationErrorAddendum(); + + if (!info) { + const parentName = + typeof parentType === 'string' + ? parentType + : parentType.displayName || parentType.name; + if (parentName) { + info = `\n\nCheck the top-level render call using <${parentName}>.`; + } + } + return info; +} + +/** + * Warn if the element doesn't have an explicit key assigned to it. + * This element is in an array. The array could grow and shrink or be + * reordered. All children that haven't already been validated are required to + * have a "key" property assigned to it. Error statuses are cached so a warning + * will only be shown once. + * + * @internal + * @param {ReactElement} element Element that requires a key. + * @param {*} parentType element's parent's type. + */ +function validateExplicitKey(element, parentType) { + if (!element._store || element._store.validated || element.key != null) { + return; + } + element._store.validated = true; + + const currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType); + if (ownerHasKeyUseWarning[currentComponentErrorInfo]) { + return; + } + ownerHasKeyUseWarning[currentComponentErrorInfo] = true; + + // Usually the current owner is the offender, but if it accepts children as a + // property, it may be the creator of the child that's responsible for + // assigning it a key. + let childOwner = ''; + if ( + element && + element._owner && + element._owner !== ReactCurrentOwner.current + ) { + // Give the component that originally created this child. + childOwner = ` It was passed a child from ${getComponentName( + element._owner.type, + )}.`; + } + + ReactDebugCurrentFrame.setCurrentlyValidatingElement(element); + if (__DEV__) { + console.error( + 'Each child in a list should have a unique "key" prop.' + + '%s%s See https://fb.me/react-warning-keys for more information.', + currentComponentErrorInfo, + childOwner, + ); + } + ReactDebugCurrentFrame.setCurrentlyValidatingElement(null); +} + +/** + * Ensure that every element either is passed in a static location, in an + * array with an explicit keys property defined, or in an object literal + * with valid key property. + * + * @internal + * @param {ReactNode} node Statically passed child of any type. + * @param {*} parentType node's parent's type. + */ +function validateChildKeys(node, parentType) { + if (typeof node !== 'object') { + return; + } + if (Array.isArray(node)) { + for (let i = 0; i < node.length; i++) { + const child = node[i]; + if (isValidElement(child)) { + validateExplicitKey(child, parentType); + } + } + } else if (isValidElement(node)) { + // This element was passed in a valid location. + if (node._store) { + node._store.validated = true; + } + } else if (node) { + const iteratorFn = getIteratorFn(node); + if (typeof iteratorFn === 'function') { + // Entry iterators used to provide implicit keys, + // but now we print a separate warning for them later. + if (iteratorFn !== node.entries) { + const iterator = iteratorFn.call(node); + let step; + while (!(step = iterator.next()).done) { + if (isValidElement(step.value)) { + validateExplicitKey(step.value, parentType); + } + } + } + } + } +} + +/** + * Given an element, validate that its props follow the propTypes definition, + * provided by the type. + * + * @param {ReactElement} element + */ +function validatePropTypes(element) { + if (__DEV__) { + const type = element.type; + if (type === null || type === undefined || typeof type === 'string') { + return; + } + const name = getComponentName(type); + let propTypes; + if (typeof type === 'function') { + propTypes = type.propTypes; + } else if ( + typeof type === 'object' && + (type.$$typeof === REACT_FORWARD_REF_TYPE || + // Note: Memo only checks outer props here. + // Inner props are checked in the reconciler. + type.$$typeof === REACT_MEMO_TYPE) + ) { + propTypes = type.propTypes; + } else { + return; + } + if (propTypes) { + ReactDebugCurrentFrame.setCurrentlyValidatingElement(element); + checkPropTypes(propTypes, element.props, 'prop', name); + ReactDebugCurrentFrame.setCurrentlyValidatingElement(null); + } else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) { + propTypesMisspellWarningShown = true; + console.error( + 'Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?', + name || 'Unknown', + ); + } + if ( + typeof type.getDefaultProps === 'function' && + !type.getDefaultProps.isReactClassApproved + ) { + console.error( + 'getDefaultProps is only used on classic React.createClass ' + + 'definitions. Use a static property named `defaultProps` instead.', + ); + } + } +} + +/** + * Given a fragment, validate that it can only be provided with fragment props + * @param {ReactElement} fragment + */ +function validateFragmentProps(fragment) { + if (__DEV__) { + ReactDebugCurrentFrame.setCurrentlyValidatingElement(fragment); + + const keys = Object.keys(fragment.props); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (key !== 'children' && key !== 'key') { + console.error( + 'Invalid prop `%s` supplied to `React.Fragment`. ' + + 'React.Fragment can only have `key` and `children` props.', + key, + ); + break; + } + } + + if (fragment.ref !== null) { + console.error('Invalid attribute `ref` supplied to `React.Fragment`.'); + } + + ReactDebugCurrentFrame.setCurrentlyValidatingElement(null); + } +} + +export function jsxWithValidation( + type, + props, + key, + isStaticChildren, + source, + self, +) { + const validType = isValidElementType(type); + + // We warn in this case but don't throw. We expect the element creation to + // succeed and there will likely be errors in render. + if (!validType) { + let info = ''; + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in, or you might have mixed up default and named imports."; + } + + const sourceInfo = getSourceInfoErrorAddendum(source); + if (sourceInfo) { + info += sourceInfo; + } else { + info += getDeclarationErrorAddendum(); + } + + let typeString; + if (type === null) { + typeString = 'null'; + } else if (Array.isArray(type)) { + typeString = 'array'; + } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) { + typeString = `<${getComponentName(type.type) || 'Unknown'} />`; + info = + ' Did you accidentally export a JSX literal instead of a component?'; + } else { + typeString = typeof type; + } + + if (__DEV__) { + console.error( + 'React.jsx: type is invalid -- expected a string (for ' + + 'built-in components) or a class/function (for composite ' + + 'components) but got: %s.%s', + typeString, + info, + ); + } + } + + const element = jsxDEV(type, props, key, source, self); + + // The result can be nullish if a mock or a custom function is used. + // TODO: Drop this when these are no longer allowed as the type argument. + if (element == null) { + return element; + } + + // Skip key warning if the type isn't valid since our key validation logic + // doesn't expect a non-string/function type and can throw confusing errors. + // We don't want exception behavior to differ between dev and prod. + // (Rendering will throw with a helpful message and as soon as the type is + // fixed, the key warnings will appear.) + + if (validType) { + const children = props.children; + if (children !== undefined) { + if (isStaticChildren) { + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + validateChildKeys(children[i], type); + } + + if (Object.freeze) { + Object.freeze(children); + } + } else { + if (__DEV__) { + console.error( + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + ); + } + } + } else { + validateChildKeys(children, type); + } + } + } + + if (__DEV__) { + if (warnAboutSpreadingKeyToJSX) { + if (hasOwnProperty.call(props, 'key')) { + console.error( + 'React.jsx: Spreading a key to JSX is a deprecated pattern. ' + + 'Explicitly pass a key after spreading props in your JSX call. ' + + 'E.g. <%s {...props} key={key} />', + getComponentName(type) || 'ComponentName', + ); + } + } + } + + if (type === REACT_FRAGMENT_TYPE) { + validateFragmentProps(element); + } else { + validatePropTypes(element); + } + + return element; +} + +// These two functions exist to still get child warnings in dev +// even with the prod transform. This means that jsxDEV is purely +// opt-in behavior for better messages but that we won't stop +// giving you warnings if you use production apis. +export function jsxWithValidationStatic(type, props, key) { + return jsxWithValidation(type, props, key, true); +} + +export function jsxWithValidationDynamic(type, props, key) { + return jsxWithValidation(type, props, key, false); +} diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 379b3fcbbaa69..d8c2e55b21ed0 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -76,6 +76,38 @@ const bundles = [ externals: [], }, + /******* React JSX Runtime *******/ + { + bundleTypes: [ + NODE_DEV, + NODE_PROD, + NODE_PROFILING, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + ], + moduleType: ISOMORPHIC, + entry: 'react/jsx-runtime', + global: 'JSXRuntime', + externals: ['react'], + }, + + /******* React JSX DEV Runtime *******/ + { + bundleTypes: [ + NODE_DEV, + NODE_PROD, + NODE_PROFILING, + FB_WWW_DEV, + FB_WWW_PROD, + FB_WWW_PROFILING, + ], + moduleType: ISOMORPHIC, + entry: 'react/jsx-dev-runtime', + global: 'JSXDEVRuntime', + externals: ['react'], + }, + /******* React DOM *******/ { bundleTypes: [ diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js index 01fd4fee5aade..99b7fa26b7b22 100644 --- a/scripts/rollup/forks.js +++ b/scripts/rollup/forks.js @@ -53,7 +53,7 @@ const forks = Object.freeze({ if (entry === 'react') { return 'react/src/ReactSharedInternals'; } - if (dependencies.indexOf('react') === -1) { + if (!entry.startsWith('react/') && dependencies.indexOf('react') === -1) { // React internals are unavailable if we can't reference the package. // We return an error because we only want to throw if this module gets used. return new Error( diff --git a/scripts/rollup/modules.js b/scripts/rollup/modules.js index 8b188c30ac18c..59b29457294c8 100644 --- a/scripts/rollup/modules.js +++ b/scripts/rollup/modules.js @@ -14,6 +14,7 @@ const importSideEffects = Object.freeze({ scheduler: HAS_NO_SIDE_EFFECTS_ON_IMPORT, 'scheduler/tracing': HAS_NO_SIDE_EFFECTS_ON_IMPORT, 'react-dom/server': HAS_NO_SIDE_EFFECTS_ON_IMPORT, + 'react/jsx-dev-runtime': HAS_NO_SIDE_EFFECTS_ON_IMPORT, }); // Bundles exporting globals that other modules rely on.